AngularJS è un framework JavaScript, patrocinato da Google, utile a semplificare la realizzazione di applicazioni Web single page: favorisce un approccio dichiarativo allo sviluppo client-side, migliore per la creazione di interfacce utente, laddove l'approccio imperativo è ideale per realizzare la logica applicativa.
AngularJS si ispira al pattern MVC, come altri framework analoghi quali Knockout o Ember.js. Ma rispetto ai diretti concorrenti, questo framework è in grado di ridurre in maniera considerevole il codice necessario a realizzare applicazioni HTML/JavaScript.
>>Leggi come creare applicazioni JavaScript MVC con Ember.js
In questo articolo esploreremo le caratteristiche principali di AngularJS implementando un semplice catalogo fotografico online. Scopriremo ciò che rende questo framework molto più che un semplice supporto al pattern MVC.
Creare un progetto AngularJS
Per creare un progetto basato su AngularJS dobbiamo innanzitutto scaricare il pacchetto dalla home page del progetto o da GitHub. Se decidiamo invece di appoggiarci al CDN di Google è sufficiente inserire nella pagina HTML un riferimento alla libreria come mostrato di seguito:
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.2/angular.min.js"></script>
È prevista anche la possibilità di creare un progetto partendo da un ambiente già predisposto secondo delle particolari linee guida: Angular-seed. Tuttavia, per lo sviluppo dell'applicazione che porteremo avanti nel corso dell'articolo non faremo ricorso ad esso per evitare di introdurre elementi che al momento possono apparire poco chiari. Ritorneremo invece su Angular-seed alla fine dell'articolo dopo avere acquisito i concetti fondamentali del framework.
Concetti di base
Prima di iniziare la creazione del nostro catalogo online, introduciamo alcuni concetti di base di AngularJS. Innanzitutto diremo che la struttura minima di una pagina è analoga alla seguente:
<!doctype html>
<html ng-app>
<head>
<meta charset="utf-8">
<title>Prima pagina con AngularJS</title>
<script src="js/angular.min.js"></script>
</head>
<body>
<p>
La somma di 12 e 13 è uguale a {{12+13}}
</p>
</body>
</html>
Questa pagina, pur nella sua estrema semplicità, è un esempio di applicazione AngularJS. Pur non avendo utilizzato una sola riga di codice JavaScript, il risultato visualizzato all'interno di un browser sarà la scritta: "La somma di 12 e 13 è uguale a 25".
Analizziamo gli elementi rilevanti presenti nel codice HTML della pagina per capire cosa sono e come funzionano. Il primo elemento nuovo che incontriamo è l'attributo ng-app in <html ng-app>
. Questo attributo rappresenta una direttiva AngularJS e indica al sistema quale elemento della pagina deve essere considerato come elemento radice della nostra applicazione. In altre parole, AngularJS prenderà il controllo della porzione di DOM la cui radice viene marcata con la direttiva ng-app.
Nel nostro semplice esempio abbiamo marcato l'intera pagina, ma è possibile marcare anche soltanto una porzione della pagina, come ad esempio il <body>
o un <div>
.
Il riferimento alla libreria è naturalmente necessario perché AngularJS sia attivo. Quello che avviene in corrispondenza del caricamento del codice della libreria è il cosiddetto processo di bootstrap di AngularJS. In sintesi, dopo il caricamento del documento HTML da parte del browser vengono creati alcuni oggetti di sistema e viene effettuata la compilazione del DOM a partire dall'elemento marcato con ng-app
, elaborando ogni direttiva, binding ed espressione incontrati. Al termine della fase di bootstrap, AngularJS si mette in attesa del verificarsi di eventi sul browser.
L'ultimo elemento chiave della nostra pagina è la stringa {{12 + 13}}
. Questa rappresenta un'espressione AngularJS e viene valutata ed inserita nel DOM dal framework. Un'espressione tuttavia non viene valutata soltanto in fase di bootstrap, ma viene generato un binding che aggiorna il risultato dell'espressione ogni qualvolta cambia il suo valore. Ciò è maggiormente evidente nel seguente esempio:
<!doctype html>
<html ng-app>
<head>
<meta charset="utf-8">
<title>Prima pagina con AngularJS</title>
<script src="js/angular.min.js"></script>
</head>
<body>
<div>
La somma di
<input type="number" ng-model="addendo1" />
e
<input type="number" ng-model="addendo2" />
è
{{addendo1 + addendo2}}
</div>
</body>
</html>
In questo caso abbiamo previsto due caselle di testo in cui l'utente può inserire dei valori numerici e visualizzare dinamicamente il risultato, come mostrato nella seguente figura:
Da evidenziare l'uso della direttiva ng-model che implementa un binding bidirezionale tra ciascuna casella di testo ed il modello dei dati interno. L'espressione che calcola la somma dei valori immessi dall'utente viene valutata automaticamente senza necessità di codice JavaScript.
Implementare MVC con AngularJS
Dopo aver visto alcuni dei concetti fondamentali di AngularJS, iniziamo a creare il nostro catalogo di foto online. Abbiamo detto che il framework supporta il pattern MVC nella creazione di interfacce Web. Il supporto di questo pattern viene effettuato tramite una combinazione di direttive, espressioni e codice JavaScript che contribuiscono a definire i tre elementi del modello: il model, la view ed il controller.
Le view
Vediamo come può essere strutturata la nostra pagina principale analizzando il seguente esempio:
<!doctype html>
<html ng-app>
<head>
<meta charset="utf-8">
<title>Catalogo fotografico</title>
<link rel="stylesheet" href="css/app.css">
<script src="js/angular.min.js"></script>
<script src="js/app.js"></script>
</head>
<body ng-controller="PhotoListCtrl">
<ul>
<li ng-repeat="photo in photos">
<p>
<img ng-src="photos/thumb/{{photo.file}}" alt="{{photo.description}}" />
{{photo.description}}
</p>
</li>
</ul>
</body>
</html>
Il codice dell'esempio rappresenta un template in base al quale AngularJS genera una view, cioè una proiezione del modello di dati sottostante. Possiamo evidenziare alcune nuove direttive che istruiscono il framework su come generare la view:
- la direttiva ng-repeat indica al sistema di generare tanti elementi
<li>
quante sono le foto contenute nel model, basandosi sull'espressionephoto in photos
, come analizzeremo più avanti. In corrispondenza a ciascun elemento<li>
saranno valutate le espressioni specificate in base al contesto corrente, cioè in base alla foto corrente. - la direttiva ng-src sostituisce l'attributo standard
src
dell'elemento<img>
. Anche se apparentemente il risultato finale è identico, la direttivang-src
impedisce che il browser interpreti l'espressione letterale{{photo.file}}
come l'URL dell'immagine da caricare ed effettui una infruttuosa richiesta HTTP al server. - la direttiva ng-controller indica qual è il controller associato all'elemento.
Il controller
Il controller viene definito tramite una funzione JavaScript analoga alla seguente:
function PhotoListCtrl($scope) {
$scope.photos = [
{"file": "balloons.jpg", "description": "Palloncini colorati."},
{"file": "cards.jpg", "description": "Asso di cuori."},
...
{"file": "watchmaker.jpg", "description": "L'orologiaio."}
];
}
In realtà questo controller fa ben poco: si limita a creare il model per la nostra applicazione costituito da un array di oggetti JSON con due proprietà: l'URL della foto e la relativa descrizione.
La cosa interessante è che la funzione riceve dal sistema lo scope corrente, cioè l'oggetto di sistema che consente la sincronizzazione tra model e view, tramite il parametro $scope
.
Da notare che per convenzione tutti gli oggetti di sistema di AngularJS hanno il nome che inizia per $. Lo scope corrente sarà l'ambiente di valutazione delle espressioni incluse all'interno dell'elemento gestito dal controller, nel nostro caso l'elemento <body>
.
Nell'ambito dello scope corrente viene creato l'array di oggetti JSON che rappresenta l'elenco delle foto ed in questo ambito vengono valutate le espressioni che abbiamo specificato nell'HTML. In particolare, l'espressione photo in photos assegnata alla direttiva ng-repeat consente di selezionare per ciascuna iterazione un elemento dell'array photos assegnandolo alla variabile photo. Tramite questa variabile abbiamo accesso alle singole proprietà dell'oggetto che rappresenta la foto.
Il risultato è mostrato dalla seguente figura:
Nella seconda parte dell'articolo vedremo come implemetare il model ed effettuare il data-binding.
Model e data-binding
Continuiamo la realizzazione del nostro catalogo fotografico introducendo la possibilità di filtrare l'elenco delle foto. AngularJS ci consente di realizzare questa funzionalità in maniera abbastanza semplice.
Per iniziare modifichiamo la struttura del model aggiungendo la proprietà tags
a ciascun oggetto che rappresenta una foto. Come si può facilmente intuire, questa proprietà consente di assegnare delle parole chiave a ciascuna foto:
function PhotoListCtrl($scope) {
$scope.photos = [
{"file": "balloons.jpg", "description": "Palloncini colorati.",
"tags": "palloncini giallo rosso verde colori"},
{"file": "cards.jpg", "description": "Asso di cuori.",
"tags": "carte da gioco asso di cuori"},
...
{"file": "watchmaker.jpg", "description": "L'orologiaio.",
"tags": "orologio orologiaio occhi"}
];
}
Aggiungiamo quindi alla pagina HTML il box di ricerca mediante il seguente codice:
Cerca: <input ng-model="query">
Ritroviamo nuovamente la direttiva ng-model
che implementa il data binding tra la casella di testo ed una variabile query del model. È importante notare che non c'è bisogno di dichiarare la variabile query tramite codice JavaScript; il sistema stesso genererà la variabile e la renderà disponibile nello scope corrente.
Modifichiamo quindi l'espressione assegnata alla direttiva ng-repeater come mostrato di seguito:
<li ng-repeat="photo in photos | filter:query">
Con questa espressione abbiamo chiesto ad AngularJS di filtrare gli elementi dell'array photos in base al valore della variabile query. Il filtro degli elementi dell'array è dinamico, nel senso che al variare del valore della variabile query viene automaticamente applicato il nuovo filtro.
Il risultato è la visualizzazione delle foto che contengono nella descrizione o tra i tag le parole chiave specificate dall'utente:
AngularJS mette a disposizione un certo numero di filtri che hanno il compito di organizzare e formattare i dati per visualizzarli all'utente all'interno di una view. Oltre a filter che abbiamo già visto segnaliamo:
Filtro | Descrizione |
---|---|
currency |
formatta un valore numerico come valuta |
date |
formatta una data in base al formato specificato |
limitTo |
crea un nuovo array con un numero limitato di elementi dall'array originario |
lowercase |
converte i caratteri di una stringa in minuscolo |
number |
formatta un numero come testo |
orderBy |
ordina un array in base ad una espressione |
uppercase |
converte i caratteri di una stringa in maiuscolo |
È possibile combinare più filtri in un'espressione tramite l'operatore pipe (|
). Ad esempio, se vogliamo filtrare le foto in base al valore inserito dall'utente ed ordinare i risultati alfabeticamente in base alla descrizione possiamo specificare la seguente espressione:
<li ng-repeat="photo in photos | filter:query | orderBy:'description'">
Ecco l'esempio:
Nella terza parte dell'articolo vediamo come effettuare il routing e dichiarare view multiple.
Routing e view multiple
Il catalogo che stiamo costruendo si limita finora a visualizzare l'elenco delle foto in formato miniatura ed a consentire il loro filtraggio. Vediamo come proseguire col nostro progetto per visualizzare la foto nelle sue dimensioni reali e con eventuali altri dettagli. In altre parole, vorremmo aggiungere alla attuale view, una view relativa ai dettagli di ciascuna foto.
La soluzione di aggiungere la nuova view direttamente nel codice della pagina HTML attuale ne complicherebbe la gestione oltre che rendere ingestibile il codice al crescere dell'applicazione. AngularJS prevede un meccanismo che consente di definire le view in file separati (partial templates) e gestirle all'interno di uno schema comune detto layout template. Vediamo in pratica come procedere.
Ridefiniamo la pagina HTML nel seguente modo:
<!doctype html>
<html ng-app="photoApp">
<head>
<meta charset="utf-8">
<title>Catalogo fotografico</title>
<link rel="stylesheet" href="css/app.css">
<script src="js/angular.min.js"></script>
<script src="js/app.js"></script>
</head>
<body>
<div ng-view>
</div>
</body>
</html>
Quello che abbiamo fatto è sostituire il contenuto del body con un <div>
marcato con la direttiva ng-view. Questa direttiva indica al sistema che il div ospiterà le view che via via si renderanno necessarie in base ad una specifica configurazione che vedremo tra un po'. Le singole view verranno rappresentate in due file HTML separati, chiamiamoli photo-list.html
e photo-detail.html
, contenenti la porzione di codice HTML da visualizzare all'interno del div.
Ritornando al nuovo codice della pagina HTML, notiamo anche che alla direttiva ng-app
abbiamo associato un nome, photoApp
nello specifico. Con questo stiamo indicando ad AngularJS che abbiamo bisogno di effettuare delle configurazioni particolari per la nostra applicazione e che queste configurazioni verranno eseguite nel modulo photoApp
.
Nella terminologia del framework, un modulo è un oggetto che ha lo scopo di definire le caratteristiche di un'applicazione in termini di configurazione, di dipendenze, di servizi utilizzati, di personalizzazioni, ecc. Nel nostro caso creeremo un modulo photoApp che indicherà ad AngularJS come gestire le viste da visualizzare nel div marcato con la direttiva ng-view:
var modulo = angular.module('photoApp', []);
modulo.config(function($routeProvider) {
$routeProvider.when('/photos',
{templateUrl: 'photo-list.html', controller: PhotoListCtrl});
$routeProvider.when('/photos/:id',
{templateUrl: 'photo-detail.html', controller: PhotoDetailCtrl});
$routeProvider.otherwise({redirectTo: '/photos'});
});
Analizzando il codice vediamo che un modulo viene definito tramite il metodo module() a cui passiamo il nome specificato in corrispondenza della direttiva ng-app ed un array vuoto. Il secondo parametro indica le eventuali dipendenze del modulo che stiamo creando da altri moduli. Nel nsotro caso non ci sono dipendenze.
Tramite il metodo config() del modulo possiamo specificare le configurazioni da applicare al nostro modulo. Nel nostro caso vogliamo indicare ad AngularJS come individuare la view corretta da visualizzare. Questo lo facciamo sfuttando un meccanismo di routing analogo a quello tipico delle applicazioni server. In sostanza l'obiettivo è quello di mappare degli URL alle view e per fare questo ricorriamo all'oggetto di sistema $routeProvider.
Tramite il metodo when() mappiamo ad un percorso un oggetto JSON che definisce il file che contiene il codice HTML della view ed il relativo controller. Il metodo otherwise() consente di specificare il percorso a cui fare riferimento nel caso non sia stato specificato un URL corretto.
La specifica :id all'interno secondo del percorso rappresenta una parte variabile dell'URL che verrà istanziata dinamicamente e che nel nostro caso corrisponderà all'id della foto da visualizzare.
In pratica, supponendo che index.html
sia la pagina contenente il template layout, in corrispondenza dell'URL index.html#/photos
sarà visualizzata la view con l'elenco delle foto, mentre a index.html#/photos/x
corrisponderà la view che visualizza la foto con id uguale a x.
A questo punto dobbiamo creare il contenuto dei file che definiscono le due viste. Il file photo-list.html
avrà il seguente contenuto:
<div>
Cerca: <input ng-model="query">
</div>
<ul>
<li ng-repeat="photo in photos | filter:query | orderBy:'description'">
<p>
<a href="#/photos/{{photo.id}}"><img ng-src="photos/thumb/{{photo.file}}" alt="{{photo.description}}" /></a>
{{photo.description}}
</p>
</li>
</ul>
È lo stesso codice che avevamo nella pagina principale con la sola aggiunta del collegamento sull'immagine. L'URL assegnato al collegamento è costruito come stabilito nella configurazione del routing, con il valore dell'id della foto specificato tramite l'espressione {{photo.id}}
.
Ricordiamo che la valutazione delle espressioni e delle direttive specificate in questo codice è possibile perché nella configurazione del modulo della nostra applicazione gli è stato assegnato il controller PhotoListCtrl
.
Per quanto riguarda la view del dettaglio, il controller PhotoDetailCtrl sarà definito come segue:
var photos = [ {"file": "balloons.jpg",
"description": "Palloncini colorati.",
"tags": "palloncini giallo rosso verde colori"},
{"file": "cards.jpg",
"description": "Asso di cuori.",
"tags": "carte da gioco asso di cuori"},
...
{"file": "watchmaker.jpg",
"description": "L'orologiaio.",
"tags": "orologio orologiaio occhi"}];
function PhotoDetailCtrl($scope, $routeParams) {
$scope.photo = photos[$routeParams.id - 1];
}
Nella definizione di questo controller, oltre allo scope abbiamo specificato l'oggetto di sistema $routeParams
che ci consente di ottenere gli eventuali parametri passati nell'URL di richiesta della view. Per semplicità abbiamo assunto che dall'id possiamo recuperare la posizione all'interno dell'array delle foto. Nella pratica dovremmo effettuare una ricerca all'interno dell'array o, molto più realisticamente, i dati dovrebbero essere gestiti sul server, ma su questo torneremo in seguito.
In sintesi, il controller recupera la foto identificata tramite l'id e crea una variabile photo nello scope corrente. Utilizzeremo questa variabile nel codice HTML presente in photo-detail.html
:
<img ng-src="photos/{{ photo.file }}" alt="{{ photo.description}}">
<p>
<b>Descrizione</b>:<br/>
{{ photo.description}}
</p>
<p>
<b>Parole chiave</b>:<br>
{{ photo.tags}}
</p>
<a href="#/photos">Torna all'elenco delle foto</a>
Il codice che rappresenta la view di dettaglio non fa altro che visualizzare la foto, i dati associati e un collegamento per tornare all'elenco delle foto:
Il meccanismo illustrato permette di creare applicazioni in maniera modulare definendo view e relativi controller e componendoli insieme grazie alla definizione dei moduli. Con questo approccio è addirittura pensabile la creazione di componenti riutilizzabili e configurabili in base alla specifica applicazione.
Form validation
Proseguiamo nella realizzazione della nostra applicazione introducendo la possibilità di aggiungere o modificare nuove foto al catalogo. Per far questo creeremo un form che consenta di gestire i dati di ciascun elemento del catalogo. Il form sarà visualizzato in una nuova view del nostro progetto, quindi occorrerà modificare la configurazione del nostro modulo photoApp, creare un template parziale per la view ed il relativo controller.
Modifichiamo quindi la configurazione del nostro modulo aggiungendo una nuova route:
modulo.config(function($routeProvider) {
$routeProvider.when('/photos', {templateUrl: 'photo-list.html', controller: PhotoListCtrl});
$routeProvider.when('/photos/photo', {templateUrl: 'photo-form.html', controller: PhotoFormCtrl});
$routeProvider.when('/photos/:id', {templateUrl: 'photo-detail.html', controller: PhotoDetailCtrl});
$routeProvider.otherwise({redirectTo: '/photos'});
});
La route che abbiamo aggiunto mappa il percorso /photos/photo
al template photo-form.html
associandogli il controller PhotoFormCtrl
. Occorre prestare attenzione all'ordine con cui si creano le route, dal momento che queste vengono valutate nello stesso ordine in fase di esecuzione. Se avessimo inserito la nostra nuova route in fondo a quelle già presenti non avremmo mai potuto accedere alla nostra form dal momento che il percorso /photos/photo sarebbe stato catturato dalla route photos/:id
.
Creiamo quindi il template della form come mostrato di seguito:
<form>
<label for="">Nome file<label> <input type="text" ng-model="photo.file"/><br />
<label for="">Descrizione<label> <input type="text" ng-model="photo.description"/><br />
<label for="">Parole chiave<label> <input type="text" ng-model="photo.tags" /><br />
<button ng-click="reset()">Annulla</button>
<button ng-click="update(photo)">Salva</button>
</form>
Abbiamo creato un form con tre campi che rappresentano i dati rilevanti per una foto. Per semplicità non abbiamo previsto un campo di tipo file per meglio concentrarci sulle funzionalità generali della gestione dei form con AngularJS piuttosto che sullo specifico controllo.
A ciascuna casella di testo abbiamo associato un elemento del model tramite la direttiva ng-model ed abbiamo già visto come ciò crei automaticamente un elemento del model e lo tenga costantemente sincronizzato con la view.
Chiudono la form due pulsanti che presentano la direttiva ng-click il cui scopo, come è facile intuire, consiste nell'associare all'evento clic l'esecuzione di funzioni del controller.
In effetti il controller per la nostra view si limita a definire le seguenti due funzioni assegnandole allo scope corrente:
function PhotoFormCtrl($scope) {
$scope.update = function(photo) {
//Salvataggio della foto
...
}
$scope.reset = function() {
$scope.photo = {};
}
}
La funzione update() è delegata al salvataggio della foto mentre la funzione reset() annulla quanto inserito dall'utente.
La definizione di questa form è già funzionante, a parte il salvataggio, in quanto riesce a catturare l'input delll'utente ed a creare l'oggetto che rappresenta la foto. Possiamo rendercene conto inserendo in fondo alla form il seguente codice:
Dati foto: {{photo | json}}
L'espressione prende il valore corrente dell'oggetto photo e lo converte tramite il filtro json in una stringa in formato JSON.
AngularJS ci consente comunque di arricchire la user experience semplicemente: a ciascun form il framework assegna automaticamente una istanza del controller di sistema FormController, che tiene traccia dello stato globale del form e dei singoli controlli in essa contenuti. In particolare lo stato del form e dei controlli viene tracciato tramite le seguenti proprietà:
Proprietà | Descrizione |
---|---|
$pristine |
valore booleano che indica se c'è stata una qualsiasi interazione con l'utente o meno; il valore true indica che l'oggetto si trova nello stato iniziale |
$dirty |
ha valore true se l'utente ha interagito con la form o con il controllo |
$valid |
è true se il controllo è valido o tutti i controlli della form contengono valori validi |
$invalid |
è true se il controllo non è valido o se almeno un controllo della form contiene valori non validi |
$error |
è un oggetto che contiene i riferimenti agli elementi non validi dell'oggetto da convalidare |
Per poter accedere allo stato e ai metodi del FormController sottostante ad un form ed ai controlli in essa contenuti è necessario assegnare un nome tramite l'attributo name.
Ad esempio, per fare in modo che il nome del file venga obbligatoriamente inserito dobbiamo assegnare un nome alla form ed alla casella di testo coinvolta e quindi specificare l'attributo required per quest'ultima:
<form name="photoForm">
<label for="">File<label>
<input type="text" ng-model="photo.file" name="file" ng-maxlength="20" required/>
...
</form>
A questo punto possiamo sfruttare l'accesso allo stato della casella di testo per segnalare che l'immissione di un valore è obbligatoria:
<form name="photoForm">
<label for="">File<label>
<input type="text" ng-model="photo.file" name="file" ng-maxlength="20" required/>
<span ng-show="photoForm.file.$dirty && photoForm.file.$error.required">Valore obbligatorio!</span><br>
...
</form>
Abbiamo inserito uno <span>
la cui visualizzazione è vincolata, tramite la direttiva ng-show
, al valore di un'espressione booleana. L'espressione valuta se c'è stata interazione con la casella di testo file (questo per evitare che il messaggio compaia prima ancora che l'utente vi interagisca) e se la condizione required
è stata rispettata.
Se l'espressione ha valore true viene visualizzato il messaggio:
Possiamo anche abilitare o disabilitare il pulsante di salvataggio in base allo stato globale della form:
<button ng-click="update(photo)" ng-disabled="photoForm.$invalid">Salva</button>
Tramite la direttiva ng-disabled indichiamo che il pulsante deve essere disabilitato fintantochè la form non è valida.
Come abbiamo potuto vedere con questi semplici esempi, la promessa di AngularJS di favorire un approccio dichiarativo si rivela realistica. Siamo infatti riusciti a fare con delle semplici configurazioni sulla pagina HTML cose che normalmente richiedono la scrittura di codice JavaScript. E le possibilità sono tante, come si può vedere dalla documentazione del framework.
Utilizzare gli Angular services
Il form che abbiamo realizzato nella sezione precedente era stata lasciata volutamente senza l'implementazione del salvataggio dei dati sul server. Anche l'implementazione dell'accesso ai dati delle foto era stato lasciato intenzionalmente semplice, basandolo su un array statico di oggetti nel codice JavaScript.
Nella realtà questo raramente accade dal momento che in genere i dati vengono gestiti dinamicamente dal server. Per poter interagire con il server dobbiamo far ricorso ad uno dei service predefiniti di AngularJS: $http
.
In generale un service è un componente che consente di eseguire delle attività tipiche delle applicazioni web. Il service $http, ad esempio, consente di interagire con il server tramite richieste HTTP mentre $location permette di gestire gli URL e $window
consente di lavorare con la finestra del browser.
AngularJS prevede diversi service predefiniti ed altri possono essere creati dallo sviluppatore.
L'architettura del framework cerca di mantenere al minimo l'accoppiamento tra i service e le applicazioni basandosi a questo scopo sulla dependency injection. Per questo motivo è necessario dichiarare esplicitamente il service che si intende utilizzare all'interno di uno scope. Vediamo di chiarire questo concetto con un esempio.
Supponiamo che la nostra applicazione preveda una Web API che ci fornisce l'elenco dei dati sulle foto in formato JSON e che tale API sia disponibile all'indirizzo /api/photos. Per ottenere questi dati dobbiamo effettuare una richiesta HTTP di tipo GET al caricamento dell'applicazione. Possiamo fare questo intervenendo sulla definizione del nostro modulo photoApp come segue:
var photos;
var modulo = angular.module('photoApp', []);
modulo.config(function($routeProvider) {
...
})
modulo.run(function($http) {
$http.get('/api/photos')
.success(function(data) {
photos = data;
})
.error(function(data, status) {
alert("Si è verificato un errore nel caricamento delle foto!")
});
});
Abbiamo aggiunto una chiamata al metodo run() del modulo associato alla nostra applicazione che ha il compito di eseguire la funzione passata come parametro al completamento dell'inizializzazione. La funzione passata dichiara di voler utilizzare il service $http indicandolo come parametro, quindi invoca il metodo get() per effettuare una richiesta all'API web. In caso di successo la risposta del server viene assegnata alla variabile globale photos altrimenti viene generato un messaggio d'errore. Da notare come AngularJS si faccia carico di decodificare automaticamente la stringa JSON inviata dal server trasformandola in un array di oggetti.
Per completare l'interazione con il server, vediamo come implementare il salvataggio dei dati inseriti dall'utente tramite la form. Supponendo che il salvataggio di una nuova foto venga effettuato tramite una richiesta HTTP di tipo POST modifichiamo il controller PhotoFormCtrl nel seguente modo:
function PhotoFormCtrl($scope, $http) {
$scope.update = function(photo) {
$http.post('/api/photos', photo)
.success(function(data, status) {
alert("Salvataggio effettuato!");
})
.error(function(data, status) {
alert("Si è verificato un problema durante il salvataggio!");
});
}
$scope.reset = function() {
$scope.photo = {};
}
}
Anche in questo caso dichiariamo l'intenzione di voler utilizzare il service $http aggiungendolo tra i parametri del costruttore del controller. Quindi implementiamo il salvataggio tramite il metodo post() gestendo il corretto salvataggio e l'eventuale errore di comunicazione.
Dependecy Injection e modello dichiarativo
In questo articolo abbiamo avuto un piccolo assaggio delle potenzialità di AngularJS. Esso va oltre il semplice supporto del pattern MVC introducendo un approccio fortemente orientato ad un modello dichiarativo della programmazione. Non a caso il suo nome vuole richiamare le parentesi angolari (angular brackets) tipiche dei tag HTML: l'intenzione è di spostare il più possibile nell'HTML le operazioni di configurazione e di presentazione dei dati consentendo a JavaScript di concentrarsi nell'elaborazione della logica applicativa e nell'interazione, tramite i service, con il server o con gli altri componenti coinvolti.
Un'attenzione particolare viene posta sulla personalizzazione: AngularJS prevede la possibilità di creare i propri service, di estendere o sostituire i service di sistema e addirittura di creare direttive personalizzate. Tutto questo è possibile grazie alla scelta di adottare la dependency injection come meccanismo di isolamento dei i componenti di un'applicazione.
Questo stesso meccanismo favorisce inoltre la testabilità del codice, argomento molto a cuore ai sostenitori del progetto.
È inoltre possibile utilizzare AngularJS in combinazione con altre librerie. Esso stesso utilizza internamente una versione ridotta di jQuery, jqLite, per l'accesso al DOM. Tuttavia, AngularJS è in grado di rilevare l'eventuale presenza della versione completa di jQuery dandogli sempre la precedenza.
Per un framework così ricco di funzionalità è molto importante l'organizzazione dei file di un progetto. Per questo motivo il team di sviluppo ha messo a dispozione uno scheletro di progetto, Angular-seed, da cui partire in maniera ordinata. Si tratta di un insieme di file organizzati in cartelle come mostrato nella seguente figura:
In ciascuna cartella sono presenti i file rilevanti per una semplice applicazione d'esempio con relativi script di gestione e test unitari. Naturalmente si tratta di un ambiente di sviluppo, per la pubblicazione finale è sufficiente copiare il contenuto della cartella app.
Concludiamo con il sottolineare la ricchezza e la completezza della documentazione disponibile sul sito del progetto.
Link utili