Nel campo delle applicazioni mobile e Web la separazione fra backend e frontend è netta: la gestione dell'interfaccia è delegata alla logica che gira su client, mentre l'applicativo lato server gestisce operazioni CRUD e l'elaborazione dei dati da fornire al client sotto forma di API o servizio.
Sebbene non vi sia una regola univoca di fornitura delle API, un buon modo per garantire la manutenibilità del proprio servizio è quello di utilizzare un architettura REST, mentre lato client il formato più digeribile è JSON, rappresentando in definitiva un oggetto JavaScript.
In questo articolo vedremo come configurare e utilizzare un servizio in AngularJS per consumare dati provenienti da un'API REST. In particolare vedremo come gestire la visualizzazione di una lista di utenti.
View e Controller di base
Il primo passo per realizzare l'applicazione è quello di creare la view che conterrà la lista degli utenti:
<table class="table table-striped" ng-controller="ListController">
<thead>
<tr>
<th>#</th>
<th>Nome</th>
<th>Username</th>
<th>E-mail</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="user in users">
<td>{{$index + 1}}</td>
<td>{{user.name}}</td>
<td>{{user.username}}</td>
<td>{{user.email}}</td>
</tr>
</tbody>
</table>
Ciò faremo sarà caricare, a partire dal ListController
, una serie di utenti in $scope.users
e ciclarli attraverso una direttiva ng-repeat
. Una prima versione statica dell'applicazione è visibile nell'esempio 1.
Poiché per gestire i dati del model è consigliabile utilizzare un servizio sposteremo l'array degli utenti in userService
. Ecco il codice della nostra applicazione e la corrispondente versione live esempio 2:
angular.module('userListApp', [])
.factory('userService', function () {
var users = [
//...
];
return {
users: users
};
})
.controller('ListController', function ($scope, userService) {
$scope.users = userService.users;
});
Chiamate AJAX con $http
Il servizio predefinito per effettuare chiamate AJAX in AngularJS è $http. Questo oggetto espone diversi metodi per i vari tipi di chiamate HTTP i quali accettano come parametro l'URL da chiamare e restituiscono una promise per poter gestire la risposta del server:
//esempio di chamata GET
$http.get('ajax/endpoint/')
.success(function (data) {
//lanciata quando la chiamata ha successo
})
.error(function () {
//lanciata quando la chiamata fallisce
});
Notiamo che di default il servizio si aspetta che i dati restituiti siano in formato JSON.
Caricare i dati
Per prima cosa realizzeremo il caricamento dei dati esterni attraverso il metodo GET
. Nell'esempio utilizzeremo il servizio jsonplaceholder che simula un API REST restituendo valori fittizi.
L'URL da utilizzare per visualizzare una lista di utenti è: http://jsonplaceholder.typicode.com/users
, quindi il codice del nostro servizio diventerà:
angular.module('userListApp', [])
.factory('userService', function ($http) {
var users = [];
return {
users: users,
load: function () {
$http
.get('http://jsonplaceholder.typicode.com/users')
.success(function (data) {
users.push.apply(users, data);
});
}
};
});
All'interno del controller richiameremo il metodo .load()
in questo modo:
angular.module('userListApp', [])
.controller('ListController', function ($scope, userService) {
$scope.users = userService.users;
userService.load();
});
Per semplicità non abbiamo impostato la callback .error()
tuttavia, in ambienti di produzione, è sempre bene definirla per gestire gli errori del server e dare un feedback coerente all'utente.
Ecco la nostra applicazione che prende vita: esempio 3.
Visualizzare dati singoli
Realizzata la lista, potremmo voler visualizzare i dettagli di un singolo record. A livello di view va aggiunto il pannello con le relative direttive ed espressioni:
<div class="panel panel-default" ng-show="currentUser">
<div class="panel-heading">
<strong>{{currentUser.name}}</strong>
<button type="button" class="close" aria-hidden="true" ng-click="showUser(null)">×</button>
</div>
<div class="panel-body">
E-mail: {{currentUser.email}} <br />
Telefono: {{currentUser.phone}}
</div>
</div>
Quindi dobbiamo inserire un link nella tabella con la relativa funzione da richiamare al click:
<tr ng-repeat="user in users">
...
<td>
<a href="" ng-click="showUser(user.id)">
{{user.name}}
</a>
</td>
...
</tr>
Infine aggiorniamo il controller:
angular.module('userListApp', [])
.controller('UsersController', function ($scope, userService) {
$scope.users = userService.users;
// utente corrente
$scope.currentUser = null;
// metodo per impostare l'utente corrente
$scope.showUser = function (id) {
};
userService.load();
});
A questo punto ciò che manca è la logica del servizio per richiamare un singolo record in base al suo id. Secondo lo stantard REST basta eseguire una chiamata GET ed appendere l'identificativo del record alla fine dell'URL usato per la lista. Nel nostro caso quindi dovremo chiamare: http://jsonplaceholder.typicode.com/users/{id}
.
angular.module('userListApp', [])
.factory('userService', function ($http) {
var users = [];
return {
//...
loadSingle: function (id) {
return $http.get('http://jsonplaceholder.typicode.com/users/' + id);
}
};
});
A differenza di .load()
, il metodo .loadSingle()
restituisce il risultato di $http.get()
che, come abbiamo visto, è una promise. Questo ci permette di accodare funzioni di callback personalizzate in ogni istanza in cui il metodo viene invocato. $scope.showUser()
diventerà quindi:
$scope.showUser = function (id) {
if (id) {
userService.loadSingle(id).success(function (data) {
$scope.currentUser = data;
});
} else {
$scope.currentUser = null;
}
};
Ecco un esempio live con il codice completo: esempio 4
Salvare, modificare e cancellare contenuti
L'architettura REST prevede altri 3 metodi oltre a GET: POST, PUT e DELETE da utilizzare rispettivamente per aggiungere, aggiornare e rimuovere un record.
Il servizio $http
supporta ognuno di questi metodi con le omonime funzioni $http.post
, $http.put
e $http.delete
. Questo ci permette di interfacciare completamente la nostra applicazione con una risorsa REST.
REST con $resource
Il problema più evidente di $http
è che ogni chiamata è indipendente dalle altre, costringendoci a riscrivere ogni volta l'URL da chiamare.
Per applicazioni un po' più complesse è necessario guardare ad uno dei moduli aggiuntivi di AngularJS $resource, un wrapper di $http
sviluppato appositamente per interfacciarsi con semplici API REST, garantendo al tempo stesso un'elevata configurabilità.
Il modulo va scaricato o installato con bower quindi, dopo averlo aggiunto agli script della pagina, va iniettato come dipendenza dell'applicazione:
angular.module('myModule', ['ngResource']);
A questo punto possiamo iniettarlo al posto di $http
:
angular.module('myModule', ['ngResource'])
.factory('userService', function ($resource) {
var res = $resource('/api/users/');
//...
});
La variabile res
è un'instanza riusabile di $resource
, a partire dalla quale possiamo chiamare una serie di metodi come .get()
, .query()
(utilizzato al posto di .get()
per ricavare liste di contenuti), .post()
e .delete()
.
Ognuno di questi metodi è personalizzabile e configurabile. Ad esempio potremmo dover gestire una risposta del server in cui i dati non si trovano nel nodo radice, in tal caso basterà intercettare la risposta del server e adattarla alle nostre esigenze:
var res = $resource('/api/users/', {}, {
query: {
isArray: true,
method: 'GET',
transformResponse: function (data) {
//data è la risposta grezza
//converto in JSON e restituisco il
//nodo con i dati
return angular.fromJson(data).body;
}
}
});
//...
res.query();
A differenza di $http
i metodi di $resource
non resituiscono una promise, quindi è sempre necessario ricorrere alle callback per gestire la logica di successo ed errore. È tuttavia possibile accedere alla promise originale della chiamata tramite la proprietà .$promise
, quindi ad esempio le due chiamate di seguito sono equivalenti:
$resource('/mi/api').query(successCallback, errorCallback);
$resource('/mi/api').query().$promise.then(successCallback, errorCallback);
Il vantaggio del secondo formato è che ci permette di sfruttare le promise a cascata per controllare il flusso dei dati.
Da $http a $resource
Nel nostro caso sarà anzitutto necessario caricare ngResource
come dipendenza, iniettarlo nel servizio e instanziarlo:
angular.module('userListApp', ['ngResource'])
.factory('userService', function ($resource) {
//...
var res = $resource('http://jsonplaceholder.typicode.com/users/:userid');
//...
});
Quindi riscriveremo i due metodi .load()
e .loadSingle()
nel seguente modo:
load: function () {
return res.query(function (data) {
users.push.apply(users, data);
);
},
loadSingle: function (id) {
//restituisco la promise
//in modo da poter user .then()
//nella view
return res.get({userid: id}).$promise;
}
Rispetto al codice originale, in .load()
restituiamo il valore della chiamata; questo ci permette di sfruttare un'altra caratteristica di $resource
e cioè che ogni metodo dell'istanza restituisce un riferimento ad un oggetto vuoto che viene popolato alla fine della chiamata.
In questo modo possiamo, ad esempio, legare il risultato di .load()
ad una proprietà dello scope ed usarla per visualizzare condizionalmente un box con il numero di record caricati:
angular.module('userListApp')
.controller('UsersController', function ($scope, userService) {
// ...
$scope.userCount = userService.load();
});
<!-- view -->
<div class="alert alert-info" ng-show="userCount && userCount.length">
Utenti: {{userCount.length}}
</div>
Ecco l'applicazione completa: esempio 5.
Documentazione e riferimenti
Per approfondire lo sviluppo di API REST e sfruttare appieno le potenzialità di AngularJS si può fare riferimento all'esaustiva documentazione ufficiale di $http e $resource. Per API REST particolarmente complesse e applicazioni in larga scala potrebbe essere anche interessante valutare RESTAngular un modulo aggiuntivo di terze molto ben documentato e performante.
Tutti gli esempi sono disponibili in allegato per il download.