Con AngularJS si possono creare delle Single Page Application (SPA) ovvero della applicazioni composte da una sola pagina principale basate su JavaScript in cui le altre pagine (risorse) vengono caricate dinamicamente quando necessario. In questo modo si ottengono delle applicazioni molto simile all'esperienza desktop dove la pagina non si ricarica completamente ma vengono caricati solo alcune parti.

L'esempio che segue è una semplice todo list basata sul modello SPA di AngularJS, è composta da tre pagine più una di errore:
  • /todo : pagina principale con l'elenco dei todo
  • /todo/new : la pagina con il form per inserire nuovi todo
  • /todo/edit/{id} : la pagina di edit di un todo esistente
  • /404 : la pagina di errore pagina non trovata
index.html

La prima cosa da fare è impostare una struttura delle directory dell'applicazione, questo è il mio modello:
/
  app4             ... l'applicazione
    controllers    ... tutti i controllers dell'applicazione
    directives     ... le eventuali direttive
    others         ... l'entry point della applicazione
    pages          ... le pagine dell'applicazione
    services       ... i service/factory che si occupano dell'accesso ai dati
  contents         ... le lbrerie comuni
    css
    fonts
    js
e questi i file che compongono l'applicazione:
/
  app4
    controllers 
      error-404-controller.js
      todo-edit-controller.js
      todo-list-controller.js
    directives
    others
      app.js
    pages
      404.html
      todo-edit.html
      todo-list.html
      todo-new.html
    services
      todo-factory.js
    index.html
  contents
    css
      bootstrap.css
    fonts
      glyphicons-halflings-regular...
    js
      angular.min.js
      angular-route.min.js
Poi vanno scaricate le librerie comuni, Bootstrap 3 (solo css e font) e AngularJS 1.4 con il modulo route.

Fatto questo si può impostare la pagina principlale, index.html, che ha la stessa funzione delle master page in ASP.NET:
<!DOCTYPE html>
<html ng-app="sgartApp">

<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Test 4</title>
  <!-- serve solo il css di bootsrap, non serve il js -->
  <link href="/contents/css/bootstrap.min.css" rel="stylesheet">
  <style type="text/css">
    .completed { text-decoration: line-through;  }
  </style>
</head>

<body>
  <div class="container">
    <h2>Test 4 - TODO Single Page Application</h2>

      <!-- il tag 'ng-view'' definisce il placeholder 
        dove verranno inserite le pagine -->
    <ng-view class="container"></ng-view>
  </div>

  <script src="/contents/js/angular.min.js"></script>
  <!-- per le SPA mi serve un altro file di angular, modulo route -->
  <script src="/contents/js/angular-route.min.js"></script>
  <!-- la definizione dell'applicazione -->
  <script src="./others/app.js"></script>
  <!-- tutti i factories/services per accedere a servizi esterni -->
  <script src="./services/todo-factory.js"></script>
  <!-- tutti i controllers usati nella app -->
  <script src="./controllers/error-404-controller.js"></script>
  <script src="./controllers/todo-list-controller.js"></script>
  <script src="./controllers/todo-edit-controller.js"></script>
</body>
</html>
dove vengono caricati i css necessari, le librerie Angular e i file JavaScript che compongono l'applicazione.
Il ruolo principale è svolto dal tag ng-view che definisce il placeholder in cui verranno inserite la singole pagine. Può essere scritto anche come attributo:
<div class="container" ng-view></div>
Gli attributi Angular possono anche essere scritti con il prefisso data- per renderli compatibili con i validatori html, ad esempio ng-view diventa data-ng-view

Il secondo passo è creare il file app.js per inizializzare l'applicazione e definire le url gestite:
(function() {	//racchiudo sempre tutto in una 'closure'
    "use strict";

    // definisco il nome della app angular
    // e carico il modulo ngRoute che serve per gestire
    // le pagine delle Single Page Applicaion (SPA)
    var app = angular.module('sgartApp', ['ngRoute']);


    // -------------------------------------------------
    // inizio a definire le route / pagine che la app dovrà gestire
    app.config(config);
    // inietto i provider necessari
    config.$inject = ['$routeProvider', '$locationProvider'];
    // definisco il gestore delle route
    function config($routeProvider, $locationProvider) {
        var appBase = '../app4/';   // la folder che contiene la app 
        var version = '?v1';    // angular fa molta cache, cambio la versione ogni volta che cambiano le pagine per forzare il reload
        // di default le route sono case sensitive le rendo insensitive
        $routeProvider.caseInsensitiveMatch = true;

        // imposto la folder dove sono le view
        var viewBase = appBase + 'pages/';
        // inizio a definire la pagine
        $routeProvider
            // gestisco alcuni redirect alla home
            .when('/', {	// path della view/pagina nella app
                redirectTo: '/todo'    // il redirect ad una pagina principale della app
            })
            .when('/index.html', {
                redirectTo: '/todo'
            })
            // inizio a definire le pagine, i relativi controller e il nome dell'alias
            .when('/todo', {	// path della pagina con l'elenco dei todo
                templateUrl: viewBase + 'todo-list.html'+version,   // la posizione fisica della pagina
                controller: 'TodoListCtrl',
                controllerAs: 'ctrl'    // scelgo un nome per l'alias, ogni pagina, se necesario, può avere un alias diverso
            })
            .when('/todo/new', {   // path della pagina new
                templateUrl: viewBase + 'todo-new.html'+version,
                controller: 'TodoEditCtrl', //uso lo stesso controller della edit
                controllerAs: 'ctrl',
                mode: 'new' // per discriminarlo dalla edit passo un parametro
            })
            .when('/todo/edit/:id', {   // path della pagina edit con definto un parametro :id
                templateUrl: viewBase + 'todo-edit.html'+version,
                controller: 'TodoEditCtrl',
                controllerAs: 'ctrl',
                mode: 'edit'
            })

            // gestisco la pagina di errore 
            .when('/404', {
                templateUrl: viewBase + '404.html'+version,
                controller: 'Error404Ctrl',
                controllerAs: 'ctrl'
            })
            .otherwise({ redirectTo: "/404" });	// se non trovata va in 404

    };
})();
dopo questo si possono definire le singole pagine todo-list.html:
<form class="form-inline">
  <a class="btn btn-success" ng-href="#/todo/new">
    <span class="glyphicon glyphicon-file"></span> Create New
  </a>
</form>
<hr />
<table class="table table-striped">
  <thead>
    <tr>
      <th>Edit</th>
      <th>ID</th>
      <th>Titolo</th>
      <th>Completato</th>
    </tr>
  </thead>
  <tbody>
    <tr ng-repeat="item in ctrl.items">
      <td>
        <a class="btn" ng-href="#/todo/edit/{{item.id}}">
          <span class="glyphicon glyphicon-pencil"></span> Edit
          </button>
      </td>
      <td>
        <!-- posso usare gli attributi che iniziano con il prefisso 'ng-' -->
        <span ng-bind="item.id"></span>
      </td>
      <td ng-class="{completed: item.completed}">
        <!-- oppure per 'data-ng-' per avere compatibilità con i validatori html -->
        <span ng-bind="item.title"></span>
      </td>
      <td>
        <button type="button" ng-click="ctrl.toggle(item)">
          <i class="glyphicon glyphicon-ok-sign text-danger" ng-show="item.completed"></i>
          <i class="glyphicon glyphicon-minus text-success" ng-hide="item.completed"></i>
        </button>
      </td>
    </tr>
  </tbody>
</table>
Da notare che non c'è nessun attributo ng-controller con la definizione del controllers e dell'alias (ctrl) in quanto entranbi sono stati definiti nel file app.js.
In questo caso viene utilizzato l'attributo ng-repeat per ciclare su tutti gli items e ng-click per gestire l'evento click. Sia l'array items che gli eventi click sono definiti nel controller todo-list-controllers.js:
(function() {
  "use strict";

  angular.module("sgartApp").controller("TodoListCtrl", localCtrl);  // per leggibilità non dichiaro direttamente la funione ma solo il nome

  // devo iniettare il service todoFactory che esegue l'accesso allo storage/DB
  localCtrl.$inject = ["$scope", "todoFactory"];

  function localCtrl($scope, todoFactory) {
    var self = this;

    self.items = [{
      id: 0,
      title: "",
      completed: false
    }];  // gli items 'todo' da visualizzare

    // l'evento agganciato al click
    self.toggle = function(item) {
      // il factory che segue il cambio di stato su DB
      todoFactory.toggle(item.id).then(
        function(data) { //success
          item.completed = data;
        },
        function(data) { //error
          alert('Error ' + data);
        }
      );
    };

    function load() {
      // il factory che ritorna tutti gli items dal DB
      todoFactory.getAll().then(
        function(data) { //success
          self.items = data;
        },
        function(data) { //error
          alert('Error ' + data);
        }
      );
    };
    // creo sempre una funzione "init" dove inizializzare i valori
    function init() {
      load();
    }
    init();	// inizializzo il controller
  }

})();
allo stesso modo vanno definite le view delle pagine todo-new.html (new) e todo-edit.html (edit) con il relativo controller todo-edit-controller,js.

L'ultima cosa che manca è il factory per l'interfacciamento con il DB todo-factory.js:
(function() {
  "use strict";

  // creo un factory per l'accesso ai dati in modo da astrarlo all'applicazione
  // espongo solo delle interfacce di comunicazioni che verranno usate dalle app
  angular.module('sgartApp').factory('todoFactory', factory);

  // gli inietto il modulo http per fare chiamate ajax
  // in questo esempio non viene usato
  // inietto $q solo per simulare una richiesta http asincrona
  factory.$inject = ['$http', '$q'];

  function factory($http, $q) {
    var dbApiBase = "/api";	//path base delle api 

    // esempio di richiamo di un API rest che ritorna un JSON
    function getAllInt() {
      return $http.get(dbApiBase + '/getAll', {
        params: {
          t: new Date().getTime()	//per evitare il caching su IE
        }
        , cache: false
      }).then(function(data){
        return data;
      }, function(data){
        alert('error');
      });
    };
    ... altri metodi ...

    // indico quali funzioni il factory espone
    var factory = {
      getAll: getAllInt,
      get: getInt,
      update: updateInt,
      delete: deleteInt,
      toggle: toggleInt
    };

    return factory;
  }
})();
In un prossimo post riporterò il factory completo con un esempio di API in c# MVC.

L'esempio completo può essere scaricato da qui.
Per funzionare deve essere esguito in un web server non direttamente da file system

Vedia anche AngularJS come funziona