AngularJS mini How-To (seconda parte)

Utilizzare i template e le rotte

Continuiamo questa introduzione ad AngularJS partendo dal punto in cui ci eravamo interrotti nella prima parte. Ad ora la nostra applicazione è composta da una sola view che mostra la lista dei piloti. Vediamo ora come estendere il nostro progetto introducendo la possibilità di visualizzare i dettaglio del pilota selezionato andando a creare una seconda pagina.

Aggiungere la nuova view direttamente nel codice della pagina HTML non risulta una buona soluzione in quanto complicherebbe la gestione e renderebbe il codice ingestibile al crescere dell’applicazione. Fortunatamente AngularJS mette a disposizione un meccanismo, chiamato partial templates, che permette di definire le view in file separati e gestirle all’interno di uno schema comune chiamato layout template. Nel nostro caso il file HTML su cui abbiamo lavorato sino ad ora diventerà il nostro layout template ed avrà questo aspetto

<!DOCTYPE html>
<html ng-app="motoGpRiders">
<head lang="en">
    <meta charset="UTF-8">
    <title>Moto GP riders</title>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.18/angular.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.18/angular-route.js"></script>
    <script src="app.js"></script>
    <script src="controller.js"></script>

    <link rel="stylesheet" href="style.css"/>
</head>
<body>
<div ng-view></div>
</body>
</html>

Com’è possibile osservare, il contenuto del tag <body> è stato sostituito con un <div> arricchito con l’attributo ng-view. Questo indica al sistema che il div ospiterà le view, definite in file HTML esterni. Come sia possibile specificare quale di queste view dovrà essere utilizzata lo vedremo tra breve quando parleremo delle routes.

Un ulteriore modifica riguarda la direttiva ng-app alla quale ora è stato associato il valore motoGpRiders. Questa dicitura indica ad AngularJS che abbiamo bisogno di effettuare delle configurazioni particolari per la nostra applicazione e che queste verranno eseguite all’interno del modulo motoGpRiders. Questo viene definito all’interno di un file chiamato app.js (il nome del file non è vincolante), con il seguente codice:

var module = angular.module('motoGpRiders', []);

La funzione angular.module accetta come primo parametro il nome del modulo il quale sarà visibile all’interno della nostra applicazione (per chi mastica Java, è paragonabile alla definizione di una classe pubblica), mentre il secondo parametro conterrà una lista di stringhe che indicano le dipendenze da altri moduli (una sorta di import).

Nella terminologia del framework, un modulo è un oggetto che ha lo scopo di definire le caratteristiche di un’applicazione in termini di configurazione, servizi, controller, direttive e molto altro, incapsulandoli all’interno di un pack riutilizzabile in futuro. Per il nostro progetto configureremo il modulo motoGpRiders in modo tale da visualizzare nel div marcato con la direttiva ng-view il corretto partial template:

var module = angular.module('motoGpRiders', [
    'ngRoute',
    'controllers'
]);

module.config(function ($routeProvider) {
        $routeProvider.when('/riders', {
            templateUrl: 'list.html',
            controller: 'ridersListController'
        }),

        $routeProvider.when('/riders/:number', {
            templateUrl: 'details.html',
            controller: 'riderDetailsController'
        }),

        $routeProvider.otherwise({
            redirectTo: '/riders'
        })
    }
);

Tramite il metodo config() possiamo specificare le configurazioni da applicare al nostro modulo. Nel nostro caso vogliamo indicare ad AngularJS come individuare la view corretta da visualizzare. Per fare questo sfruttiamo un meccanismo di routing analogo a quello tipico delle applicazioni server. In sostanza l’obiettivo è quello di creare delle rotte, ovvero mappare degli URL a delle view ricorrendo all’oggetto $routeProvider (nota: è necessario inserire la dipendenza dal modulo ngRoute).

Tramite il metodo when() abbiamo la possibilità di mappare un URL ad un oggetto JSON che definisce il file che contiene il codice HTML del partial template ed il relativo controller. Il metodo otherwise() consente di specificare la configurazione a cui fare riferimento nel caso non sia stato specificato un URL corretto. Nel nostro caso riportiamo l’applicazione della pagina principale tramite un semplice

redirectTo: '/riders'

La specifica :number all’interno della seconda route rappresenta una parte variabile dell’URL che verrà istanziata dinamicamente e che nel nostro caso corrisponderà al numero del pilota da visualizzare.
In pratica, supponendo che index.html sia la pagina contenente il template layout, in corrispondenza dell’URL index.html#/riders sarà visualizzata la view con l’elenco dei piloti, mentre a index.html#/riders/n corrisponderà la view che visualizza il dettaglio del pilota con il numero di gara uguale a n.

A questo punto è necessario creare due file (list.html e details.html) che conterranno il codice HTML delle relative view. Infine è necessario aggiornare il nostro file controller nel seguente modo:

var riders = [
    {
        name: "Valentino Rossi",
        number: 46,
        team: "Movistar Yamaha MotoGP",
        nation: 'ita',
        height: 182,
        weight: 65,
        city: "Urbino"
    },
    ...
]

var controllerManager = angular.module('controllers', []);

controllerManager.controller('ridersListController', function($scope){
    $scope.riders = riders;
});

controllerManager.controller('riderDetailsController', function($scope, $routeParams){
    $scope.rider = riders.filter(function(rider){
            return rider.number == $routeParams.number;
    })[0];
});

Sostanzialmente abbiamo definito un modulo, denominato controllers, il cui scopo è definire i controller per la nostra applicazione. La creazione avviene utilizzando la funzione

controllerManager.controller('ridersListController', function($scope){
    ...
});

dove il primo parametro indica il nome assegnato, mentre il secondo è una funzione all’interno della quale verranno inserite tutte le logiche applicative per gestire le diverse interazioni.

Nella definizione del secondo 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 partial template. In sintesi, il controller recupera le informazioni sul pilota tramite il numero di gara  e crea una variabile rider nello scope corrente.  Tale variabile verrà utilizzata nel codice HTML presente in details.html. E’ possibile reperire il codice sorgente di questo passo al seguente indirizzo: https://github.com/yupitz/motoGpRiders/tree/step5

Il risultato finale è il seguente: routing result

Comunicazione via HTTP

Sino ad ora abbiamo prelevato, per semplicità, le informazioni dei piloti da un array JSON definito all’interno del controller. Nella realtà questo raramente accade dal momento che in genere i dati vengono prelevati tramita la comunicazione con un server web. Per poter fare ciò 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. Per poter utilizzare i diversi service è necessario dichiarare esplicitamente il service che si intende utilizzare all’interno di una scope.

Vediamo da punto di vista pratico come si traduce quanto è stato appena descritto. Supponiamo che la nostra applicazione preveda una Web API che ci fornisce l’elenco dei dati sui piloti in formato JSON e che tale API sia disponibile all’indirizzo server/api/riders.json. Per ottenere questi dati dobbiamo effettuare una richiesta HTTP di tipo GET al caricamento dell’applicazione. Possiamo fare questo intervenendo sulla definizione del nostro controller come segue:

var controllerManager = angular.module('controllers', []);
var riders;

module.run(function($http){
    $http.get('server/api/riders.json').
        success(function(data){
            riders=data;
        }).
        error(function(data, status){
            alert("Si è verificato un errore nel caricamento della lista dei piloti.");
        });
});

controllerManager.controller ...

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.

Termina qui questo tutorial su AngularJS; è possibile reperire il codice sorgente al seguente indirizzo: https://github.com/yupitz/motoGpRiders/tree/step-http

Alessandro Modolo

Dopo essermi laureato in informatica nel 2012, specializzandomi nella progettazione e sviluppo software, ho intrapreso il mio cammino lavorativo come programmatore. Ho avuto moto di conoscere da vicino gli ambienti silverlight e android, ma da un'anno a questa parte mi dedico allo sviluppo di applicazioni ibride in campo mobile.

  • Alessandro Puzielli

    Bell’articolo, ma vedendo il codice sorgente c’è una cosa che mi sfugge.

    In app.js e controller.js sono dichiarati due moduli diversi (var module e var controllerManager): ma se il module è l’elemento che che connette i vari controller, service etc, non dovrebbe essere unico?

    • Alessandro Modolo

      Ottima osservazione.

      I moduli in angular possono essere pensati come dei blocchi che inglobano delle funzionalità comuni. Tali funzionalità possono essere servizi, controller, rotte, etc. Nel caso specifico ho voluto separare la gestione delle rotte dall’utilizzo dei controller per la gestione dell’interfaccia grafica, pertanto ho creato un modulo per ciascuna funzionalità: motoGpRiders per le rotte e controllers in cui ho definito l’occorrente per la gestione delle pagine html. Il fatto di creare moduli diversi permette di a realizzare applicazioni modulari (scusa il gioco di parole) il che consente di mantenere bassa la complessità del codice al crescere dell’applicazione.

      Concordo con te nel fatto che in applicazioni dalle dimensioni così ridotte, l’utilizzo di due moduli diversi possa essere eccessivo/inutile nonché creare un po di confusione (complice anche la scelta poco accurata dei nomi) ma lo scopo era quello di dare un assaggio della potenza che Angular offre nella definizione e nel riutilizzo di moduli.

      Un eventuale refactoring potrebbe prevedere lo spostamento del contenuto del modulo controllers all’interno del modulo motoGpRiders; il comportamento finale resterebbe invariato.

      • maria

        Ciao Alessandro, ho importato il progetto (step 5) ma non gira bene, la pagina localhost…./list.html mostra un campo di ricerca e
        {{rider.number}}
        {{rider.name}}
        Hai idea di cosa potrebbe essere? (ho aggiornato la libreria alla 1.4.7)
        Ciao e grazie

        • Ciao,
          so che abbiamo risolto il problema in forma privata, ma rispondo per rendere partecipi anche gli altri lettori.
          In sintesi si trattava di un problema nel caricamento di uno dei moduli (probabilmente qualche svista sintattica). Cose che succedono 😉

  • giovanni poidomani

    ottimo articolo