Matteo Ronchi e Fabio Biondi ci spiegano come padroneggiare le tecniche per creare componenti ed architetture avanzate in AngularJS.
Iscriviti qui per partecipare ad altri Tech Webinar: http://goo.gl/iW81VD
Per info sul Bootcamp AngularJS Advanced Class clicca qui: https://goo.gl/NHbv9D
Scrivici a training@codemotion.it
Tw: @codemotionTR
3. Matteo Ronchi, web architect e sviluppatore full-
stack fin dal 2003 realizza applicazioni web, desktop
e mobile. Si occupa quotidianamente di architetture
web e scalabilità ponendo particolare attenzione a
UX e visual design.
Quando non è impegnato a scrivere codice offre
servizi di consulenza, mentoring e formazione su
angularJS e tecnologie frontend.
Fabio Biondi è un istruttore certificato Adobe con
più di 10 anni di esperienza nello sviluppo di
componenti custom, infografiche e interfacce
interattive cross-platform.
Attualmente si occupa di sviluppo, consulenza e
training su AngularJS, React, CreateJS, D3.js e
Firebase e organizza eventi formativi online.
GLI SPEAKER
2 / 92
4. AGENDA TECH WEBINAR
1. Introduzione al Bootcamp AngulrJS Advanced Class
2. CUSTOM DIRECTIVES: scope isolation, accesso al DOM e integrazione librerie 3d party
3. Differenze tra SERVICES, FACTORIES e PROVIDERS e quando usarli
4. Come usare al meglio le PROMISES
5. Cosa sono i DECORATORS e quando possono essere utili
3 / 92
5. A CHI E' RIVOLTO
Agli sviluppatori che già utilizzano AngularJS e che conoscono le principali funzionalità
del framework ma che desidano approfondire alcune tematiche avanzate, fondamentali
per la creazione di codice di qualità e lo sviluppo di componenti riusabili.
4 / 92
6. REQUISITI
Familiarità nell’utilizzo delle direttive e dei servizi inclusi in AngularJS
E' inoltre adatto a tutti i partecipanti che hanno frequentato il corso Sviluppare applicazioni con
AngularJS organizzato da CodeMotion.
5 / 92
9. Il programma
SVILUPPO DIRETTIVE
- Manipolazione DOM in direttive
- Integrazione librerie 3rd party e tecnologie
- Nested directives: ereditarietà e proprietà require
- Direttive multiple: priority and terminal
- Utilizzo pre and post link functions
- Utilizzo di $compile all’interno di direttive
8 / 92
10. Il programma
Utilizzo avanzato delle chiamate REST
- $http service
- $http Interceptors
- Integrazione con sessionStorage e localStorage
9 / 92
16. PROBLEMATICHE COMUNI NELLA CREAZIONE DI DIRETTIVE:
Scope isolation (@, =, &, true e false) e bindToController
Accesso e Manipolazione del Dom
Integrazione librerie esterne
Direttive nidificate: utilizzo di controller e require
Direttive multiple: priority e terminal
link function (pre e post) e compile
utilizzo del servizio $compile
NOTA: gli argomenti evidenziati in verde saranno trattati superficialmente durante questo webinar ed estesi, insieme ai rimanenti,
durante le giornate di formazione "Mastering AngularJS" e "Bootcamp AngularJS Advances Class" organizzati da CodeMotion
15 / 92
17. Tip & Tricks
per lo sviluppo di direttive / componenti riutilizzabili
16 / 92
18. 1. Isolare lo scope (=)
Utilizzando il simbolo = è possibile passare proprietà alla direttiva abilitando implicitamente il binding
bidirezionale:
<svg-chart title="ctrl.value" />
.directive('svgChart', function () {
return {
scope: {
title: '='
},
template: '<div>{{title}} ... </div>'
}
})
ATTENZIONE:
Ogni modifica effettuata dalla direttiva ai valori delle proprietà "intacherranno" il modello di dati
originale (ad es. se la direttiva modificasse la proprietà 'title')
Non è possibile applicare più di una direttiva con scope isolato allo stesso elemento del DOM
17 / 92
19. 1. DIFFERENTI TIPOLOGIE DI SCOPE ISOLATION
1. @ : il simbolo @ (aka "Text binding") viene utilizzato per passare stringhe alla direttiva e in questo
scenario il binding sarà di tipo unidirezionale (dal padre al figlio e non viceversa). Esempio di utilizzo:
<svg-chart title="{{ctrl.value}}"
2. false (default): la direttiva non crea un nuovo scope e può quindi accedere a tutte le proprietà e metodi
del contesto in cui è definita (ad es. un controller di una view).
Sconsigliato nel caso l'obiettivo sia quello di creare componenti riusabili.
3. true : la direttiva crea un nuovo scope ereditando tutte le proprietà del padre ed eventuali modifiche alle
proprietà non intaccheranno il modello originale
4. & : aka 'method binding', viene utilizzato per invocare funzione esterne alla direttiva
Esempio jsFiddle
Lettura: Mastering the Scope of the Directives in AngularJS
18 / 92
20. 2. Utilizzare il servizio $watch per monitorare le proprietà
<svg-chart title="ctrl.title" data="ctrl.data" />
.directive('svgChart', function () {
return {
scope: {
title: '=',
data: '=',
},
template: '...',
link: function (scope, element, attrs) {
console.log (scope.title);
scope.$watch('data', function(newValue) {
// do something, i.e. redraw chart
});
}
}
}).
Se per qualche motivo non potete isolare la direttiva, potete passare le proprietà alla direttiva
utilizzando gli attributi (attrs). Il tal caso utilizzare $observe al posto di $watch.
19 / 92
21. 3. Accedere al DOM della direttiva (element e link function)
<div resizable />
La link function è l'ultima fase del ciclo di vita di un componente AngularJS ed è il momento in cui lo scope è
accessibile e in cui si può accedere al DOM grazie alla proprietà element.
.directive('resizable', function () {
return {
restrict: 'A',
link: function (scope, element) {
element.resizable();
}
}
}).
element ritorna l'elemento root del template della direttiva in formato jQuery ( anzi jqLite! )
resizable() è un metodo di jQuery reso disponibile dall'inclusione di jQueryUI in index.html
20 / 92
22. Hai la necessità di accedere alla reference del DOM
e non all'elemento "wrappato" in jQuery??
Utilizza:
element[0]
21 / 92
23. 4. Comunicazione Direttiva -> Controller (&)
<div resizable on-resize="ctrl.doSomething()" />
.controller('MyCtrl', function() {
this.doSomething = function() { /* ... */ };
});
Per invocare una funzione del controller utilizzare & (aka "method" binding)
.directive('resizable', function () {
return {
restrict: 'A',
scope: {
onResize: '&'
},
link: function (scope, elem, attrs) {
elem.resizable();
elem.on('resize', function (evt, ui) {
scope.onResize();
});
}
};
})
22 / 92
25. 5. Gestire eventi 'non-AngularJS'
<div resizable on-resize="ctrl.doSomething()" />
Se l'evento è generato da una libreria esterna, come nel caso del resize di jQueryUI, è necessario invocare
manualmente il digest di AngularJS utilizzando ad esempio il servizio $applyAsync:
...
elem.resizable();
// Evento resize di jQueryUI resizable plugin
elem.on('resize', function () {
elem.resizable();
scope.$applyAsync(function() {
scope.onResize();
})
});
}
...
$evalAsync, $apply o $timeout sono alternative e/o trick utilizzabili al posto $applyAsync con risultati molti
simili.
24 / 92
29. LA DIRETTIVA <resizable>:
deve applicare il plugin "resizable" di jQuery UI all'elemento HTML in cui è utilizzata
dev'essere configurabile e supportare tutte le opzioni di jQuery UI Resizable
deve invocare una funzione ad ogni evento resize passando i parametri restituiti da jQueryUI
View e Controller:
<div ng-controller="MyCtrl">
<div resizable options="ctrl.options" on-resize="ctrl.onResize($evt, $ui)" />
</div>
.controller('MyCtrl', function MyCtrl() {
this.options = {maxWidth: 400};
this.onResize = function(evt, ui) {
this.w = ui.size.width;
this.h = ui.size.height;
};
});
28 / 92
30. LA DIRETTIVA < resizable />
<div resizable config="ctrl.config" on-resize="ctrl.onResize($evt, $ui)" />
.directive('resizable', function () {
return {
restrict: 'A',
scope: {
options: '=',
onResize: '&'
},
link: function postLink(scope, elem, attrs) {
elem.resizable(scope.options);
elem.on('resize', function (evt, ui) {
if (scope.onResize) {
scope.$applyAsync(function () {
scope.onResize({ $evt: evt, $ui: ui });
});
}
});
}
};
});
Ad ogni evento resize vengono passati gli oggetti evt e ui ritornati da jQueryUI
29 / 92
36. < tmax-knob > - pt.1 - Demo
E' possibile ruotare gli elementi su cui è applicata simulando la creazione di una manopola:
<img tmax-knob src="imgs/knob.png"
width="200" height="200"
options="ctrl.knobOptions"
on-drag="ctrl.onDrag($rotation)"
on-drag-end="ctrl.onDragEnd($rotation)"/>
.directive('tmaxKnob', function () {
return {
restrict: 'A',
scope: {
options: '=',
onDragEnd: '&',
onDrag: '&'
},
link: function (scope, element) {
// Code in next slide
}
}
})
35 / 92
37. < tmax-knob > - pt.2: utilizzo di angular.extend
...
link: function (scope, element) {
// Draggable default settings
var defaultOptions = {
type: 'rotation',
bounds: {
minRotation:0,
maxRotation:360
},
onDrag : function (){
// 'this.rotation' is available only in this context by TweenMax onDrag event
scope.$applyAsync(function () {
scope.onDrag({$rotation: this.rotation)})
});
},
onDragEnd : function (){...}
}
// Extend default options with custom options
angular.extend(defaultOptions, scope.options);
// Apply Draggable
Draggable.create(element, defaultOptions);
}
...
36 / 92
38. Applicare + direttive ad un elemento - Demo
<img src="myImg.jpg"
tmax-drag bounds="body"
tweener props="ctrl.props"
box-shadow="24px" box-shadow-color="#222">
37 / 92
40. Questa direttiva crea un chart in un tag HTML5 <Canvas>,
utilizzando come modello di dati un array di numeri interi
(espressi in percentuale 0-100)
<percent-chart width="300" height="100"
data="ctrl.data"
color="#FF0000"
padding="5"
on-bar-over="ctrl.doIt($percentage)"
on-bar-out="ctrl.doIt($percentage)"
>
</percent-chart>
> Demo
LA DIRETTIVA <percent-chart>
39 / 92
41. ESEMPIO DI UTILIZZO
View:
<percent-chart data="ctrl.data" color-bar="#ff0000" width="200" height="140"> </percent-chart>
Controller:
angular.module('myApp')
.controller('AppController', function () {
this.data = [100, 20, 30, 10, 70, 90];
});
40 / 92
42. UTILIZZARE IL CANVAS NELLE DIRETTIVE - Demo
// Codice per visualizzare un semplice rettangolo nel canvas utilizzando CreateJS
.directive('percentChart', function () {
return {
replace: true, // the directive's root DOM element is now <canvas>
scope: {
data: '=',
colorBar: '@'
},
template: '<canvas></canvas>',
link: function (scope, element) {
var stage = new createjs.Stage( element[0] );
var color = scope.colorBar || '#FF9900';
// Il codice completo sarà presto disponibile su github
bar = new createjs.Graphics();
bar.beginFill(color);
bar.drawRect(0,0,stage.canvas.width, stage.canvas.height);
barShape = new createjs.Shape(bar);
barShape.x = 0; barShape.y = 0;
stage.addChild(barShape);
stage.update();
}
};
});
41 / 92
44. 1. scope ISOLATION e $watch
Direttive e componenti riutilizzabili, come ad esempio un chart, dovrebbero essere implementate con scope
isolato (= e @) L'utilizzo di watcher permette di monitorare eventuali modifiche al modello di dato e avviare
azioni quali il ridisegnato del chart, una chiamata REST e così via.
.directive('percentChart', function () {
return {
//...
scope: {
data: '='
},
link: function (scope, element, attrs) {
//...
scope.$watch('data', function(newVal) {
// Do something: i.e. redraw the chart
});
}
}
}
});
43 / 92
45. 2. UTILIZZARE IL TAG <canvas> COME template
myApp.directive('percentChart', function () {
return {
//...
template: '<canvas></canvas>',
link: function (scope, element, attrs) {
//...
}
}
}
});
Grazie a questo trick, la proprietà element ritornata dalla link function della direttiva corrisponderà
esattamente al nostro <canvas>
44 / 92
47. 3. LA PROPRIETÀ replace
Quando la direttiva viene renderizzata nel DOM, il contenuto del suo template viene inserito all'interno di un
custom tag HTML5 il cui nome corrisponde a quello della direttiva stessa.
In questo scenario element fa riferimento all'elemento "wrapper" e non al canvas.
Con l'opzione replace: true il DOM generato sarà invece il seguente:
element ora corrisponde al nostro elento <canvas>. Obiettivo raggiunto!
46 / 92
48. 4. element[0]
La libreria EaselJS, utilizzata nell'esempio, in fase di inizializzazione richiede tuttavia un riferimento
all'elemento Canvas e non al suo "wrapper" jqLite.
Cosa contiene element ?
element rappresenta una reference jqLite dell'elemento HTML della direttiva, che nel nostro caso è
rappresentato dall'oggetto <canvas>, che sarà invece disponibile in formato RAW utilizzando element[0]
47 / 92
49. replace è deprecated dalla versione 1.3
Come accedere quindi ai figli di element?
find(): fornito da jqLite ma limitato ai tag name
var elem = element.find('canvas')
var stage = new createjs.Stage(elem[0]);
querySelector o qualunque altra tecnica:
template: '<canvas class="chart" />',
link: function (scope, element) {
var elem = element[0].querySelector(".chart");
var stage = new createjs.Stage(elem); // in questo caso elem non è un elemento jqLite
...
48 / 92
50. 5. HAVE FUN! :)
In questo esempio utilizziamo le Graphics API della libreria EaselJS per disegnare un fantastico rettangolo ;)
link: function (scope, element, attrs) {
var elem = element.find('canvas');
var stage = new createjs.Stage( elem[0] );
var color = scope.colorBar || '#FF9900';
bar = new createjs.Graphics();
bar.beginFill(color);
bar.drawRect(0,0,stage.canvas.width, stage.canvas.height);
barShape = new createjs.Shape(bar);
barShape.x = 0; barShape.y = 0;
stage.addChild(barShape);
stage.update();
// ... insert here your code to generate a custom chart
}
Visualizza l'esercizio completo su direttive custom e integrazione HTML5 Canvas.
49 / 92
52. ARCHITETTURE IN ANGULARJS
Promises permettono di gestire meglio dinamiche asincrone.
Services|Factories|Providers elementi cardine di qualsiasi architettura realizzata
in AngularJSAngularJS, abbinati alle promises permettono di creare soluzioni eleganti e
mantenibili.
Decorators offrono la possibilità di modificare e/o estendere le API di moduli
importati nell'applicazione.
NOTA: questi argomenti saranno trattati superficialmente durante questo webinar ed estesi, insieme ai rimanenti, durante le giornate di
formazione "Mastering AngularJS" e "Bootcamp AngularJS Advances Class" organizzati da CodeMotion
51 / 92
54. IN PRINCIPIO ERANO CALLBACK
La realizzazione di un SPA richiede un uso esteso di invocazioni asincrone
per inviare o ricevere dati da un server remoto
53 / 92
55. JAVASCRIPT CALLBACK
La principale soluzione adottata in Javascript per gestire codice asincrono è l'uso
di callback
getDataFromServer( config, (data) => console.log(data) );
Node.js ha ulteriormente standardizzato questa pratica definendo una firma
consistente per i callback:
getDataFromServer( config, (data) => console.log(err, data) );
() => Arrow function (es2015)
54 / 92
56. CALLBACK HELL
I callback funzionano benissimo
ma espongono alcuni effetti secondari non sempre desiderabli
55 / 92
57. JAVASCRIPT CALLBACK
Eccessivo annidamento del codice e contaminazione tra closures differenti.
getDataFromServer(config, function(data) {
invokeMethodB(data, function(dataB) {
invokeMethodC(data, function(dataC) {
invokeMethodD(data, function(dataD) {
console.log(dataD);
});
});
});
});
Difficoltà nel refactoring e nella mantenibilità del codice.
Difficile leggibilità del codice
56 / 92
58. PER SEMPLIFICARE LA SCRITTURA DI CODICE ASINCRONO
SONO STATE INTRODOTTE LE PROMISES.
57 / 92
59. Una promise permette di registrare una funzione che verrà invocata quando
l'invocazione asincrona completa o fallisce.
Questo permette a metodi asincroni di restituire in maniera sincrona la promessa
di un valore futuro.
58 / 92
60. PROMISES IN PRATICA
Una promise può esistere in uno di questi stati:
pending stato iniziale, in attesa di risoluzione
fulfilled quando l'operazione asincrona è stat completata con successo
rejected quando l'operazione asincrona è fallita
59 / 92
61. PROMISES IN PRATICA
Una promise espone 2 methodi, entrambi accettano una funzione come argomento:
then invoca il proprio handler alla risoluzione dell'azione invocata
catch invoca il proprio handler al fallimento dell'azione invocata
60 / 92
62. PROMISES IN PRATICA
La funzione handler passata a then o catch riceverà un solo argomento quando
invocata.
// es5
myPromise
.then(function(data) {
console.log(data);
}).catch(function(reason) {
console.error(reason);
});
// es2015
myPromise
.then( data => console.log(data) )
.catch( reason => console.error(reason) );
61 / 92
63. Uno dei fondamenti delle promises è:
"una promise restituisce sempre una promise"
62 / 92
64. PROMISES IN PRATICA
Questo permette di creare catene complesse di azioni asincrone mantenendo il codice
pulito e facile da mantenere.
login()
.then( user => loadProfile(user) )
.then( profile => loadAssets(profile) )
.then( assets => cacheAssets(profile) )
.then( profile => updateView(profile) )
.catch( reason => gotoLoginPage(reason) );
L'uso delle promises assieme alle arrow function introdotte con es2015
permette di avere del codice davvero conciso.
63 / 92
65. PROMISES: GESTIONE DEGLI ERRORI
Un altro elemento chiave è che un singolo catch intercetta errori ed eccezioni di
qualsiasi elemento della catena.
Se desiderate propagare l'eccezione ad un catch successivo dovete semplicemente
risollevarla:
login()
.then( user => loadProfile(user) )
.catch( reason => {
if (reason.type === 'FATAL') {
// DO SOMETHING
} else {
throw reason;
}
} )
.catch( reason => console.error(reason) );
64 / 92
66. ANGULARJS PROMISES
AngularJS offre supporto alle promises tramite il servizio $q .
Promises Basic 1
Promises Basic 2
Promises Errors
65 / 92
68. ANGULARJS
Offre diversi elementi a supporto della creazione di architetture altamente
scalabili e flessibili
Non impone nessun vincolo sulla definizione dell'architettura di un applicazione
Permette di utilizzare i Pattern e le strategie che più si adeguano ai requisiti di un
progetto
67 / 92
70. LA LIBERTÀ HA UN COSTO
La grande flessibilità di AngularJS
è anche il più grande ostacolo per sviluppatori con poca esperienza
nel disegnare architetture scalabili e flessibili
69 / 92
71. ORGANIZZAZIONE DEL PROGETTO
La struttura che si definisce per il proprio progetto è fondamentale e alla radice di
una buona architettura
AngularJSAngularJSè basato su moduli.
Disegnare la propria applicazione in moduli specializzati è un buon punto di
partenza per creare un applicazione mantenibile.
70 / 92
72. I MODULI IN ANGULARJS
favoriscono il riutilizzo di componenti
favoriscono lo unit testing
aiutano il lavoro distribuito
permettono lo sviluppo asincrono di funzionalità
aiutano a definire una struttura di cartelle e file facile da comprendere e mantenere
favoriscono la composizione di funzionalità
71 / 92
73. COS'È UN MODULO
Un modulo è un aggregato di funzionalità predisposte ad un fine comune
Può essere considerato, in termini Object Oriented , come un package
un elemento autosufficiente e facilmente trasportabile
72 / 92
74. DEFINIZIONE DI UN MODULO
Registrazione di un modulo
angular.module('xyz.code-browser', []);
Registrazione di un modulo con dipendenze da altri moduli
angular.module('xyz.code-browser', ['ui.router', 'ngAnimate']);
Accesso ad un modulo già registrato
angular.module('xyz.code-browser');
73 / 92
75. Cosa può contenere un modulo
Servizi (service, factory, provider)
View Controllers
Direttive
Costanti
74 / 92
76. CICLO DI VITA DI UN MODULO
Una volta istanziato ogni modulo esegue una serie di step interni che lo inizializzano.
75 / 92
77. CICLO DI VITA DI UN MODULO
Alcuni di questi step sono esposti all'esterno e possono essere utilizzati per
configurare l'inizializzazione di un modulo:
Config Phase Eseguita dopo che il modulo ha caricato tutte le proprie dipendenze
ma prima che venga avviato il motore di Dependency Injection
Run Phase Eseguita quando il motore di DI ha soddisfatto tutte le dipendenze e
l'applicazione AngularJS è pronta per operare.
76 / 92
78. SERVICE, FACTORY E PROVIDER
live demo
quando si approccia AngularJS per la prima volta
è spesso difficile discernere tra i tre servizi
e soprattutto è difficile decidere quale usare a seconda delle situazioni
77 / 92
79. ALCUNI CONCETTI FONDAMENTALI
I tre servizi sono in realtà un unico servizio
factory e service sono implementazioni differenti di provider
vengono registrati come singleton all'interno dell'applicazione AngularJS
78 / 92
81. SERVICE
Rappresenta l'istanza di una funzione creata usando l'operatore new
angular.module('app',[]).service('myService', MyService);
function MyService() {
this.say = function say(msg) {
return msg;
}
}
AngularJS inizializza il servizio, durante gli step di inizializzazione così:
var myServiceSingleton = new myService();
console.log(myServiceSingleton.say('ciao'));
80 / 92
83. FACTORY
Viene definita dal valore restituito dall'invocazione della funzione registrata.
angular.module('app',[]).factory('myFactory', MyFactory);
function MyFactory() {
return {
say: function say(msg) {
return msg;
}
};
}
AngularJS inizializza il servizio, durante gli step di inizializzazione così:
var myFactorySingleton = myFactory();
console.log(myFactorySingleton.say('ciao'));
82 / 92
84. FACTORY
come aggirare il limite del singleton pattern
angular.module('app',[]).factory('myFactory', MyFactory);
function MyService() {
this.say = function say(msg) {
return msg;
}
}
function MyFactory() {
return MyService;
}
var myFactorySingleton = myFactory();
var myServiceInstance = new myFactorySingleton();
console.log(myServiceInstance.say('ciao'));
83 / 92
86. Può comportarsi sia come una factory che come un service .
Inoltre permette di esporre una API di configurazione accessibile durante la config
phase di un modulo.
85 / 92
87. DEFINIRE UN PROVIDER
angular.module('app',[]).provider('myProvider', MyProvider);
function MyProvider() {
var PREFIX = 'AngularJS says: ';
this.setPrefix = function setPrefix(value) {
PREFIX = value;
};
this.$get = function myProviderFactory() {
return return {
say: function say(msg) {
return PREFIX + msg;
}
};
};
}
86 / 92
88. CONFIGURARE UN PROVIDER
Durante la fase di configurazione di un modulo si può accedere alle API di
personalizzazione esposte dal provider
AngularJS per ogni provider definito rende disponibile, nella fase di config, un
oggetto di configurazione accessibile tramite la sintassi: nomeMioServizio + Provider
angular.module('app',[]).config(
function config(myProviderProvider) {
myProviderProvider.setPrefix('[service said: ] ');
}
)
87 / 92
89. PROVIDER
AngularJS inizializza il servizio, durante gli step di inizializzazione così:
var myProviderSingleton = myProvider();
console.log(myProviderSingleton.say('ciao'));
88 / 92
91. Un decorator intercetta la creazione di un servizio permettendo di modificarne le
API.
Il valore restituito potrebbe essere l'oggetto originale modificato o un oggetto
completamente nuovo.
90 / 92