AngularJS Single Page Application
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:
La prima cosa da fare è impostare una struttura delle directory dell'applicazione, questo è il mio modello:
e questi i file che compongono l'applicazione:
Poi vanno scaricate le librerie comuni, Bootstrap 3 (solo CSS font) e AngularJS 1.4 con il modulo route.
Fatto questo si può impostare la pagina principale, index.html, che ha la stessa funzione delle master page in ASP.NET:
dove vengono caricati i CSS necessari, le librerie AngularJS 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:
Il secondo passo è creare il file app.js per inizializzare l'applicazione e definire le url gestite:
dopo questo si possono definire le singole pagine todo-list.html:
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:
In un prossimo post riporterò il factory completo con un esempio di API in C# MVC.
L'esempio completo è questo sgart-angular4.zip.
Vedia anche AngularJS come funziona
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
Vedi esempio scaricabile a fondo pagina.
La prima cosa da fare è impostare una struttura delle directory dell'applicazione, questo è il mio modello:
Text
/
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
Text
/
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
Fatto questo si può impostare la pagina principale, index.html, che ha la stessa funzione delle master page in ASP.NET:
HTML
<!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>
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:
HTML
<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:
JavaScript
(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
};
})();
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 entrambi 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:JavaScript
(function() {
"use strict";
angular.module("sgartApp").controller("TodoListCtrl", localCtrl); // per leggibilità non dichiaro direttamente la funzione 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
}
})();
L'ultima cosa che manca è il factory per l'interfacciamento con il DB todo-factory.js:
C#
(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;
}
})();
L'esempio completo è questo sgart-angular4.zip.
Per funzionare deve essere esguito in un web server non direttamente da file system
Vedia anche AngularJS come funziona