I hereby declare AngularJS to be MVW framework – Model-View-Whatever. Where Whatever stands for "whatever works for you
AngularJS è un framework per applicazioni web “single page” sviluppato da Google.
Si differenzia dai framework precedenti come jQuery nei quali lo sviluppatore doveva avere una conoscenza completa del DOM per poi manipolarlo scrivendo la logica in javascript, ad esempio:
var btn = $("<button>Hi</button>");
btn.on('click', function(evt) { console.log("Clicked button") });
$("#checkoutHolder").append(btn);
AngularJS invece aumenta il potere espressivo dell'HTML fornendo agli sviluppatori nuovi tag built-in e la possibilità di crearne degli altri.
Inoltre la libreria è di dimensioni veramente ridotte, appena 9KB (la versione minimizzata) ed è disponibile open source.
Data Binding
I framework web classici come Rails a partire da dati del modello producono una view per l'utente, generando una view “single-way”che riflette i dati del modello solo al momento al momento dell'istanziazione.
AngularJS invece adotta un approccio unico, creando dei live templates come view che sono sempre aggiornati rispetto ai dati del modello. Questo ci permette di scrivere nell'HTML
<input ng-model="name" type="text" placeholder="Your name">
<h1>Hello </h1>
Le doppie parentesi graffe indicano un'espressione AngularJS, il framework valuterà l'espressione per effettuare il rendering del contenuto.
Senza scrivere nessuna riga di codice in Javascript abbiamo ottenuto un two-way data binding direttamente nella view.
In questo esempio, ogni volta che cambierà il valore del tag <input> verrà aggiornato anche il contenuto del tag <h1> sottostante.
Scopes
Gli scopes sono componenti core di ogni applicazione AngularJS, vengono utilizzati come glue tra i controller e le view. Grazie al live binding di AngularJS unoscope è immediatamente aggiornato se l'utente interagisce con la view, e viceversa la view è immediatamente aggiornata se modifichiamo lo scope eseguendo qualche operazione nel codice Javascript.
AngularJS gestisce i diversi scope di una applicazione attraverso una relazione gerarchica che rispetta la composizione del DOM, all'avvio dell'applicazione viene infatti creato un rootScope e tutti gli altri scope discendono da questo.
L'oggetto scope è un plain old Javascript object, possiamo quindi aggiungere tutte le proprietà di cui necessitiamo. Esso funge da data model e tutte le sue proprietà sono automaticamente accessibili dalla view:
<div ng-app="myApp"> <h1>Hello </h1>
</div>
angular.module('myApp', []) .run(function($rootScope) {
$rootScope.name = "World";
});
Controllers
I controllers in AngularJS sono funzioni definite dallo sviluppatore per aggiungere funzionalità allo scope della view. Attraverso la Dependency Injection AngularJS passa automaticamente lo scope associato ad ogni controller al relativo costruttore. Nel costruttore può essere configurato lo stato iniziale dello scope, lo sviluppatore si deve preoccupare solamente di scrivere la funzione costruttore, poiché l'instanziazione dei controller è a carico del framework.
Ad esempio:
function FirstController($scope) {
$scope.message = "hello";
}
Per creare delle azioni invocabili dalle view basterà creare delle funzioni sullo scope associato e poi effettuare il binding, ad esempio sfruttano la built-in directive ng-click che gestisce l'evento click del browser. Quindi volendo fare un esempio leggermente più complesso avremo nella view:
<div ng-controller="FirstController">
<h4>Il mio contatore</h4>
<button ng-click="add(1)">Add</button>
<a ng-click="subtract(1)">Subtract</a>
<h4>Valore : </h4>
</div>
E nel relativo controller:
app.controller('FirstController', function($scope) {
$scope.counter = 0;
$scope.add = function(amount) { $scope.counter += amount; };
$scope.subtract = function(amount) { $scope.counter -= amount; };
});
Sia il bottone che il link sono collegati ad una azione definita nello scope, per cui quando verranno premuti/selezionati AngularJS chiamerà i rispettivi metodi.
Come si può notare nei controllers scritti dal programmatore non c'è una modifica manuale del DOM, il tutto viene gestito automaticamente da AngularJS.
Tutti gli scope che sono creati con ereditarietà prototipale, hanno cioè accesso agli scope “avi”. Quindi ogni qual volta AngularJS non riesce a trovare una proprietà richiesta nello scope corrente risalirà questa catena di ereditarietà fino al rootScope associato all'applicazione.
È importante sapere che per motivi di gestione ottimale della memoria e di performances i controllers sono instanziati solo quando sono necessari, e deallocati quando non lo sono più. Per cui ogni volta che cambia lo stato di una applicazione o ricarichiamo una view, i controller correnti vengono deallocati e instanziati i nuovi controller.
Directives
AngularJS fornisce molte built-in directives che servono ad aumentare l'espressività dell'HTML.Le directives built-in sono tag che iniziano con il prefisso ng, per un elenco dettagliato si rimanda alla documentazione di AngularJS.
Inoltre AngularJS permette di definire nuove directive agli sviluppatori con le funzionalità desiderate e di unire diverse directive attraverso un processo chiamato composizione.
Per creare una nuova directive bisogna definirla come segue:
angular.module('myApp', []) .directive('myDirective', function() {
return {
restrict: 'E',
template: '<a href="http://google.com"> Click me to go to Google</a>'
} });
Nell'esempio sono state utilizzate solo la proprietà restrict e template per semplicità, per l'elenco esaustivo si rimanda alla documentazione di AngularJS su come creare directives custom.
Avendo definito questa directive, ogni qual volta nell'HTML verrà incontrata la directive AngularJS invocherà la funzione associata. Possiamo quindi scrivere nella nostra view
<my-directive></my-directive>
e AngularJS provvederà a inserire nel DOM un tag <a> con link a google.
Per poter passare dello stato ad una custom directive attraverso gli attributi:
<my-directive my-url="http://google.com" my-link-text="Click me to go to Google">
</my-directive>
definendo opportunamente la directive:
angular.module('myApp', []) .directive('myDirective', function() {
return {
restrict: 'E',
replace: true,
scope: {
myUrl: '@', // binding strategy
myLinkText: '@' // binding strategy
},
template: '<a href="">' + '</a>'
} })
Così facendo la directive avrà uno scope interno contenente l'url e il testo del link. Ci sono diverse strategie di binding tra attributi passati nella view e lo scope interno, nell'esempio è stata utilizzata la strategia @ che copia il valore dell'attributo nella corrispondente proprietà dello scope.
Views
In applicazioni “single page” la navigazione da una view ad un altra è un aspetto critico, vogliamo mostrare del nuovo contenuto all'utente senza forzarlo ad aspettare l'apertura di un altra pagina HTML. AngularJS per ottenere questo risultato permette di scrivere diversi templates, per poi mostrare il contenuto di interesse in base allo “stato” dell'applicazione.
AngularJS fornisce il modulo ngRoute (da scaricare ed includere manualmente) che realizza la direttiva ng-view utilizzata come placeholder per il contenuto della view dinamico, ad esempio:
<header>
<h1>Header</h1>
</header>
<div class="content">
<ng-view></ng-view>
</div>
<footer>
<h5>Footer</h5>
</footer>
Così facendo l'header e il footer della pagina rimarranno sempre gli stessi, mentre il contenuto verrà aggiornato (senza ricaricare l'intera pagina) in base al valore corrente dell'URL.
Per configurare quali template caricare e in che circostanza occorre configurare routeProvider come segue:
angular.module('myApp', []). config(['$routeProvider', function($routeProvider) {
$routeProvider
.when('/', {
templateUrl: 'views/home.html',
controller: 'HomeController'
});
}]);
Si specifica l'url (“/” nell'esempio) e che template e controller verranno utilizzati. Per semplicità sono stati riportati solamente alcune proprietà esprimibili, per un elenco esaustivo si rimanda alla documentazione di ngRoute.
Passando degli argomenti (con “:” ) attraverso l'URL, AngularJS provvederà ad effettuare il parsing e a creare un array chiamato routeParams utilizzabile dal controller che gestisce la view. E poiché l'utente deve solamente scrivere la funzione costruttore del controllore basterà inserirlo tra gli argomenti e AngularJS si preoccuperà di iniettarlo:
$routeProvider
.when('/inbox/:name', {
controller: 'InboxController',
templateUrl: 'views/inbox.html'
})
app.controller('InboxController', function($scope, $routeParams) {
// Qui è abbiamo accesso all'array $routeParams
});
UI-Router
UI-Router è una libreria che ci permette di organizzare l'interfaccia in stati invece che semplici url, questo ci da la possibilità di avere view innestate usando la directive ui-view invece che ng-view.
Infatti come accade usando ngRoute, il template definito per lo stato corrente verrà utilizzato per visualizzare il contenuto all'utente, ma usando ui-route i template possono a loro volta includere altre ui-view.
La configurazione è leggermente diversa, poiché si usa la keyword “state” invece di “when”.
$stateProvider
.state('inbox', {
url: '/inbox/:inboxId',
template: '<div><h1>Welcome to your inbox</h1>\
<a ui-sref="inbox.priority">Show priority</a>\
<div ui-view></div>\
</div>',
controller: function($scope, $stateParams) { $scope.inboxId = $stateParams.inboxId;
} })
.state('inbox.priority', {
url: '/priority',
template: '<h2>Your priority inbox</h2>'
});
Abbiamo uno stato figlio (specificato usando il “.”) chiamato inbox.priority. La ui-view dentro la view padre verrà quindi popolata con il template di inbox.priority.
Quando abbiamo più ui-view innestate possiamo dare un nome ad ognuna e collegare ad ognuna di esse un template diverso. Ad esempio:
<div>
<div ui-view="filters"></div>
<div ui-view="mailbox"></div>
</div>
E ogni sub-view può avere il suo template e controller:
$stateProvider
.state('inbox', {
views: {
'filters': {
template: '<h4>Filter inbox</h4>',
controller: function($scope) {} },
'mailbox': {
templateUrl: 'partials/mailbox.html'
}
});
Dependency Injection
AngularJS permette agli sviluppatori di utilizzare il pattern dependency injection e specificare le dipendenze di ogni componente senza preoccuparsi di come reperire quest'ultime. A runtime l'injector di AngularJS si occuperà di reperire le dipendenze specificate e se necessario instanziare i componenti richiesti. Possiamo ad esempio definire un servizio e poi specificare ad AngularJS che un controller ne avrà bisogno a runtime:
angular.module('myApp', [])
.factory('greeter', function() {
return {
greet: function(msg) { alert(msg); }
} })
.controller('MyController', function($scope, greeter) {
$scope.sayHello = function() { greeter.greet("Hello!");
};
});
Nel costruttore del controller “MyController” è specificata la dipendenza dal servizio “greeter”.
Annotazione Con Inferenza
AngularJS assume che i nomi degli argomenti siano i nomi delle risorse da reperire o instanziare a runtime, e per questo motivo questo approccio può funzionare solo con codice Javascript non minimizzato e non offuscato, poiché AngularJS ha bisogno di effettuare il parsing degli argomenti immutati.
Annotazione Esplicita
Si possono comunque indicare manualmente i nomi delle dipendenze, per poter minimizzare il codice e non avere problemi specificando la proprietà $inject di una funzione (che corrisponde ad un controller o un servizio).
var aControllerFactory =
function aController($scope, greeter) {
console.log("LOADED controller", greeter);
// ... Controller
};
aControllerFactory.$inject = ['$scope', 'greeter'];
In questo approccio diventa però importante l'ordine con il quale vengono specificati i parametri della funzione, che deve coincidere con l'ordine delle dipendenze specificate nell'array $inject.
Annotazione Inline
Questo tipo di annotazione è equivalente alla precedente ma permette di definire l'array di dipendenze inline alla definizione della funzione:
angular.module('myApp')
.controller('MyController',
['$scope', 'greeter', function($scope, greeter) {
}]);
NgMin
NgMin è un tool che sgrava lo sviluppatore dal definire le dipendenze in maniera esplicita e poter comunque minimizzare il codice, trasforma il seguente codice:
angular.module('myApp', [])
.controller('IndexController', function($scope, $q) {
});
automaticamente in questo:
angular.module('myApp', [])
.controller('IndexController', [ $scope', '$q',
function ($scope, $q) {
} ]);
Services
Prima di introdurre i services vale la pena ricordare che i controllers sono volativi (e lo sviluppatore non ne controlla il ciclo di vita), quindi non idonei a contenere lo stato dell'applicazione. Per questo motivo, e per fornire una modalità di scambio di dati in maniera consistente esistono i services.
I services sono oggetti singleton instanziati dall'injector di AngularJS e lazy-loaded. Come per le directives AngularJS fornisce molti services built-in (ad esempio $http) e permette agli sviluppatori di crearne di personalizzati.
Per usare un service è sufficiente elencarlo nelle dipendenze del controller, directive o altro service che vuole invocarlo.
Factory
Il modo più semplice per creare e registrare (per essere trovato dall'injector) un nuovo service è utilizzare l'API .factory di angular.module.
angular.module('myApp', []) .
factory('UserService', function($http) {
var current_user;
return {
getCurrentUser: function() {
return current_user; },
setCurrentUser: function(user) {
current_user = user;
} }
La funzione factory ha due argomenti: il primo specifica il nome del servizio, il secondo è una funzione che verrà eseguita da AngularJS una volta soltanto per instanziare il service
Service
La funzione service di angular.module è simile alla funzione factory, ed ha come quest'ultima due argomenti, dei quali il primo indica il nome mentre il secondo è una funzione costruttore, che verrà invocata da AngularJS usando la keyword new.
Provider
Tecnicamente le funzioni factory e service sono solo delle “scorciatoie” per la funzione provider che è la vera responsabile per la creazione e registrazione dei vari “services”.
Utilizzando però direttamente provider è possibile configurare dall'esterno il servizio che si sta definendo attraverso la funzione config, iniettando alcuni valori.
Eventi
Per ottenere un accoppiamento più debole tra i diversi componenti dell'applicazione AngularJS mette a disposizione un meccanismo di creazione e propagazione di eventi.
Rispecchiando la gerarchia degli scope, le due operazioni basilari di propagazione degli eventi sono $emit per risalire la catena di scope dai figli al rootScope e $broadcast con funzionamento opposto, da uno scope ai suoi scope figli.
Quando si scatena un evento è possibile specificare oltre al nome anche degli argomenti.
Per registrarsi ad un evento si utilizza la funzione $on specificando il nome dell'evento di interesse e una funzione di gestione. Un gestore di un evento può anche decidere di interrompere la propagazione dell'evento.
Anche in questo caso AngularJS ha diversi eventi built-in, degno di nota è l'evento $destroy che viene emesso sullo scope prima che venga distrutto e permette di pulire l'ambiente (ad esempio distruggendo un timeout che si era registrato). Per un elenco dettagliato degli eventi built-in si rimanda alla documentazione.
Attraverso questo meccanismo è inoltre possibile emettere eventi “globali” poiché ogni componente dell'applicazione AngularJS può richiedere di farsi iniettare il rootScope e poi utilizzarlo con la funzione di $broadcast.
Moduli
In Javascript non è mai una buona idea utilizzare l'ambiente globale e per questo motivo AngularJS permette di dividere l'applicazione in moduli, nei quali racchiudere le funzionalità.
Quando vogliamo definire un nuovo modulo dobbiamo usare la funzione angular.module a due argomenti, il primo specifica il nome del modulo, mentre il secondo è una lista di dipendenze da iniettare.
Per ottenere invece il riferimento ad un modulo utilizziamo la funzione angular.module ad un solo parametro, il nome del modulo. Ottenuto il modulo possiamo definire i nostri componenti per quel modulo (controller/service/directive).
Promises
Una promise è un oggetto che rappresenta il valore di ritorno o un eccezione di di una funzione, può essere vista come un proxy per un oggetto.
Storicamente in Javascript sono state utilizzate funzioni di callback per poter lavorare con dati non disponibili in maniera sincrona, come i dati ottenuti da una richiesta XHR, con le promises possiamo invece interagire con i dati, supponendo che siano già stati restituiti.
Le promises permettono di far sembrare funzioni asincrone come se fossero sincrone, permettendoci di catturare sia i valori di ritorno che le eventuali eccezioni, ma sono sempre eseguite in maniera asincrona, per cui possono essere usate senza preoccuparsi del fatto che possano bloccare l'esecuzione.
AngularJS fornisce il service $q per semplificare la gestione delle promises, una volta creata un promise con il metodo $q.defer() possiamo utilizzare il metodo resolve(value) nel caso l'esecuzione sia andata come previsto e il metodo reject(reason) per notificare invece l'errore. Un ulteriore metodo è notify(value) che permette di mandare una notifica (ad esempio se l'esecuzione richiede un tempo lungo potremmo notificare il progresso attraverso questo metodo). Sia nel caso di successo che nel caso di errore la funzione ritornerà la promise creata al chiamante che può interagire con il risultato attraverso la funzione then().
La funzione then() accetta tre parametri, le funzioni che saranno eseguite in caso di successo, errore e ricezione di notifica. È importante notare che le funzioni di successo o fallimento saranno invocate in maniera asincrona appena la promise è risolta, (solamente una delle due) e con un singolo parametro che indica il risultato in un caso o il motivo del fallimento nell'altro. Mentre la funzione di notifica può essere invocata anche più volte.
Le promises sono profondamente integrate in AngularJS, infatti anche i services built-in come $http le utilizzano per restituire i risultati ai chiamanti.