3. Il client Angular JS
3.1. Riferimenti per il framework Angular JS
All'inizio di questo documento sono stati forniti due riferimenti per il framework Angular JS. Li elenchiamo nuovamente qui:
- [rif1]: il libro "Pro AngularJS" scritto da Adam Freeman e pubblicato da Apress. Si tratta di un libro eccellente. Il codice sorgente degli esempi contenuti nel libro è disponibile gratuitamente all'indirizzo [http://www.apress.com/downloadable/download/sample/sample_id/1527/];
- [rif2]: la documentazione ufficiale di Angular JS [https://docs.angularjs.org/guide];
AngularJS merita un libro tutto suo. Il libro di Adam Freeman supera le 600 pagine e non ce n'è una sola sprecata. Descriveremo un'applicazione Angular e, nel corso di questa descrizione, discuteremo i fondamenti di questo framework. Tuttavia, ci limiteremo solo alle spiegazioni necessarie per comprendere la soluzione proposta. Angular è un framework estremamente ricco e ci sono molti modi per ottenere lo stesso risultato. Questo può essere impegnativo perché, quando si è agli inizi, non si sa se si sta utilizzando una soluzione migliore o peggiore di un'altra. Questo è il caso della soluzione qui presentata. Potrebbe essere scritta in modo diverso e forse utilizzando pratiche migliori.
3.2. Architettura client di Angular
L'architettura client di Angular assomiglia a quella di una classica applicazione web MVC con alcune differenze. Un'applicazione web Spring MVC, ad esempio, ha la seguente architettura:
![]() |
L'elaborazione di una richiesta client procede come segue:
- richiesta - gli URL richiesti hanno il formato http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... Il [Dispatcher Servlet] è la classe Spring che gestisce gli URL in entrata. Esso "instradano" l'URL all'azione che dovrebbe gestirlo. Queste azioni sono metodi di classi specifiche chiamate [Controller]. La C in MVC in questo caso è la catena [Dispatcher Servlet, Controller, Action]. Se non è stata configurata alcuna azione per gestire l'URL in entrata, il [Dispatcher Servlet] risponderà che l'URL richiesto non è stato trovato (errore 404 NOT FOUND);
- l'elaborazione
- l'azione selezionata può utilizzare i parametri che il [Dispatcher Servlet] le ha passato. Questi possono provenire da diverse fonti:
- il percorso [/param1/param2/...] dell'URL,
- i parametri dell'URL [p1=v1&p2=v2],
- dai parametri inviati dal browser con la sua richiesta;
- durante l'elaborazione della richiesta dell'utente, l'azione potrebbe aver bisogno del livello [business] [2b]. Una volta elaborata la richiesta del client, questa può innescare varie risposte. Un esempio classico è:
- una pagina di errore se la richiesta non è stata elaborata correttamente
- una pagina di conferma in caso contrario
- l'azione indica di visualizzare una vista specifica [3]. Questa vista mostrerà i dati noti come modello di vista. Questa è la M in MVC. L'azione creerà questo modello M [2c] e indicherà di visualizzare una vista V [3];
- risposta - la vista V selezionata utilizza il modello M costruito dall'azione per inizializzare le parti dinamiche della risposta HTML che deve inviare al client, quindi invia questa risposta.
L'architettura del nostro client Angular sarà simile, con una terminologia leggermente diversa. Innanzitutto, le applicazioni Angular sono generalmente applicazioni web a pagina singola (SPA):

- l'utente richiede l'URL iniziale dell'applicazione nel formato: http://machine:port/contexte. Il browser interroga un server web per recuperare il documento richiesto. Si tratta di una pagina HTML stilizzata con CSS e resa dinamica da JavaScript;
- quindi l'utente interagisce con le viste che gli vengono presentate. Possiamo distinguere vari tipi di interazioni:
- quelle che non richiedono alcuna interazione con fonti esterne, come nascondere o mostrare elementi della vista. Queste sono gestite da JavaScript incorporato;
- quelle che richiedono dati da un servizio web remoto. Questi dati saranno recuperati tramite una richiesta AJAX (Asynchronous JavaScript and XML), verrà costruito un modello e verrà visualizzata una vista;
- quelle che richiedono una vista diversa da quella iniziale. Verrà richiesta tramite una chiamata AJAX al server che ha servito la pagina iniziale. Quindi il processo precedente si ripeterà. La pagina risultante verrà memorizzata nella cache del browser. Alla richiesta successiva, non verrà recuperata dal server HTML remoto;
In definitiva, il browser effettua una sola richiesta HTTP: quella che recupera la pagina iniziale. Le successive richieste HTTP al server della pagina HTML o ai servizi web remoti vengono effettuate dal codice JavaScript incorporato nelle pagine.
Presenteremo ora l'architettura dell'applicazione all'interno del browser. Tralasceremo il server HTML che fornisce le pagine HTML dell'applicazione. Ai fini della spiegazione, possiamo supporre che siano tutte presenti nella cache del browser.
![]() |
Innanzitutto, dobbiamo contestualizzare questa architettura:
- in [1], ci troviamo in un browser;
- in [2], un utente interagisce con le viste visualizzate dal browser;
- in [3], i dati vengono recuperati dalla rete, spesso da servizi web;
L'utente interagisce con le viste: compila i moduli e li invia. Spieghiamo questo processo utilizzando la vista V1 sopra riportata. Supponiamo che questa sia la vista iniziale dell'applicazione. È stata ottenuta come segue:
- l'utente richiede l'URL iniziale dell'applicazione nel formato: http://machine:port/contexte;
- il browser ha richiesto il documento associato a questo URL. Ha ricevuto la pagina HTML/CSS/JS per la vista V1;
- il JavaScript incorporato nella pagina ha quindi preso il controllo e lo ha passato al controller C1 [5];
- il controller ha costruito il modello M1 [8] [9] per la vista V1. La costruzione di questo modello potrebbe aver richiesto l'uso di servizi interni [6] e l'interrogazione di servizi esterni [7];
L'utente ora ha davanti a sé una vista V1. Immaginiamo che si tratti di un modulo. Lo compila e poi lo invia:
- in [4], l'utente invia il modulo;
- in [5], questo evento verrà gestito da uno dei metodi del controller C1;
Se l'evento comporta solo una semplice modifica alla vista V1 (nascondere/mostrare campi), il controller C1 modificherà il modello M1 della vista V1 e poi visualizzerà nuovamente la vista V1. Per farlo, potrebbe aver bisogno di uno dei servizi del livello [services] [6].
Se l'evento richiede dati esterni:
- in [6], il controller C1 richiederà al livello [DAO] di recuperarli;
- in [7], il livello [DAO] effettuerà una o più chiamate AJAX per recuperarli;
- in [8] e [9], il modello M1 verrà modificato e verrà visualizzata la vista V1;
Se l'evento innesca un cambiamento di vista, in entrambi i casi precedenti, invece di visualizzare la vista V1, il controller C1 richiederà un nuovo URL [10]. Si tratta di un URL interno al browser. Non comporta immediatamente una richiesta HTTP al server della pagina HTML. Questa modifica dell'URL viene gestita da un router configurato in modo tale che ogni URL interno corrisponda a una vista V e al suo controller C. Il router visualizza quindi la nuova vista Vn. Prima della visualizzazione, il suo controller Cn prende il controllo, costruisce il modello Mn e quindi visualizza la vista Vn [11]. Se la pagina HTML per la vista Vn non è memorizzata nella cache del browser, verrà richiesta al server delle pagine HTML.
Il livello [Presentazione] di questa architettura è simile all'architettura JSF (Java Server Faces):
- la vista V corrisponde alla vista JSF Facelet;
- il controller C corrisponde al bean JSF, una classe Java che contiene sia il modello M della vista V che i suoi gestori di eventi;
Il livello [Servizi] differisce dai livelli [Servizi] a cui siamo abituati. Nello sviluppo web lato server, molto spesso abbiamo la seguente architettura a livelli:
![]() |
Nel diagramma sopra, il livello [web] comunica con il livello [DAO] solo attraverso il livello [business]. Nulla ci impedirebbe di inserire un riferimento al livello [DAO] nel livello [web] per abilitare questa comunicazione. Tuttavia, evitiamo di farlo.
Con Angular, non ci poniamo limiti. L'architettura diventa quindi la seguente:
![]() |
- in [1], il livello [di presentazione] può comunicare direttamente con qualsiasi servizio;
- in [2], i servizi sono a conoscenza l'uno dell'altro. Un servizio può utilizzare uno o più altri servizi.
3.3. Le viste client di Angular
Le viste client di Angular sono già state presentate nella Sezione 1.3.3. Per rendere questo nuovo capitolo più facile da seguire, le ripetiamo qui. La prima vista è la seguente:
![]() |
- [6], la pagina di accesso all'applicazione. Si tratta di un'applicazione per la prenotazione di appuntamenti medici;
- in [7], una casella di controllo che consente all'utente di abilitare o disabilitare la modalità [debug]. Questa modalità è indicata dalla presenza del pannello [8], che visualizza il modello della vista corrente;
- in [9], un tempo di attesa artificiale in millisecondi. Il valore predefinito è 0 (nessuna attesa). Se N è il valore di questo tempo di attesa, qualsiasi azione dell'utente verrà eseguita dopo un tempo di attesa di N millisecondi. Ciò consente di vedere la gestione dell'attesa implementata dall'applicazione;
- in [10], l'URL del server Spring 4. In base a quanto detto prima, è [http://localhost:8080];
- in [11] e [12], il nome utente e la password dell'utente che desidera utilizzare l'applicazione. Ci sono due utenti: admin/admin (login/password) con un ruolo (ADMIN) e user/user con un ruolo (USER). Solo il ruolo ADMIN ha il permesso di utilizzare l'applicazione. Il ruolo USER è incluso esclusivamente per dimostrare la risposta del server in questo caso d'uso;
- in [13], il pulsante che consente di connettersi al server;
- in [14], la lingua dell'applicazione. Ce ne sono due: francese (predefinita) e inglese.
![]() |
- in [1], si effettua l'accesso;
![]() |
- una volta effettuato l'accesso, puoi scegliere il medico con cui desideri fissare un appuntamento [2] e la data dell'appuntamento [3];
- In [4], richiedi di visualizzare l'orario del medico selezionato per il giorno scelto;
![]() |
- Una volta visualizzato l'orario del medico, puoi prenotare una fascia oraria [5];
![]() |
- In [6], selezionare il paziente per l'appuntamento e confermare la selezione in [7];
![]() |
Una volta confermato l'appuntamento, si torna automaticamente al calendario, dove il nuovo appuntamento è ora elencato. Questo appuntamento può essere cancellato in un secondo momento [7].
Le funzionalità principali sono state descritte. Sono semplici. Quelle non descritte sono funzioni di navigazione per tornare a una vista precedente. Concludiamo con le impostazioni della lingua:
![]() |
- in [1], si passa dal francese all'inglese;

- in [2], la visualizzazione passa all'inglese, compreso il calendario;
3.4. Configurazione del progetto Angular
Costruiremo il nostro client Angular passo dopo passo. Stiamo utilizzando l'IDE WebStorm.
Creiamo una cartella vuota [rdvmedecins-angular-v1] e poi apriamola con WebStorm:
![]() |
- in [1], apriamo una cartella;
- In [2], selezioniamo la cartella che abbiamo creato;
- in [3], otteniamo un progetto WebStorm vuoto;
![]() |
- in [4], configuriamo il progetto tramite l'opzione [File / Impostazioni];
- in [5] e [6], configuriamo la proprietà [Spelling], che gestisce il controllo ortografico. Per impostazione predefinita, questa funzione è abilitata. Poiché il software scaricato è in inglese, i nostri commenti in francese nei programmi verranno sottolineati come potenziali errori ortografici. Disabilitiamo quindi questo controllo ortografico [7];
![]() |
- In [8], creare un nuovo file;
- In [9], scegliamo di creare il file [package.json], che descrive l'applicazione utilizzando la sintassi JSON;
- in [10], il file generato viene modificato come mostrato in [11];
- in [12], salvare questo file sia come [package.json] che come [bower.json];
![]() |
- In [13], riconfigurare il progetto;
![]() |
- in [14], configurare la proprietà [Javascript / Bower], che ci consentirà di dichiarare le librerie JavaScript di cui abbiamo bisogno;
- in [15], specificare il file [bower.json] appena creato;
![]() |
- in [16], aggiungere una libreria JavaScript;
- in [17], vengono visualizzate tutte le librerie JavaScript scaricabili;
- In [18], possiamo inserire un termine per filtrare l'elenco [17]. Qui specifichiamo che vogliamo la libreria [Angular JS];
- in [19], compaiono i dettagli della libreria. Qui vediamo che verrà scaricata la versione 1.2.18 di Angular;
- in [20], la scarichiamo;
![]() |
- in [21], vediamo che è stata scaricata;
- in [22], vediamo la versione scaricata. In realtà è la 1.2.19;
- in [23], vediamo l'ultima versione disponibile;
![]() |
- in [24], seguendo la stessa procedura di prima, scarichiamo le seguenti librerie:
per codificare la stringa "user:password" in Base64; | ||
per internazionalizzare il calendario | ||
per indirizzare gli URL interni dell'applicazione al controller e alla vista corretti; | ||
consente l'internazionalizzazione delle viste. Si tratta di un progetto indipendente da Angular. In questo caso, verranno utilizzate due lingue: il francese e l'inglese; | ||
fornisce componenti visivi compatibili con Bootstrap. Qui useremo il suo calendario; | ||
il framework CSS Bootstrap. Verrà utilizzato per creare le viste; | ||
fornisce un componente visivo di tipo "tabella". È "responsive" nel senso che si adatta alle dimensioni dello schermo; | ||
fornisce un componente "elenco a discesa"; |
![]() |
- In [25], le librerie scaricate sono state installate nella cartella [bower_components];
- in [26], vediamo che la libreria jQuery è stata scaricata. Questo perché Bootstrap la utilizza. Il sistema di installazione delle dipendenze JavaScript in un progetto è analogo a Maven nel mondo Java: se una libreria scaricata ha delle dipendenze proprie, tali dipendenze vengono scaricate automaticamente;
Il file [bower.json] è cambiato:
Tutte le dipendenze scaricate sono state elencate nel file.
3.5. La pagina iniziale del client Angular
Creiamo una versione iniziale della home page del client Angular:
![]() |
- nei punti [1] e [2], creiamo un file HTML denominato [app-01] [3] e [4];
Il file [app-01.html] fungerà da pagina principale per il momento. Configureremo l'importazione dei file CSS e JS richiesti dall'applicazione:
<!DOCTYPE html>
<html>
<head>
<title>RdvMedecins</title>
<!-- META -->
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Angular client for RdvMedecins">
<meta name="author" content="Serge Tahé">
<!-- on CSS -->
<link href="bower_components/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" />
<link href="bower_components/bootstrap/dist/css/bootstrap-theme.min.css" rel="stylesheet"/>
<link href="bower_components/bootstrap-select/bootstrap-select.min.css" rel="stylesheet"/>
<link href="bower_components/footable/css/footable.core.min.css" rel="stylesheet"/>
</head>
<body>
<div class="container">
<h1>Rdvmedecins - v1</h1>
</div>
<!-- Bootstrap core JavaScript ================================================== -->
<script type="text/javascript" src="bower_components/jquery/dist/jquery.min.js"></script>
<script type="text/javascript" src="bower_components/bootstrap/dist/js/bootstrap.min.js"></script>
<script type="text/javascript" src="bower_components/bootstrap-select/bootstrap-select.min.js"></script>
<script type="text/javascript" src="bower_components/footable/dist/footable.min.js"></script>
<!-- angular js -->
<script type="text/javascript" src="bower_components/angular/angular.min.js"></script>
<script type="text/javascript" src="bower_components/angular-ui-bootstrap-bower/ui-bootstrap-tpls.min.js"></script>
<script type="text/javascript" src="bower_components/angular-route/angular-route.min.js"></script>
<script type="text/javascript" src="bower_components/angular-translate/angular-translate.min.js"></script>
<script type="text/javascript" src="bower_components/angular-base64/angular-base64.min.js"></script>
</body>
</html>
- righe 11-12: i file CSS per Bootstrap;
- riga 13: il file CSS per il componente [boostrap-select];
- riga 14: il file CSS per il componente [footable];
- righe 21-24: i file JS per i componenti Bootstrap;
- riga 21: i componenti Bootstrap sono basati su jQuery;
- riga 22: il file JS di Bootstrap;
- riga 23: il file JS per il componente [boostrap-select];
- riga 24: il file JS per il componente [footable];
- righe 26–30: i file JS per Angular e i progetti correlati;
- riga 26: il file JS di Angular. Deve essere caricato dopo jQuery se si utilizza quella libreria;
- riga 27: il file JS per il progetto [angular-ui-bootstrap];
- riga 28: il file JS per il router [angular-route];
- riga 29: il file JS per il modulo di internazionalizzazione dell'applicazione Angular;
- riga 30: il file JS per il modulo [angular-base64];
È possibile verificare la validità del file [app-01.html]:
![]() |
- in [1], richiediamo un'ispezione del codice;
- in [2], il risultato quando tutto è corretto;
Si raccomanda questa ispezione sistematica del codice prima dell'esecuzione. In questo caso, tale controllo consente di individuare eventuali errori nei riferimenti ai file CSS e JS. Se un percorso è errato, l'ispettore del codice lo segnalerà.
- In [3], la pagina può essere caricata in un browser tramite un debugger. Nel browser viene visualizzato il seguente risultato:
![]() |
- in [4], la pagina [app-01.html] è stata servita da un server WebStorm interno in esecuzione qui sulla porta 63342;
- In [5], la console del debugger. Se si fossero verificati degli errori, sarebbero apparsi qui. Qui viene visualizzato anche l'output generato dall'istruzione JavaScript [console.log(espressione)]. Faremo ampio uso di questa funzionalità;
La modalità debug consente di modificare la pagina in WebStorm e vedere i risultati di tali modifiche nel browser senza dover ricaricare la pagina. Quindi, se aggiungiamo la riga 3 qui sotto:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<h2>Version 1</h2>
</div>
e quando torniamo al browser, vediamo che la pagina è cambiata:
![]() |
3.6. Introduzione a Bootstrap
Illustreremo ora alcune delle funzionalità di Bootstrap utilizzate nell'applicazione. Ho una conoscenza limitata di questo framework, acquisita copiando e incollando codice trovato su Internet. Spiegherò il ruolo delle classi CSS che credo di comprendere. Mi asterrò dal commentare le altre.
3.6.1. Esempio 1
In Angular, le operazioni che recuperano informazioni da fonti esterne sono asincrone. Ciò significa che l'operazione viene avviata e il controllo torna immediatamente alla vista, consentendo all'utente di continuare a interagire con essa. L'applicazione viene informata del completamento dell'operazione tramite un evento. Questo evento viene gestito da una funzione JavaScript che può quindi aggiornare o modificare la vista corrente. Se l'operazione richiede molto tempo, è utile offrire all'utente la possibilità di annullarla. Offriremo questa opzione in modo sistematico. Per farlo, useremo un banner Bootstrap:

Per ottenere questo risultato, duplichiamo [app-01.html] in [app-02.html] e modifichiamo le seguenti righe:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<div class="alert alert-warning">
<h1>Opération en cours. Veuillez patienter...
<button class="btn btn-primary pull-right">Annuler</button>
<img src="assets/images/waiting.gif" alt=""/>
</h1>
</div>
</div>
- riga 1: la classe CSS [container] definisce un'area di visualizzazione all'interno del browser;
- riga 3: la classe CSS [alert] visualizza un'area colorata. La classe [alert-warning] utilizza un colore predefinito;
- riga 5: la classe [btn] definisce lo stile di un pulsante. La classe [btn-primary] gli assegna un colore specifico. La classe [pull-right] lo posiziona a destra del banner di avviso;
- riga 6: un'immagine di caricamento animata;
3.6.2. Esempio 2
Le diverse viste dell'applicazione avranno un titolo comune:

Per ottenere questo risultato, duplichiamo [app-01.html] in [app-03.html] e modifichiamo le seguenti righe:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- Bootstrap Jumbotron -->
<div class="jumbotron">
<div class="row">
<div class="col-md-2">
<img src="assets/images/caduceus.jpg" alt="RvMedecins"/>
</div>
<div class="col-md-10">
<h1>Les Médecins associés</h1>
</div>
</div>
</div>
</div>
- L'area colorata viene creata utilizzando la classe [jumbotron] nella riga 4;
- Riga 5: la classe [row] definisce una riga con 12 colonne;
- riga 6: la classe [col-md-2] definisce un'area a due colonne all'interno della riga;
- riga 7: un'immagine viene inserita in queste due colonne;
- Righe 9–11: il testo viene inserito nelle restanti 10 colonne;
3.6.3. Esempio 3
Le viste avranno una barra di controllo superiore. Conterrà opzioni di controllo, collegamenti o pulsanti. Conterrà anche elementi di modulo. Ad esempio:
![]() |
Per ottenere questo risultato, duplichiamo [app-01.html] in [app-04.html] e modifichiamo le seguenti righe:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<div class="navbar-collapse collapse">
<form class="navbar-form navbar-right">
<!-- debug mode -->
<label style="width: 100px">
<input type="checkbox">
<span style="color: white">Debug</span>
</label>
<!-- identification form -->
<div class="form-group">
<input type="text" class="form-control" placeholder="Temps d'attente"
style="width: 150px"/>
<input type="text" class="form-control" placeholder="URL du service web"
style="width: 200px"/>
<input type="text" class="form-control" placeholder="Login"
style="width: 100px"/>
<input type="password" class="form-control" placeholder="Mot de passe"
style="width: 100px"/>
</div>
<button class="btn btn-success">
Connexion
</button>
</form>
</div>
<button class="btn btn-success">
Connexion
</button>
</form>
</div>
</div>
</div>
</div>
- riga 4: la classe [navbar] definisce lo stile della barra di navigazione. La classe [navbar-inverse] le assegna uno sfondo nero. La classe [navbar-fixed-top] garantisce che, quando si scorre la pagina visualizzata dal browser, la barra di navigazione rimanga nella parte superiore dello schermo;
- Righe 6–14: definiscono l'area [1]. Si tratta in genere di una serie di classi che non capisco. Utilizzo il componente così com'è;
- riga 15: definisce un'area "responsive" della barra di navigazione. Su uno smartphone, quest'area si riduce a un'area menu;
- riga 16: la classe [navbar-form] racchiude un modulo nella barra dei comandi. La classe [navbar-right] lo posiziona a destra del modulo;
- righe 23–32: i quattro campi di input del modulo della riga 17 [3]. Si trovano all'interno di una classe [form-group] che racchiude gli elementi di un modulo, e ciascuno di essi ha la classe [form-control];
- riga 33: la classe [btn] che abbiamo già visto, arricchita dalla classe [btn-success], che le conferisce il colore verde;
3.6.4. Esempio 4
La barra di controllo ti permetterà di cambiare la lingua utilizzando un elenco a discesa:

Per ottenere questo risultato, duplichiamo [app-01.html] in [app-05.html] e aggiungiamo le seguenti righe alla barra di controllo:
<button class="btn btn-success">
Connexion
</button>
<!-- languages -->
<div class="btn-group">
<button type="button" class="btn btn-danger">
Langues
</button>
<button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu" role="menu">
<li>
<a href="">Français</a>
</li>
<li>
<a href="">English</a>
</li>
</ul>
</div>
</form>
Le righe aggiunte sono le righe da 4 a 21.
- Riga 5: la classe [btn-group] racchiude un gruppo di pulsanti. Ce ne sono due alle righe 6 e 9;
- Righe 6–8: il primo pulsante definisce l'etichetta per l'elenco a discesa. La classe [btn-danger] gli conferisce un colore rosso;
- righe 9–12: il secondo pulsante è il pulsante dell'elenco a discesa. È posizionato accanto al primo, dando l'impressione di un unico componente;
- riga 10: visualizza la freccia verso il basso che indica che il pulsante è un elenco a discesa;
- riga 11: per gli screen reader;
- righe 13–20: le voci dell'elenco a discesa sono gli elementi di un elenco non ordinato;
3.6.5. Esempio 5
Per inviare un modulo o navigare, l'utente avrà a disposizione opzioni o pulsanti nella barra di controllo, come mostrato di seguito:
![]() |
Le opzioni di menu sono state aggiunte in [1]. Per ottenere questo risultato, duplichiamo [app-01.html] in [app-06.html] e aggiungiamo le seguenti righe:
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
...
</div>
<!-- menu options -->
<div class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li class="active">
<a href="">
<span>Home</span>
</a>
</li>
<li class="active">
<a href="">
<span>Agenda</span>
</a>
</li>
<li class="active">
<a href="">
<span>Valider</span>
</a>
</li>
<li class="active">
<a href="">
<span>Annuler</span>
</a>
</li>
</ul>
<!-- right buttons -->
<form class="navbar-form navbar-right" role="form">
...
</form>
</div>
</div>
</div>
</div>
- Le opzioni del menu sono generate dalle righe 8–29. Anche in questo caso si tratta di elementi di un elenco <ul>. La classe [active] sottolinea il testo, indicando che l'opzione è cliccabile.
3.6.6. Esempio 6
Visualizzeremo medici e clienti in elenchi a discesa come mostrato di seguito:
![]() |
L'elenco a discesa utilizzato non è un componente nativo di Bootstrap. Si tratta del componente [bootstrap-select] (http://silviomoreto.github.io/bootstrap-select/). Per ottenere questo risultato, duplichiamo [app-01.html] in [app-07.html] e aggiungiamo le seguenti righe:
<!DOCTYPE html>
<html>
<head>
...
<link href="bower_components/bootstrap-select/bootstrap-select.min.css" rel="stylesheet"/>
</head>
<body>
<div class="container">
<h1>Rdvmedecins - v1</h1>
<h2><label for="medecins">Médecins</label></h2>
<select id="medecins" data-style="btn btn-primary" class="selectpicker">
<option value="1">Mme Marie PELISSIER</option>
<option value="1">Mr Jacques BROMARD</option>
<option value="1">Mr Philippe JANDOT</option>
<option value="1">Mme Justine JACQUEMOT</option>
</select>
</div>
<!-- Bootstrap core JavaScript ================================================== -->
...
<script type="text/javascript" src="bower_components/bootstrap-select/bootstrap-select.min.js"></script>
<!-- local script -->
<script>
$('.selectpicker').selectpicker();
</script>
</body>
</html>
- Riga 5: è necessario importare il foglio di stile [bootstrap-select];
- riga 13: l'attributo [data-style] è utilizzato da [bootstrap-select]. Serve a definire lo stile dell'elenco a discesa. Qui gli conferiamo l'aspetto di un pulsante blu [btn-primary];
- riga 13: l'attributo [class] viene utilizzato alla riga 23. Può essere qualsiasi cosa;
- Righe 14–17: gli elementi dell'elenco a discesa. Si tratta di tag HTML standard;
- riga 22: il JS [bootstrap-select] deve essere importato;
- righe 24–26: uno script JavaScript eseguito al termine del caricamento della pagina;
- riga 25: un'istruzione jQuery. Applichiamo il metodo [selectpicker] (selectpicker()) a tutti gli elementi con la classe [selectpicker] ($('.selectpicker')). Ce n'è solo uno: il tag <select> alla riga 13. Il metodo [selectpicker] proviene dal file JS a cui si fa riferimento alla riga 22;
3.6.7. Esempio 7
Per visualizzare l'orario di un medico, useremo una tabella reattiva fornita dalla libreria JS [footable]:
![]() |
- in [1]: la tabella con una visualizzazione normale;
- in [2]: la tabella quando la finestra del browser viene ridimensionata. La colonna [Azione] si sposta automaticamente alla riga successiva. Questo è chiamato componente "responsive" o semplicemente adattivo.
Duplichiamo [app-01.html] in [app-08.html] e aggiungiamo le seguenti righe:
...
<link href="bower_components/footable/css/footable.core.min.css" rel="stylesheet"/>
<link href="assets/css/rdvmedecins.css" rel="stylesheet"/>
...
<div class="container">
<h1>Rdvmedecins - v1</h1>
<div class="row alert alert-warning">
<div class="col-md-6">
<table id="creneaux" class="table">
<thead>
<tr>
<th data-toggle="true">
<span>Créneau horaire</span>
</th>
<th>
<span>Client</span>
</th>
<th data-hide="phone">
<span>Action</span>
</th>
</thead>
<tbody>
<tr>
<td>
<span class='status-metro status-active'>
9h00-9h20
</span>
</td>
<td>
<span></span>
</td>
<td>
<a href="" class="status-metro status-active">
Réserver
</a>
</td>
</tr>
<tr>
<td>
<span class='status-metro status-suspended'>
9h20-9h40
</span>
</td>
<td>
<span>Mme Paule MARTIN</span>
</td>
<td>
<a href="" class="status-metro status-suspended">
Supprimer
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
...
<script src="bower_components/footable/dist/footable.min.js" type="text/javascript"></script>
- Le righe 2 e 60 sono già presenti in [app-01.html]. Si tratta dei file CSS e JS forniti dalla libreria [footable];
- La riga 3 fa riferimento al seguente file CSS:
@CHARSET "UTF-8";
#creneaux th {
text-align: center;
}
#creneaux td {
text-align: center;
font-weight: bold;
}
.status-metro {
display: inline-block;
padding: 2px 5px;
color:#fff;
}
.status-metro.status-active {
background: #43c83c;
}
.status-metro.status-suspended {
background: #fa3031;
}
Gli stili [status-*] provengono da un esempio di utilizzo della tabella [footable] presente sul sito web della libreria.
- riga 8: inserisce la tabella in una riga [row] e in un riquadro di avviso colorato [alert alert-warning];
- riga 9: la tabella occuperà 6 colonne [col-md-6];
- riga 10: la tabella HTML è formattata da Bootstrap [class='table'];
- riga 13: l'attributo [data-toggle] specifica la colonna contenente il simbolo [+/-] che espande/comprime la riga;
- riga 19: l'attributo [data-hide='phone'] specifica che la colonna deve essere nascosta se lo schermo ha le dimensioni di uno schermo di telefono. È possibile utilizzare anche il valore 'tablet';
3.6.8. Esempio 8
Per aiutare l'utente, creeremo dei tooltip attorno ai componenti principali delle viste:
![]() |
Per farlo, duplichiamo [app-01.html] in [app-09.html] e aggiungiamo le seguenti righe:
<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
...
</head>
<body>
<div class="container">
<h1>Rdvmedecins - v1</h1>
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<!-- menu options -->
<div class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li class="active">
<a href="">
<span tooltip="Retourne à la page d'accueil" tooltip-placement="bottom">Home</span>
</a>
</li>
<li class="active">
<a href="">
<span tooltip="Affiche l'agenda" tooltip-placement="top">Agenda</span>
</a>
</li>
<li class="active">
<a href="">
<span tooltip="Valide le rendez-vous" tooltip-placement="right">Valider</span>
</a>
</li>
<li class="active">
<a href="">
<span tooltip="Annule l'opération en cours" tooltip-placement="left">Annuler</span>
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
<!-- Bootstrap core JavaScript ================================================== -->
<...
<script type="text/javascript" src="bower_components/angular-ui-bootstrap-bower/ui-bootstrap-tpls.min.js"></script>
<!-- local script -->
<script>
// --------------------- module Angular
angular.module("rdvmedecins", ['ui.bootstrap']);
</script>
</body>
</html>
I tooltip sono forniti dalla libreria [angular-ui-bootstrap], che a sua volta si basa sulla libreria [angular]. La riga 50 importa la libreria [angular-ui-bootstrap]. Per implementare i componenti della libreria [angular-ui-bootstrap], dobbiamo creare un modulo Angular. Questo viene fatto alle righe 52–55. Queste righe definiscono un modulo Angular denominato [rdvmedecins] (primo parametro). Un modulo Angular può utilizzare altri moduli Angular. Questi sono chiamati dipendenze del modulo. Sono forniti in un array come secondo parametro della funzione [angular.module]. Qui, il modulo denominato [ui.bootstrap] è fornito dalla libreria [angular-ui-bootstrap]. Questo modulo ci fornirà i tooltip.
La riga 54 definisce un modulo Angular. Per impostazione predefinita, questo non ha alcun effetto sulla pagina. Specifichiamo che la pagina deve essere gestita da Angular collegandola a un modulo Angular. Questo è ciò che viene fatto alla riga 2. L'attributo [ng-app='rdvmedecins'] collega la pagina al modulo creato alla riga 54. La pagina verrà quindi analizzata da Angular. Gli attributi [tooltip] saranno rilevati e gestiti dal modulo [ui.bootstrap].
La sintassi per il tooltip è la seguente:
<span tooltip="Retourne à la page d'accueil" tooltip-placement="bottom">Home</span>
Sopra, aggiungiamo un tooltip al testo [Home]:
- [tooltip]: definisce il testo del tooltip;
- [tooltip-placement]: definisce la sua posizione (in basso, in alto, a sinistra, a destra);
Angular JS consente di aggiungere nuovi tag o attributi a quelli già esistenti in HTML. Questa estensione dell'HTML si ottiene utilizzando le direttive Angular. In questo caso, gli attributi [tooltip] e [tooltip-placement] sono creati da [angular-ui-bootstrap].
3.6.9. Esempio 9
Per aiutare l'utente a scegliere la data di un appuntamento, forniremo un calendario:

Come per i tooltip, questo calendario è fornito dalla libreria [angular-ui-bootstrap]. Per ottenere questo risultato, duplichiamo [app-01.html] in [app-10.html] e aggiungiamo le seguenti righe:
<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
...
<body>
<div class="container">
<h1>Rdvmedecins - v1</h1>
<div>
<pre>Date <em>{{jour | date:'fullDate'}}</em></pre>
<div class="row">
<div class="col-md-2">
<h4>Calendrier</h4>
<div style="display:inline-block; min-height:290px;">
<datepicker ng-model="jour" show-weeks="true" class="well"></datepicker>
</div>
</div>
</div>
</div>
</div>
</div>
...
<!-- local script -->
<script>
// --------------------- module Angular
angular.module("rdvmedecins", ['ui.bootstrap'])
</script>
</body>
</html>
Come in precedenza, la pagina è associata a un modulo Angular (righe 2 e 28). Il calendario è definito dal tag <datepicker> alla riga 16, fornito dalla libreria [angular-ui-bootstrap]:
- [show-weeks='true']: per visualizzare i numeri delle settimane;
- [class='well']: per circondare il calendario con un riquadro grigio con angoli arrotondati;
- [ng-model='day']: gli attributi [ng-*] sono attributi Angular. L'attributo [ng-model] indica i dati che verranno inseriti nel view model. Quando l'utente clicca su una data, questa verrà inserita nella variabile [day] del modello. Questa variabile viene utilizzata alla riga 10. La sintassi {{expression}} valuta un'espressione composta da elementi del modello. In questo caso, {{day}} visualizzerà il valore della variabile [day] del modello. Una caratteristica fondamentale di Angular è che la vista si aggiorna automaticamente in risposta alle modifiche della variabile [day]. Pertanto, quando l'utente modifica le date, tali modifiche verranno immediatamente visualizzate alla riga 10. In generale, il processo funziona come segue:
- una vista V è associata a un modello M;
- Angular osserva il modello M e aggiorna automaticamente la vista V quando si verifica una modifica al modello M;
La sintassi {{day|date}} è chiamata filtro. Non è il valore di [day] che viene visualizzato, ma il valore di [day] filtrato attraverso un filtro chiamato [date]. Questo filtro è predefinito in Angular. Viene utilizzato per formattare le date. Accetta parametri che specificano il formato desiderato. Pertanto, l'espressione {{day | date:'fullDate'}} indica che vogliamo il formato della data completo, in questo caso [venerdì 20 giugno 2014], poiché il calendario è in inglese per impostazione predefinita. Tra poco parleremo della sua internazionalizzazione.
3.6.10. Conclusione
Abbiamo presentato gli elementi del framework CSS Bootstrap che useremo. Si trattava di componenti passivi: i loro eventi non venivano gestiti. Quindi cliccare sui pulsanti o sui link non faceva nulla. Questi eventi saranno gestiti in JavaScript. È possibile usare questo linguaggio senza framework, ma come nel caso del lato server, alcuni framework sono essenziali sul lato client. È il caso del framework AngularJS, che introduce un nuovo approccio allo sviluppo di applicazioni JavaScript eseguite da un browser. Lo presenteremo ora.
3.7. Introduzione ad Angular JS
Illustreremo ora alcune delle caratteristiche del framework Angular JS utilizzato nell'applicazione. Ne abbiamo già incontrate alcune:
- Una pagina HTML è basata su Angular JS se vi è associato un modulo:
<html ng-app="rdvmedecins">
- Angular consente di creare nuovi tag e attributi HTML utilizzando le direttive:
- Angular consente di creare filtri:
- Una vista V visualizza un modello M. Angular monitora il modello M e aggiorna automaticamente la vista V ogni volta che si verifica una modifica al modello M. Il valore di una variabile nel modello M viene visualizzato nella vista V utilizzando:
Inizieremo approfondendo l'implementazione del pattern di progettazione Model–View–Controller in Angular. Esaminiamo le relazioni tra di essi da una prospettiva architettonica:
![]() |
- La vista V1 visualizza il modello M1 costruito dal controller C1. Il controller C1 contiene non solo il modello M1, ma anche i gestori di eventi per la vista V1. Ci troviamo nei cicli 5, 8, 9:
- [5]: Si verifica un evento nella vista V1. Viene gestito dal controller C1;
- il controller esegue il proprio compito [6-7] e quindi costruisce il modello M1 [8];
- [9]: La vista V1 visualizza il nuovo modello M1. Come abbiamo accennato, questo passo finale è automatico. A differenza di altri framework MVC, non vi è alcun push esplicito (C1 inserisce il modello M1 in V1) né alcun pull esplicito (la vista V1 recupera il modello M1 da C1). Esiste un push implicito che lo sviluppatore non vede;
- quindi il ciclo 5, 8, 9 riprende;
3.7.1. Esempio 1: il modello MVC di Angular
Rivediamo l'esempio del calendario. Abbiamo visto la direttiva che lo genera:
<datepicker ng-model="jour" show-weeks="true" class="well"></datepicker>
Questa direttiva supporta altri attributi oltre a quelli mostrati sopra, incluso l'attributo [min-date], che imposta la data più vicina possibile che può essere selezionata nel calendario. Questo ci sarà utile. Quando l'utente seleziona una data per un appuntamento, questa deve essere uguale o successiva alla data corrente. Scriveremo quindi:
<datepicker ng-model="jour" ... min-date="dateMin"></datepicker>
dove [dateMin] sarà una variabile nel modello della pagina con un valore pari alla data odierna. Il risultato sarà la seguente pagina:
![]() |
- in [1], è il 19 giugno 2014. Il cursore indica che il 19 giugno può essere selezionato;
- in [2], il cursore indica che il 18 giugno non può essere selezionato;
Duplichiamo [app-10.html] in [app-11.html] e apportiamo le seguenti modifiche:
<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
...
</head>
<body ng-controller="rdvMedecinsCtrl">
<div class="container">
<h1>Rdvmedecins - v1</h1>
<div>
<pre>Date <em>{{jour | date:'fullDate' }}</em></pre>
<div class="row">
<div class="col-md-2">
<h4>Calendrier</h4>
<div style="display:inline-block; min-height:290px;">
<datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"></datepicker>
</div>
</div>
</div>
</div>
</div>
<!-- Bootstrap core JavaScript ================================================== -->
...
<!-- local script -->
<script>
// --------------------- module Angular
angular.module("rdvmedecins", ['ui.bootstrap']);
// contrôleur
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope',
function ($scope) {
// date minimale
$scope.minDate = new Date();
}]);
</script>
</body>
</html>
Esaminiamo innanzitutto lo script locale alle righe 26–37:
- riga 28: creazione del modulo [rdvmedecins] con la sua dipendenza dal modulo [ui.bootstrap], che fornisce il calendario;
- righe 30–35: creazione di un controller. Questo è ciò che conterrà il modello della nostra pagina. Qui non ci sarà alcun gestore di eventi;
- Righe 30–31: il controller [rdvMedecinsCtrl] appartiene al modulo [rdvmedecins]. È possibile aggiungere a un modulo tutti i controller che si desidera. Nella nostra applicazione avremo:
- un modulo di gestione dell'applicazione;
- un controller per ogni vista;
- il secondo parametro della funzione [controller] è un array della forma ['O1', 'O2', ..., 'On', function(O1, O2, ..., On)]. L'ultimo parametro è la funzione che implementa il controller. I suoi parametri sono oggetti che AngularJS fornirà alla funzione.
Torniamo all'architettura di un'applicazione Angular:
![]() |
Come mostrato sopra, il controller C1 contiene tutti i gestori di eventi per la vista V1 e il relativo modello M1. I gestori di eventi potrebbero richiedere uno o più servizi [6] per eseguire le loro attività. Passiamo tutti questi elementi come parametri alla funzione costruttore del controller:
I servizi Si sono singleton. Angular crea una singola istanza di ciascuno di essi. Sono identificati da un nome Si. Perché compaiono due volte nella tabella sopra? In produzione, gli script JS vengono minificati. Durante questo processo di minificazione, la tabella sopra diventa:
I parametri perdono i loro nomi. Tuttavia, questi sono i nomi dei servizi. È quindi importante preservare questi nomi. Questo è il motivo per cui vengono passati come stringhe come parametri che precedono la funzione. Le stringhe non vengono alterate durante il processo di minificazione. Quando Angular costruisce il controller utilizzando il nuovo array, sostituirà a1 con S1, a2 con S2 e così via. L'ordine dei parametri è quindi importante. Deve corrispondere all'ordine dei servizi che precedono la definizione della funzione.
Torniamo alla definizione del controller [rdvMedecinsCtrl]:
// controller
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope',
function ($scope) {
// minimum date
$scope.minDate = new Date();
}]);
- righe 3-4: l'unico oggetto iniettato nel controller è l'oggetto $scope. Si tratta di un oggetto predefinito che rappresenta il modello M delle viste associate al controller. Per arricchire il modello di una vista, è sufficiente aggiungere campi all'oggetto $scope;
- come avviene alla riga 6. Creiamo il campo [minDate] con la data corrente come valore;
La vista V utilizza questo modello M come segue:
<body ng-controller="rdvMedecinsCtrl">
<div class="container">
...
<div style="display:inline-block; min-height:290px;">
<datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"></datepicker>
</div>
...
</div>
...
- riga 1: il corpo della pagina è associato al controller [rdvMedecinsCtrl] tramite l'attributo [ng-controller]. Ciò significa che tutto ciò che si trova all'interno del tag <body> utilizzerà il controller [rdvMedecinsCtrl] per gestire i propri eventi e recuperare il proprio modello M. Una pagina HTML può dipendere da più controller, sia che siano annidati l'uno nell'altro o meno:
Sopra:
- il contenuto di [div1] (righe 1–10) visualizza il modello M1 gestito dal controller c1. I tag in quest'area possono fare riferimento ai gestori di eventi del controller c1;
- il contenuto di [div11] (righe 3-4) visualizza il modello M11 gestito dal controller c11 oltre al modello M1. Esiste un'ereditarietà dei modelli. I tag in quest'area possono fare riferimento sia ai gestori di eventi del controller c11 che a quelli del controller c1. Non possono fare riferimento né al modello M12 del controller c12 né ai suoi gestori di eventi. Il controller c12 non è definito tra le righe 3–5;
- righe 7–9: possiamo applicare un ragionamento simile a quello utilizzato in precedenza;
Torniamo al codice del calendario:
<datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"></datepicker>
L'attributo [min-date] viene inizializzato con il valore [minDate] del modello. Implicitamente, si tratta di [$scope.minDate]. Il campo viene sempre cercato nell'oggetto $scope.
3.7.2. Esempio 2: Localizzazione delle date
Per ora, il calendario non ci è molto utile poiché è in inglese. È possibile localizzarlo:
![]() |
- in [1] abbiamo un calendario in francese;
- in [2], lo impostiamo in inglese;
- in [3], il calendario in inglese;
Duplichiamo la pagina [app-11.html] in [app-12.html] e poi modifichiamo quest'ultima come segue:
<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
...
</head>
<body ng-controller="rdvMedecinsCtrl">
<div class="container">
<h1>Rdvmedecins - v1</h1>
<pre>Date <em>{{jour | date:'fullDate' }}</em></pre>
<div class="row">
<!-- the calendar-->
<div class="col-md-4">
<h4>Calendrier</h4>
<div style="display:inline-block; min-height:290px;">
<datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"></datepicker>
</div>
</div>
<!-- languages -->
<div class="col-md-2">
<div class="btn-group" dropdown is-open="isopen">
<button type="button" class="btn btn-primary dropdown-toggle" style="margin-top: 30px">
Langues<span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu">
<li><a href="" ng-click="setLang('fr')">Français</a></li>
<li><a href="" ng-click="setLang('en')">English</a></li>
</ul>
</div>
</div>
</div>
</div>
...
<script type="text/javascript" src="rdvmedecins.js"></script>
</body>
</html>
Ci sono poche modifiche. L'unica aggiunta riguarda le righe 21–31, che contengono l'elenco a discesa delle lingue. Per la prima volta, incontriamo un gestore di eventi alle righe 27–28:
- riga 27: l'attributo [ng-click] è un attributo Angular che specifica il gestore di eventi da eseguire quando si fa clic sull'elemento con questo attributo. Qui verrà eseguita la funzione [$scope.setLang('fr')]. Impostare il calendario in francese;
- riga 28: qui impostiamo il calendario in inglese;
- riga 35: poiché il codice JavaScript del controller è piuttosto consistente, lo inseriamo in un file denominato [rdvmedecins.js];
Angular gestisce la localizzazione della vista con un modulo chiamato [ngLocale]. La definizione del nostro modulo [rdvmedecins] sarà quindi la seguente:
// --------------------- Angular module
angular.module("rdvmedecins", ['ui.bootstrap', 'ngLocale']);
Riga 2: Non dimenticare le dipendenze, poiché i messaggi di errore di Angular a volte possono essere vaghi. L'omissione di una dipendenza è particolarmente difficile da individuare. Qui abbiamo una nuova dipendenza dal modulo [ngLocale].
Per impostazione predefinita, Angular gestisce solo la localizzazione di date, numeri, ecc., che hanno varianti locali. Non gestisce l'internazionalizzazione del testo. Per questo, useremo la libreria [angular-translate]. La localizzazione è gestita dalla libreria [angular-i18n]. Questa libreria include tanti file quanti sono le varianti per date, numeri, ecc.
![]() |
Per il calendario francese useremo il file [angular-locale_fr-fr.js], mentre per quello inglese useremo il file [angular-locale_en-us.js]. Diamo un'occhiata al contenuto del file [angular-locale_fr-fr.js], ad esempio:
'use strict';
angular.module("ngLocale", [], ["$provide", function($provide) {
var PLURAL_CATEGORY = {ZERO: "zero", ONE: "one", TWO: "two", FEW: "few", MANY: "many", OTHER: "other"};
$provide.value("$locale", {
"DATETIME_FORMATS": {
"AMPMS": [
"AM",
"PM"
],
"DAY": [
"dimanche",
"lundi",
"mardi",
"mercredi",
"jeudi",
"vendredi",
"samedi"
],
"MONTH": [
"janvier",
"f\u00e9vrier",
"mars",
"avril",
"mai",
"juin",
"juillet",
"ao\u00fbt",
"septembre",
"octobre",
"novembre",
"d\u00e9cembre"
],
"SHORTDAY": [
"dim.",
"lun.",
"mar.",
"mer.",
"jeu.",
"ven.",
"sam."
],
"SHORTMONTH": [
"janv.",
"f\u00e9vr.",
"mars",
"avr.",
"mai",
"juin",
"juil.",
"ao\u00fbt",
"sept.",
"oct.",
"nov.",
"d\u00e9c."
],
"fullDate": "EEEE d MMMM y",
"longDate": "d MMMM y",
"medium": "d MMM y HH:mm:ss",
"mediumDate": "d MMM y",
"mediumTime": "HH:mm:ss",
"short": "dd/MM/yy HH:mm",
"shortDate": "dd/MM/yy",
"shortTime": "HH:mm"
},
"NUMBER_FORMATS": {
"CURRENCY_SYM": "\u20ac",
"DECIMAL_SEP": ",",
"GROUP_SEP": "\u00a0",
"PATTERNS": [
{
"gSize": 3,
"lgSize": 3,
"macFrac": 0,
"maxFrac": 3,
"minFrac": 0,
"minInt": 1,
"negPre": "-",
"negSuf": "",
"posPre": "",
"posSuf": ""
},
{
"gSize": 3,
"lgSize": 3,
"macFrac": 0,
"maxFrac": 2,
"minFrac": 2,
"minInt": 1,
"negPre": "(",
"negSuf": "\u00a0\u00a4)",
"posPre": "",
"posSuf": "\u00a0\u00a4"
}
]
},
"id": "fr-fr",
"pluralCat": function (n) { if (n >= 0 && n <= 2 && n != 2) { return PLURAL_CATEGORY.ONE; } return PLURAL_CATEGORY.OTHER;}
});
}]);
Ecco gli elementi utilizzati per creare un calendario francese:
- righe 10–18: l'array dei giorni della settimana;
- righe 19–32: l'array dei mesi dell'anno;
- righe 33–41: la tabella abbreviata dei giorni della settimana;
- righe 42–55: la tabella dei mesi dell'anno abbreviati;
- righe 56–63: formati di data e ora. La riga 62 mostra il formato 'gg/mm/aa' utilizzato per le date in francese;
- righe 65–95: informazioni sulla formattazione dei numeri. Questo non è rilevante in questo contesto;
- riga 96: l'identificatore «fr-fr» per le impostazioni locali del file (fr-fr: francese della Francia, fr-ca: francese del Canada, ...)
Nel file [angular-locale_en-us.js], abbiamo esattamente la stessa cosa, ma questa volta per l'inglese degli Stati Uniti (en-us).
Il codice sopra riportato non è molto facile da leggere. Se lo leggete attentamente, vedrete che tutto questo codice definisce la variabile [$locale] alla riga 4. È modificando il valore di questa variabile che otteniamo l'internazionalizzazione di date, numeri, valuta, ecc. Curiosamente, Angular non consente di modificare la variabile [$locale] in fase di esecuzione. La si definisce una volta per tutte importando il file per la lingua desiderata:
<script type="text/javascript" src="bower_components/angular-i18n/angular-locale_fr-fr.js"></script>
Non ha senso importare tutti i file per le impostazioni locali desiderate, perché, come abbiamo visto, ogni file fa una sola cosa: definisce la variabile [$locale]. L'ultimo file importato ha la precedenza e non c'è modo di cambiare l'impostazione locale in seguito.
Cercando una soluzione a questo problema sul web, non sono riuscito a trovarne una. Ne propongo una qui [https://github.com/stahe/angular-ui-bootstrap-datepicker-with-locale-updated-on-the-fly]. L'idea è quella di inserire le diverse impostazioni locali di cui abbiamo bisogno in un dizionario. È da lì che le recupereremo quando avremo bisogno di cambiarle. Il codice JavaScript in [rdvmedecins.js] ha la seguente struttura:
![]() |
Se rimuoviamo le definizioni delle impostazioni locali, che occupano 200 righe (righe 15–215 sopra), il codice è semplice:
- riga 6: definisce il modulo [rdvmedecins] e le sue dipendenze;
- righe 8–10: definisce il controller [rdvMedecinsCtrl] della pagina;
- riga 9: il costruttore del controller accetta due parametri:
- $scope: per creare il modello di visualizzazione;
- $locale: è la variabile che gestisce le impostazioni locali del calendario. È questa che devi modificare quando cambi lingua;
- riga 13: la variabile del modello [minDate] viene inizializzata con la data odierna;
- riga 15: definisce il dizionario [locales]. Si noti che non abbiamo scritto [$scope.locales]. La variabile [locales] non fa parte del modello esposto alla vista;
- righe 15–215: definiscono un dizionario {'fr':locale-fr-fr, 'en':locale-en-us}. I valori [locale-fr-fr] e [locale-en-us] sono presi rispettivamente dai file JS [angular-locale_fr-fr.js] e [angular-locale_en-us.js]. La parte più difficile è non commettere errori con le numerose parentesi presenti in questo dizionario...
- riga 217: inizializziamo la variabile $locale con locales['fr'], ovvero la versione francese della locale. Non possiamo semplicemente scrivere [$locale=locales['fr']] perché ciò assegnerebbe l'indirizzo di locales['fr'] a $locale. Dobbiamo eseguire una copia del valore. Ciò può essere fatto utilizzando la funzione predefinita [angular.copy];
- riga 219: la variabile [day] nel modello viene inizializzata con la data odierna. Ciò garantisce che il calendario venga visualizzato con la data impostata su oggi;
- Righe 223–230: definiscono il gestore di eventi che viene chiamato quando la lingua cambia. Notare la sintassi:
per definire un gestore di eventi denominato [nome_funzione] che accetta i parametri [param1, param2, ...];
Rivediamo il codice HTML per l'elenco a discesa:
<!-- languages -->
<div class="col-md-2">
<div class="btn-group" dropdown is-open="isopen">
<button type="button" class="btn btn-primary dropdown-toggle" style="margin-top: 30px">
Langues<span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu">
<li><a href="" ng-click="setLang('fr')">Français</a></li>
<li><a href="" ng-click="setLang('en')">English</a></li>
</ul>
</div>
</div>
- riga 8: selezionando il francese si attiva la chiamata a [setLang('fr')];
- riga 9: selezionando l'inglese si attiva la chiamata a [setLang('en')];
- riga 3: l'attributo [is-open] è un valore booleano che controlla se l'elenco a discesa è aperto (true) o chiuso (false). Viene inizializzato con la variabile [isopen] dal modello di vista;
Torniamo al codice in [rdvmedecins.js]:
- riga 225: modifichiamo il valore della variabile [$locale] con il valore appropriato dal dizionario [locales];
- riga 227: abbiamo detto che quando il modello M di una vista V cambia, la vista V viene automaticamente aggiornata con il nuovo modello. Alla riga 225, abbiamo cambiato il valore della variabile [$locale], che non fa parte del modello M visualizzato dalla vista V. Dobbiamo trovare un modo per aggiornare questo modello M in modo che il calendario si aggiorni e utilizzi la nuova impostazione locale. Qui, modifichiamo la variabile [day] nel modello del calendario. La inizializziamo con un nuovo puntatore (new) che punta a una data identica a quella attualmente visualizzata. [$scope.day.getTime()] è il numero di millisecondi trascorsi tra il 1° gennaio 1970 e la data visualizzata dal calendario. Utilizzando questo numero, ricostruiamo una nuova data. Ovviamente, otterremo la stessa data e il calendario rimarrà posizionato sulla data che stava visualizzando. Ma il valore di [$scope.day], che in realtà è un puntatore, sarà cambiato e il calendario si aggiornerà;
- riga 229: impostiamo il valore della variabile [isopen] nel template su false. Questa variabile controlla uno degli attributi dell'elenco a discesa:
<div class="btn-group" dropdown is-open="isopen">
<button type="button" class="btn btn-primary dropdown-toggle" style="margin-top: 30px">
Langues<span class="caret"></span>
</button>
...
</div>
Nella riga 1 sopra, l'attributo [is-open] passerà a false, chiudendo così l'elenco a discesa.
3.7.3. Esempio 3: Internazionalizzazione del testo
Rivediamo la localizzazione del calendario:
![]() |
In [3], vediamo che il calendario è in inglese ma i testi [Calendar, Languages] non lo sono. Per impostazione predefinita, Angular non fornisce uno strumento per l'internazionalizzazione dei messaggi. Qui useremo la libreria [angular-translate] (https://github.com/angular-translate/angular-translate).
Svilupperemo il seguente esempio:
![]() |
- in [1], la versione in francese;
- in [2], la vista in inglese;
Diamo un'occhiata alla configurazione necessaria per l'internazionalizzazione. Lo script [rdvmedecins.js] viene modificato come segue:
// --------------------- Angular module
angular.module("rdvmedecins", ['ui.bootstrap', 'ngLocale', 'pascalprecht.translate']);
// configuration i18n
angular.module("rdvmedecins")
.config(['$translateProvider', function ($translateProvider) {
// messages français
$translateProvider.translations("fr", {
'msg_header': 'Cabinet Médical<br/>Les Médecins Associés',
'msg_langues': 'Langues',
'msg_agenda': 'Agenda de {{titre}} {{prenom}} {{nom}}<br/>le {{jour}}',
'msg_calendrier': 'Calendrier',
'msg_jour': 'Jour sélectionné : ',
'msg_meteo': "Aujourd'hui, il va pleuvoir..."
});
// messages anglais
$translateProvider.translations("en", {
'msg_header': 'The Associated Doctors',
'msg_langues': 'Languages',
'msg_agenda': "{{titre}} {{prenom}} {{nom}}'s Diary<br/> on {{jour}}",
'msg_calendrier': 'Calendar',
'msg_jour': 'Selected day: ',
'msg_meteo': 'Today, it will be raining...'
});
// langue par défaut
$translateProvider.preferredLanguage("fr");
}]);
- riga 2: la prima modifica consiste nell'aggiunta di una nuova dipendenza. L'internazionalizzazione dell'applicazione richiede il modulo Angular [pascalprecht.translate];
- righe 5–26: definiscono la funzione [config] del modulo [rdvmedecins]. All'avvio di un'applicazione Angular, il framework istanzia tutti i servizi richiesti dall'applicazione, inclusi i servizi predefiniti di Angular e quelli definiti dall'utente. Per ora non abbiamo definito alcun servizio. La funzione [config] del modulo di un'applicazione viene eseguita prima che qualsiasi servizio venga istanziato. Può essere utilizzata per definire le informazioni di configurazione per i servizi che verranno successivamente istanziati. Qui, la funzione [config] verrà utilizzata per definire i messaggi internazionalizzati dell'applicazione;
- riga 5: il parametro della funzione [config] è un array ['O1', 'O2', ..., 'On', function(O1, O2, ..., On)] dove Oi è un oggetto noto fornito da Angular. Qui, l'oggetto [$translateProvider] è fornito dal modulo [pascalprecht.translate]. [function] è la funzione eseguita per configurare l'applicazione;
- righe 7–14: la funzione [$translateProvider.translations] accetta due parametri:
- il primo parametro è la chiave di una lingua. Puoi usare quella che preferisci. In questo caso, abbiamo usato "fr" per le traduzioni in francese (riga 7) e "en" per quelle in inglese (riga 16),
- il secondo è l'elenco delle traduzioni sotto forma di un dizionario {'key1':'msg1', 'key2':'msg2', ...};
- righe 7–14: i messaggi in francese;
- righe 16–23: i messaggi in inglese;
- riga 25: il metodo [preferredLanguage] imposta la lingua predefinita. Il suo parametro è uno degli argomenti utilizzati come primo parametro della funzione [$translateProvider.translations], quindi qui è 'fr' (riga 7) o 'en' (riga 16);
- Si noti che esistono tre tipi di messaggi:
- messaggi senza parametri o elementi HTML (righe 9, 11, 12, ...),
- messaggi con elementi HTML (righe 8, 10, ...),
- messaggi con parametri (righe 10, 19);
Ora duplichiamo [app-11.html] in [app-12.html] e apportiamo le seguenti modifiche:
<div class="container">
<!-- a first text with HTML elements in it -->
<h3 class="alert alert-info" translate="{{'msg_header'}}"></h3>
<!-- a second text with parameters -->
<h3 class="alert alert-warning" translate="{{msg.text}}" translate-values="{{msg.model}}"></h3>
<!-- a third text translated by the controller -->
<h3 class="alert alert-danger">{{msg2}}</h3>
<pre>{{'msg_jour'|translate}}<em>{{jour | date:'fullDate' }}</em></pre>
<div class="row">
<!-- the calendar-->
<div class="col-md-4">
<h4>{{'msg_calendrier'|translate}}</h4>
<div style="display:inline-block; min-height:290px;">
<datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"></datepicker>
</div>
</div>
<!-- languages -->
<div class="col-md-2">
<div class="btn-group" dropdown is-open="isopen">
<button type="button" class="btn btn-primary dropdown-toggle" style="margin-top: 30px">
{{'msg_langues'|translate}}<span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu">
<li><a href="" ng-click="setLang('fr')">Français</a></li>
<li><a href="" ng-click="setLang('en')">English</a></li>
</ul>
</div>
</div>
</div>
</div>
- Le sostituzioni avvengono alle righe 3, 5, 9, 13 e 23;
- ci sono tre sintassi distinte:
- la sintassi [translate={{'msg_key'}}] (riga 3), dove [msg_key] è una delle chiavi presenti in un dizionario di traduzione. Questa sintassi è adatta per i messaggi con o senza elementi HTML, ma non per quelli con parametri;
- la sintassi [translate={{'msg_key'}} translate-values={{dictionary]}}] (riga 5), adatta per messaggi con o senza elementi HTML e con parametri;
- la sintassi [{{'msg_key'|translate}}] (righe 9, 13, 23) è adatta per messaggi senza parametri e senza elementi HTML;
Diamo un'occhiata ai diversi messaggi in questa vista:
Studio medico<br/>The Associated Doctors | The Associated Doctors | |
Calendario | Calendario | |
Lingue | Lingue | |
Giorno selezionato: | Giorno selezionato: |
Esaminiamo ora la riga 5:
<h3 class="alert alert-warning" translate="{{msg.text}}" translate-values="{{msg.model}}"></h3>
Si noti che [msg.text] e [msg.model] non sono racchiusi tra virgolette singole. Non si tratta di stringhe, ma di elementi del modello:
- msg.text: definisce la chiave del messaggio configurato da utilizzare;
- msg.model: è il dizionario che fornisce i valori dei parametri;
I nomi dei campi [text, model] possono essere qualsiasi cosa. Nel controller [rdvMedecinsCtrl] della vista, l'oggetto [msg] è definito come segue:

- riga 245: la definizione dell'oggetto [msg];
- riga 245: il campo [text] ha il valore [msg_agenda], che è associato a due valori:
- {{title}} Diario di {{first_name}} {{last_name}}<br/>il {{day}} nel dizionario francese;
- {{title}} Diario di {{first_name}} {{last_name}}<br/> il {{day}} nel dizionario inglese;
Il messaggio da visualizzare ha quindi quattro parametri [title, first_name, last_name, day];
- Riga 245: Il campo [model] è un dizionario che assegna valori a questi quattro parametri. C'è un problema con il parametro [day]. Vogliamo visualizzare il nome completo del giorno. Questo varia a seconda che la lingua sia il francese o l'inglese. Utilizziamo quindi il filtro [date], già utilizzato nella vista, nella forma {{ day | date:'fullDate'}}. Qualsiasi filtro può essere utilizzato nel codice JavaScript nella forma $filter('filter')(value, options), dove $filter è un oggetto Angular predefinito e 'filter' è il nome del filtro;
- righe 33–34: l'oggetto $filter predefinito viene passato come parametro al controller, consentendone l'utilizzo alla riga 245;
Torniamo a un'altra riga nella vista visualizzata:
<!-- un troisième texte traduit par le contrôleur -->
<h3 class="alert alert-danger">{{msg2}}</h3>
Tutte le traduzioni precedenti sono state eseguite nella vista utilizzando gli attributi del modulo [pascalprecht.translate]. Possiamo anche scegliere di eseguire questa traduzione sul lato server. È ciò che viene fatto qui. Nel controller (riga 247 nella schermata sopra), abbiamo il seguente codice:
$scope.msg2 = $filter('translate')('msg_meteo');
Utilizziamo la stessa sintassi del filtro 'date' poiché anche 'translate' è un filtro. Qui richiediamo il messaggio con la chiave 'msg_meteo'.
Esaminiamo il meccanismo per i cambiamenti di lingua. Abbiamo visto che la funzione [config] nel modulo [rdvmedecins] aveva impostato il francese come lingua predefinita (riga 9 qui sotto):
// i18n configuration
angular.module("rdvmedecins")
.config(['$translateProvider', function ($translateProvider) {
// french messages
$translateProvider.translations("fr", {...});
// english messages
$translateProvider.translations("en", {...});
// default language
$translateProvider.preferredLanguage("fr");
}]);
Si noti che anche la lingua predefinita era il francese. Nell'inizializzazione del controller [rdvmedecins], abbiamo scritto:
// we put the locale in French
angular.copy(locales['fr'], $locale);
- riga 2: [locales] è un dizionario che abbiamo creato;
Non c'è alcun collegamento tra l'internazionalizzazione dei messaggi fornita dal modulo [pascalprecht.translate] e la localizzazione della data che abbiamo implementato. Quest'ultima utilizza una variabile $locale che non viene utilizzata dal modulo [pascalprecht.translate]. Si tratta di due processi indipendenti l'uno dall'altro.
Ora è il momento di vedere cosa succede quando l'utente cambia la lingua:

- riga 251: quando la lingua cambia, viene chiamata la funzione [setLang] con uno dei due parametri ['fr','en'];
- righe 252–257: sono già state spiegate: modificano la variabile [$locale] del calendario. Ciò non ha alcun effetto sulla lingua delle traduzioni;
- riga 259: cambiamo la lingua di traduzione. Usiamo l'oggetto [$translate] fornito dal modulo [pascalprecht.translate]. Per farlo, dobbiamo inserirlo nel controller:
// controller
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', '$locale', '$translate', '$filter',
function ($scope, $locale, $translate, $filter) {
Nelle righe 3 e 4 sopra riportate, viene inserito l'oggetto $translate;
- il parametro lang della funzione [$translate.use(lang)] deve essere impostato su una delle chiavi utilizzate nella configurazione come primo parametro della funzione [$translateProvider.translations], ovvero 'fr' o 'en'. Questo è effettivamente il caso;
- Riga 261: Ricalcoliamo il valore di msg2. Perché? Nella vista, dopo il cambio di lingua effettuato dalla riga 259, tutti gli attributi [translate] esistenti verranno rivalutati. Questo non accadrà per l'espressione {{msg2}}, che non possiede questo attributo. Pertanto, il suo nuovo valore viene calcolato nel controller. Ciò deve essere fatto dopo il cambio di lingua alla riga 259 in modo che la nuova lingua venga utilizzata per il calcolo di [msg2];
Se ci fermiamo qui, osserviamo due anomalie:
![]() |
- in [1], il giorno rimane in francese mentre il resto della schermata è in inglese;
- in [2] e [3], la data selezionata è il 24 giugno, mentre in [1] la data rimane impostata sul 20 giugno;
Proviamo a spiegare questi problemi prima di trovare delle soluzioni. Il messaggio [1] è generato nel controller con il seguente codice:
$scope.msg = {'text': 'msg_agenda', 'model': {'titre': 'Mme', 'prenom': 'Laure', 'nom': 'PELISSIER', 'jour': $filter('date')($scope.jour, 'fullDate')}};
e visualizzato nella vista con il seguente codice:
<h3 class="alert alert-warning" translate="{{msg.text}}" translate-values="{{msg.model}}"></h3>
L'anomalia [1] (il giorno è rimasto in francese mentre il resto della vista è in inglese) sembra indicare che, mentre l'attributo [translate] viene rivalutato durante un cambio di lingua, ciò non è avvenuto per l'attributo [translate-values]. Possiamo quindi forzare questa valutazione nel controller:
// ------------------- evts manager
// language change
$scope.setLang = function (lang) {
...
// update msg2
$scope.msg2 = $filter('translate')('msg_meteo');
// and msg day
$scope.msg.model.jour = $filter('date')($scope.jour, 'fullDate');
};
Ogni volta che la lingua cambia, la riga 8 sopra ricalcola il giorno visualizzato. Questo risolve efficacemente il primo problema ma non il secondo (il giorno visualizzato nel messaggio non cambia quando si seleziona un altro giorno nel calendario). La ragione di questo comportamento è la seguente. Il messaggio viene visualizzato nella vista con il seguente codice:
<h3 class="alert alert-warning" translate="{{msg.text}}" translate-values="{{msg.model}}"></h3>
La vista V visualizzata cambia solo se cambia il suo modello M. Tuttavia, in questo caso, la selezione di un nuovo giorno nel calendario attiva un evento che non viene gestito, il che significa che il modello [msg] non cambia e quindi non cambia nemmeno la vista. Aggiorniamo la definizione del calendario nella vista:
<datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"
ng-click="calendarClick()"></datepicker>
Sopra, specifichiamo che il clic sul calendario deve essere gestito dalla funzione [$scope.calendarClick]. Questa funzione è la seguente:

- riga 267: il gestore del clic sul calendario;
- riga 269: forziamo l'aggiornamento del giorno visualizzato utilizzando il messaggio [msg];
3.7.4. Esempio 4: un servizio di configurazione
Rivediamo l'architettura di un'applicazione AngularJS:
![]() |
Qui ci concentreremo sul concetto di servizio. Si tratta di un concetto piuttosto ampio. Sebbene il livello [DAO] sopra indicato sia chiaramente un servizio, qualsiasi oggetto Angular può diventare un servizio:
- un servizio segue una sintassi specifica. Ha un nome, e Angular lo identifica tramite quel nome;
- un servizio può essere iniettato da Angular nei controller e in altri servizi;
Alcuni dei servizi che configureremo nel modulo [rdvmedecins] dovranno essere configurati. Poiché un servizio può essere iniettato in un altro servizio, è allettante eseguire la configurazione in un servizio che chiameremo [config] e iniettarlo nei servizi e nei controller da configurare. Descriveremo ora questo processo.
Duplichiamo [app-13.html] in [app-14.html] e apportiamo le seguenti modifiche:
<div class="container">
<!-- waiting msg control -->
<label>
<input type="checkbox" ng-model="waiting.visible">
<span>Voir le message d'attente</span>
</label>
<!-- the waiting message -->
<div class="alert alert-warning" ng-show="waiting.visible">
<h1>{{ waiting.text | translate}}
<button class="btn btn-primary pull-right" ng-click="waiting.cancel()">
{{'msg_cancel'|translate}}</button>
<img src="assets/images/waiting.gif" alt=""/>
</h1>
</div>
...
</div>
...
<script type="text/javascript" src="rdvmedecins-02.js"></script>
- Righe 3–6: Una casella di controllo che determina se visualizzare il messaggio di attesa nelle righe 9–15. Il valore della casella di controllo è memorizzato nella variabile [waiting.visible] del modello M della vista V. Questo valore è true se la casella di controllo è selezionata e false in caso contrario. Funziona in entrambi i modi. Se impostiamo la variabile [waiting.visible] su true, la casella di controllo sarà selezionata. Esiste un'associazione bidirezionale tra la vista V e il suo modello M;
- righe 9–15: un messaggio di attesa con un pulsante per annullare l'attesa (riga 11);
- riga 9: il messaggio è visibile solo se la variabile [waiting.visible] ha il valore true. Quindi, quando selezioniamo la casella di controllo alla riga 4:
- alla variabile [waiting.visible] viene assegnato il valore true (ng-model, riga 4);
- poiché c'è stata una modifica al modello M, la vista V viene automaticamente rivalutata. Il messaggio di attesa verrà quindi reso visibile (ng-show, riga 9);
- il ragionamento è simile quando si deseleziona la casella di controllo alla riga 4: il messaggio di attesa viene nascosto;
- riga 10: il messaggio di attesa viene tradotto (filtro translate);
- riga 11: quando si fa clic sul pulsante, viene eseguito il metodo [waiting.cancel()] (attributo ng-click);
- riga 12: l'etichetta del pulsante viene tradotta;
- riga 19: inseriamo il codice JavaScript dell'applicazione in un nuovo file JS [rdvmedecins-02] per non perdere il codice già scritto e che ora deve essere riorganizzato;
Il risultato è la seguente vista:
![]() |
- in [1], casella deselezionata;
- in [2], casella selezionata;
Lo script [rdvmedecins-02] è una riorganizzazione dello script [rdvmedecins]:

- riga 6: il modulo [rdvmedecins] dell'applicazione;
- righe 9-10: la funzione di configurazione dell'applicazione;
- righe 38-39: il servizio [config];
- righe 283-284: il controller [rdvMedecinsCtrl];
In precedenza, avevamo definito il dizionario locales={'fr':..., 'en': ...} nel controller, che era lungo 200 righe. Questo dizionario è chiaramente un elemento di configurazione, quindi lo stiamo spostando nel servizio [config] alle righe 38–39. Questo servizio è definito come segue:

- Righe 38-39: un servizio viene creato utilizzando la funzione [factory] dell'oggetto [angular.module]. La sintassi di questa funzione è la stessa delle precedenti: factory('service_name', ['O1', 'O2', ..., 'On', function (O1, O2, ..., On){...}]), dove gli Oi sono nomi di oggetti noti ad Angular (predefiniti o creati dallo sviluppatore) che Angular inietta come parametri nella funzione factory. Poiché la funzione non ha parametri in questo caso, abbiamo utilizzato una sintassi più breve, ma ugualmente valida: factory('service_name', function (){...}]);
- riga 40: la funzione [factory] deve implementare il servizio utilizzando un oggetto che restituisce. Questo oggetto è il servizio. Ecco perché la funzione si chiama factory (fabbrica di creazione di oggetti);
In generale, il codice del servizio assume la forma:
Angular.module('nom_module')
.factory('nom_service',['O1','O2', ...., 'On', function (O1, O2, ..., On){
// service preparation
...
// render the object implementing the service
return {
// fields
...
// methods
...
}
});
- Riga 6: restituiamo un oggetto JavaScript che può contenere sia campi che metodi. Sono i metodi a gestire il servizio;
Qui, il servizio [config] definisce solo campi e nessun metodo. Metteremo qui tutto ciò che può essere configurato nell'applicazione:
- righe 42–47: le chiavi per i messaggi da tradurre;
- righe 59–62: gli URL dell'applicazione;
- righe 64–69: gli URL del servizio web remoto;
- riga 71: una chiamata HTTP a un servizio web che non risponde potrebbe richiedere molto tempo. Qui impostiamo il tempo massimo di attesa per la risposta del servizio web a 1 secondo. Trascorso questo tempo, la chiamata HTTP fallisce e viene generata un'eccezione JavaScript;
- riga 73: prima di ogni chiamata al server, simuleremo un'attesa la cui durata è impostata qui in millisecondi. Un'attesa pari a 0 significa che non c'è attesa. L'applicazione sarà progettata in modo che l'utente possa annullare un'operazione che ha avviato. Affinché sia annullabile, deve durare almeno alcuni secondi. Useremo questa attesa artificiale per simulare operazioni di lunga durata;
- riga 75: in modalità [debug=true], vengono visualizzate informazioni aggiuntive nella vista corrente. Per impostazione predefinita, questa modalità è abilitata. In produzione, imposteremmo questo campo su false;
- righe 77–278: il dizionario per le due impostazioni locali, 'fr' e 'en'. In precedenza si trovava nel controller [rdvMedecinsCtrl];
Con questo servizio, il controller [rdvMedecinsCtrl] si evolve come segue:

- righe 284-285: il servizio [config] viene iniettato nel controller;
- riga 290: il dizionario [locales] si trova ora nel servizio [config] e non più nel controller;
- riga 294: l'oggetto [waiting] che controlla la visualizzazione del messaggio di attesa. La chiave per il messaggio di attesa si trova nel servizio [config] (campo text). Per impostazione predefinita, il messaggio di attesa è nascosto (campo visible). Il campo cancel ha come valore il nome della funzione alla riga 316. Questo campo è quindi un metodo o una funzione;
- riga 316: la funzione [cancel] è privata (non abbiamo scritto $scope.cancel=function(){}). Rivediamo il codice per il pulsante di annullamento:
<button class="btn btn-primary pull-right" ng-click="waiting.cancel()">
Quando l'utente fa clic sul pulsante di annullamento, viene chiamato il metodo [$scope.waiting.cancel()]. In definitiva, è la funzione privata cancel della riga 316 che viene eseguita. Essa nasconde semplicemente il messaggio di attesa impostando la variabile del modello [waiting.visible] su false (riga 318);
3.7.5. Esempio 5: Programmazione asincrona
Introdurremo ora un nuovo servizio con un nuovo concetto: la programmazione asincrona.
![]() |
La nostra applicazione disporrà di tre servizi:
- [config]: il servizio di configurazione che abbiamo appena presentato;
- [utils]: un servizio di metodi di utilità. Ne presenteremo due;
- [dao]: il servizio per accedere al servizio web di pianificazione degli appuntamenti. Lo presenteremo tra poco;
Scriveremo la seguente applicazione:
![]() |
![]() |
- l'obiettivo è visualizzare il banner [2] per un periodo di tempo impostato da [1]. L'attesa può essere annullata da [3].
Duplichiamo [app-01.html] in [app-15.html] e modifichiamo il codice come segue:
<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
<title>RdvMedecins</title>
...
</head>
<body ng-controller="rdvMedecinsCtrl">
<div class="container">
<!-- the waiting message -->
<div class="alert alert-warning" ng-show="waiting.visible" ng-cloak="">
<h1>{{ waiting.text | translate}}
<button class="btn btn-primary pull-right" ng-click="waiting.cancel()">{{'msg_cancel'|translate}}</button>
<img src="assets/images/waiting.gif" alt=""/>
</h1>
</div>
<!-- the form -->
<div class="alert alert-info" ng-hide="waiting.visible">
<div class="form-group">
<label for="waitingTime">{{waitingTimeText | translate}}</label>
<input type="text" id="waitingTime" ng-model="waiting.time"/>
</div>
<button class="btn btn-primary" ng-click="execute()">Exécuter</button>
</div>
</div>
..
<script type="text/javascript" src="rdvmedecins-03.js"></script>
</body>
</html>
- riga 11: l'attributo [ng-cloak] impedisce la visualizzazione dell'area fino a quando le sue espressioni Angular non sono state valutate. Ciò impedisce che l'area appaia brevemente prima che l'attributo [ng-show] venga valutato, il che ne causerà effettivamente l'occultamento;
- riga 22: l'input dell'utente (tempo di attesa) verrà memorizzato nel modello [waiting.time] (attributo ng-model);
- riga 28: la pagina utilizza un nuovo script [rdvmedecins-03];
Lo script [rdvmedecins-03] è il seguente:

- riga 6: il modulo Angular che gestisce l'applicazione;
- riga 10: la funzione [config] utilizzata per internazionalizzare i messaggi;
- riga 41: il servizio [config] che abbiamo descritto;
- riga 286: il servizio [utils] che realizzeremo;
- riga 315: il controller [rdvmedecinsCtrl] che realizzeremo;
Aggiungiamo una nuova chiave di messaggio alla funzione [config] (righe 6, 11):
angular.module("rdvmedecins")
.config(['$translateProvider', function ($translateProvider) {
// french messages
$translateProvider.translations("fr", {
...
'msg_waiting_time_text': "Temps d'attente : "
});
// english messages
$translateProvider.translations("en", {
...
'msg_waiting_time_text': "Waiting time:"
});
// default language
$translateProvider.preferredLanguage("fr");
}]);
Aggiungiamo una nuova riga (riga 6) al servizio [config] per questa chiave di messaggio:
angular.module("rdvmedecins")
.factory('config', function () {
return {
// messages to be internationalized
...
waitingTimeText: 'msg_waiting_time_text',
Il servizio [utils] contiene due metodi (righe 4, 12):
angular.module("rdvmedecins")
.factory('utils', ['config', '$timeout', '$q', function (config, $timeout, $q) {
// display the Json representation of an object
function debug(message, data) {
if (config.debug) {
var text = data ? message + " : " + angular.toJson(data) : message;
console.log(text);
}
}
// waiting
function waitForSomeTime(milliseconds) {
// asynchronous waiting milliseconds milliseconds
var task = $q.defer();
$timeout(function () {
task.resolve();
}, milliseconds);
// we return the task
return task;
};
// service authority
return {
debug: debug,
waitForSomeTime: waitForSomeTime
}
}]);
- riga 2: il servizio si chiama [utils] (primo parametro). Dipende da tre servizi: due servizi Angular predefiniti, $timeout e $q, e il servizio config. Il servizio [$timeout] consente di eseguire una funzione dopo che è trascorso un determinato periodo di tempo. Il servizio [$q] consente di creare attività asincrone;
- riga 4: una funzione locale [debug];
- riga 12: una funzione locale [waitForSomeTime];
- righe 23–26: l'istanza del servizio [utils]. Si tratta di un oggetto che espone due metodi, quelli alle righe 4 e 12. Si noti che i campi dell'oggetto possono avere nomi qualsiasi. Per coerenza, abbiamo dato loro i nomi delle funzioni a cui fanno riferimento;
- righe 4–9: il metodo [debug] scrive un messaggio [message] sulla console e, se applicabile, la rappresentazione JSON di un oggetto [data]. Ciò consente di visualizzare oggetti di qualsiasi complessità;
- righe 12–20: il metodo [waitForSomeTime] crea un'attività asincrona che dura [milliseconds] millisecondi;
- riga 14: creazione di un'attività utilizzando l'oggetto predefinito [$q] (https://docs.angularjs.org/api/ng/service/$q). Di seguito è riportata l'API per l'attività denominata [deferred] nella documentazione di Angular:

- un'attività asincrona [task] viene creata dall'istruzione [$q.defer()];
- viene completato utilizzando uno dei due metodi:
- [task.resolve(value)]: che completa con successo l'attività e restituisce il valore [value] a coloro che attendono il completamento dell'attività;
- [task.reject(value)]: che termina l'attività con un errore e restituisce il valore [value] a chi sta aspettando che l'attività finisca;
L'attività [task] può fornire periodicamente informazioni a chi ne attende il completamento:
- [task.notify(value)]: invia il valore [value] a coloro che attendono il completamento dell'attività. L'attività continua a essere eseguita;
Chi desidera attendere il completamento del task utilizza il suo campo [promise]:
L'oggetto [promise] presenta la seguente API (http://www.frangular.com/2012/12/api-promise-angularjs.html):

Per gestire sia il successo che il fallimento dell'attività, scriviamo:
- Riga 1: recuperiamo la promessa del task;
- riga 2: definiamo le funzioni da eseguire in caso di successo o di fallimento. Possiamo scegliere di non includere una funzione di fallimento. La funzione [successCallback] verrà eseguita solo se il [task] viene completato con successo [task.resolve()]. La funzione [errorCallback] verrà eseguita solo se il [task] fallisce [task.reject()].
- Riga 3: Definiamo la funzione da eseguire dopo che una delle due funzioni precedenti è stata eseguita. Qui inseriamo il codice comune a entrambe le funzioni [successCallback, errorCallback].
Torniamo al codice della funzione [waitForSomeTime]:
// attente
function waitForSomeTime(milliseconds) {
// attente asynchrone de milliseconds millisecondes
var task = $q.defer();
$timeout(function () {
task.resolve();
}, milliseconds);
// on retourne la tâche
return task;
};
- riga 4: viene creato un task;
- righe 5–7: l'oggetto [$timeout] consente di definire una funzione (primo parametro) che viene eseguita dopo un certo ritardo espresso in millisecondi (secondo parametro). In questo caso, il secondo parametro della funzione [$timeout] è il parametro del metodo (riga 1);
- riga 6: dopo un ritardo di [milliseconds], l'operazione viene completata con successo;
- riga 9: viene restituito il task [task]. È importante notare che la riga 9 viene eseguita immediatamente dopo la definizione dell'oggetto [$timeout]. Non si attende che trascorra il ritardo di [milliseconds]. Il codice nelle righe 2–10 viene quindi eseguito in due momenti diversi:
- la prima volta quando viene definito l'oggetto [$timeout];
- una seconda volta quando il timeout [milliseconds] è trascorso;
Si tratta di una funzione asincrona: il suo risultato viene ottenuto in un momento successivo alla sua esecuzione.
Il codice del controller che utilizza il servizio [config] è il seguente:
// controller
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', '$filter',
function ($scope, utils, config, $filter) {
// ------------------- model initialization
// waiting message
$scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: undefined};
$scope.waitingTimeText = config.waitingTimeText;
// waiting task
var task;
// logs
utils.debug("libellé temps d'attente", $filter('translate')($scope.waitingTimeText));
utils.debug("locales['fr']=", config.locales['fr']);
// execution action
$scope.execute = function () {
// log
utils.debug('début', new Date());
// the waiting msg is displayed
$scope.waiting.visible = true;
// simulated waiting
task = utils.waitForSomeTime($scope.waiting.time);
// end of wait
task.promise.then(function () {
// success
utils.debug('fin', new Date());
}, function () {
// failure
utils.debug('Opération annulée')
});
task.promise['finally'](function () {
// end of wait in all cases
$scope.waiting.visible = false;
});
};
// cancel wait
function cancel() {
// complete the task
task.reject();
}
}]);
- riga 3: il controller utilizza il servizio [config];
- riga 7: abbiamo aggiunto il campo [time] all'oggetto [$scope.waiting]. L'oggetto [$scope.waiting.time] riceve il valore del tempo di attesa impostato dall'utente;
- riga 8: la chiave per il messaggio di attesa visualizzato dalla vista è inserita nel modello [$scope.waitingTimeText]. In generale, tutto ciò che viene visualizzato da una vista V deve essere inserito nell'oggetto [$scope];
- riga 10: una variabile locale. Non è esposta alla vista V;
- righe 12-13: utilizzo del metodo [debug] del servizio [config]. Il seguente risultato viene visualizzato sulla console:
Riga 2: Otteniamo la rappresentazione JSON dell'oggetto locales['fr'].
- Riga 16: il metodo eseguito quando l'utente fa clic sul pulsante [Esegui];
- riga 18: visualizza l'ora di inizio dell'esecuzione del metodo;
- riga 22: viene avviata l'attività [waitForSomeTime]. Non attendiamo che finisca. L'esecuzione prosegue con la riga 24;
- righe 24–30: definiscono le funzioni da eseguire quando l'attività viene completata con successo (riga 26) e in caso di errore (riga 29);
- riga 26: visualizza l'ora di fine dell'esecuzione del metodo;
- riga 29: mostra che l'operazione è stata annullata. Questo avviene solo quando l'utente clicca sul pulsante [Cancel]. L'istruzione alla riga 41 interrompe quindi il task asincrono con un codice di errore;
- righe 31–34: definiscono la funzione da eseguire dopo che una delle due funzioni precedenti è stata eseguita;
È importante comprendere la sequenza di esecuzione di questo codice. Se l'utente imposta un ritardo di 3 secondi e non annulla l'attesa:
- quando fa clic sul pulsante [Esegui], viene eseguita la funzione [$scope.execute]. Le righe 16–34 vengono eseguite senza attendere i 3 secondi. Al termine di questa esecuzione, la vista V viene sincronizzata con il modello M. Viene visualizzato il messaggio di attesa (ng-show=$scope.waiting.visible=true, riga 20) e il modulo viene nascosto (ng-hide=$scope.waiting.visible=true, riga 20);
- da questo momento in poi, l'utente può interagire nuovamente con la vista. In particolare, può cliccare sul pulsante [Annulla];
- se non lo fa, dopo 3 secondi, viene eseguita la funzione [$timeout] (vedi righe 5–7 di seguito):
// attente
function waitForSomeTime(milliseconds) {
// attente asynchrone de milliseconds millisecondes
var task = $q.defer();
$timeout(function () {
task.resolve();
}, milliseconds);
// on retourne la tâche
return task;
};
- Dopo 3 secondi, il codice viene eseguito. Questo codice completa l'attività [task] con un codice di successo (resolve). Ciò attiverà l'esecuzione di tutto il codice che era in attesa di questo completamento (riga 4 di seguito):
// simulated waiting
task = utils.waitForSomeTime($scope.waiting.time);
// end of wait
task.promise.then(function () {
// success
utils.debug('fin', new Date());
}, function () {
// failure
utils.debug('Opération annulée')
});
task.promise['finally'](function () {
// end of wait in all cases
$scope.waiting.visible = false;
});
- Verrà quindi eseguita la riga 6 sopra riportata (completata con successo). Successivamente verranno eseguite le righe 11–14. Una volta eseguito questo codice, torniamo alla vista V, che verrà quindi sincronizzata con il suo modello M. Il messaggio di attesa viene nascosto (ng-show=$scope.waiting.visible=false, riga 13) e viene visualizzato il modulo (ng-hide=$scope.waiting.visible=false, riga 13);
La schermata viene quindi visualizzata come segue:
Come mostrato sopra, c'è un ritardo di 3 secondi (06:01–05:58) tra l'inizio e la fine dell'attesa. Al contrario, se l'utente annulla l'attesa prima che siano trascorsi i 3 secondi, viene visualizzato quanto segue:
Infine, è importante comprendere che in un dato momento esiste un solo thread di esecuzione, noto come thread UI (User Interface). Il completamento di un'attività asincrona viene segnalato da un evento, proprio come il clic su un pulsante. Questo evento non viene elaborato immediatamente. Viene inserito nella coda degli eventi in attesa di esecuzione. Quando arriva il suo turno, viene elaborato. Questa elaborazione utilizza il thread UI, quindi durante questo periodo l'interfaccia si blocca. Non risponde agli input dell'utente. Per questo motivo, è importante che l'elaborazione degli eventi sia veloce. Poiché ogni evento viene elaborato dal thread UI, non è mai necessario risolvere problemi di sincronizzazione tra thread in esecuzione simultanea. In un dato momento, è in esecuzione solo il thread UI.
3.7.6. Esempio 6: Servizi HTTP
Presentiamo ora il servizio [dao] che comunica con il server web:
![]() |
3.7.6.1. La vista V
![]() |
Scriveremo un modulo per richiedere l'elenco dei medici:

Duplichiamo [app-01.html] in [app-16.html], che poi modifichiamo come segue:
<div class="container" ng-cloak="">
<h1>Rdvmedecins - v1</h1>
<!-- the waiting message -->
<div class="alert alert-warning" ng-show="waiting.visible" ng-cloak="">
<h1>{{ waiting.text | translate}}
<button class="btn btn-primary pull-right" ng-click="waiting.cancel()">{{'msg_cancel'|translate}}</button>
<img src="assets/images/waiting.gif" alt=""/>
</h1>
</div>
<!-- the request -->
<div class="alert alert-info" ng-hide="waiting.visible">
<div class="form-group">
<label for="waitingTime">{{waitingTimeText | translate}}</label>
<input type="text" id="waitingTime" ng-model="waiting.time"/>
</div>
<div class="form-group">
<label for="urlServer">{{urlServerLabel | translate}}</label>
<input type="text" id="urlServer" ng-model="server.url"/>
</div>
<div class="form-group">
<label for="login">{{loginLabel | translate}}</label>
<input type="text" id="login" ng-model="server.login"/>
</div>
<div class="form-group">
<label for="password">{{passwordLabel | translate}}</label>
<input type="password" id="password" ng-model="server.password"/>
</div>
<button class="btn btn-primary" ng-click="execute()">{{medecins.title|translate:medecins.model}}</button>
</div>
<!-- list of doctors -->
<div class="alert alert-success" ng-show="medecins.show">
{{medecins.title|translate:medecins.model}}
<ul>
<li ng-repeat="medecin in medecins.data">{{medecin.titre}}{{medecin.prenom}} {{medecin.nom}}</li>
</ul>
</div>
<!-- the error list -->
<div class="alert alert-danger" ng-show="errors.show">
{{errors.title|translate:errors.model}}
<ul>
<li ng-repeat="message in errors.messages">{{message|translate}}</li>
</ul>
</div>
</div>
...
<script type="text/javascript" src="rdvmedecins-04.js"></script>
- Righe 13–31: implementazione del modulo. Questo modulo non è visibile quando viene visualizzato il messaggio di attesa (ng-hide="waiting.visible"). Si noti che i quattro input sono memorizzati negli attributi (ng-model) [waiting.time (riga 16), server.url (riga 20), server.login (riga 24), server.password (riga 28)];
- righe 34–39: visualizza l'elenco dei medici. Questo elenco non è sempre visibile (ng-show="medecins.show").
- riga 35: un'alternativa alla sintassi <div ... translate="{{medecins.title}}" translate-values="{{medecins.model}}"> già incontrata;
- riga 36: un elenco non ordinato;
- riga 37: l'elenco dei medici si trova nel modello [medecins.data]. La direttiva Angular [ng-repeat] consente di iterare attraverso un elenco. La sintassi ng-repeat="doctor in medecins.data" indica al tag <li> di essere ripetuto per ogni elemento dell'elenco [medecins.data]. L'elemento corrente nell'elenco è chiamato [medecin];
- riga 37: per ogni <li>, visualizziamo il titolo, il nome e il cognome del medico corrente designato dalla variabile [medecin];
- righe 42–47: visualizza l'elenco degli errori. Questo elenco non è sempre visibile (ng-show="errors.show"). Questa visualizzazione segue lo stesso schema della visualizzazione dell'elenco dei medici. In generale, per visualizzare un elenco di oggetti, si utilizza la direttiva Angular [ng-repeat];
- riga 51: il codice JavaScript si trova ora nel file [rdvmedecins-04]
3.7.6.2. Il controller C e il modello M
![]() |
Il codice JavaScript cambia come segue:

- righe 6–9: il modulo [rdvmedecins] dichiara una dipendenza dal modulo [base64] fornito dalla libreria [angular-base64], che è una delle dipendenze del progetto. Questo modulo viene utilizzato per codificare in Base64 la stringa [login:password] inviata al servizio web per l'autenticazione;
- righe 12–13: la funzione di inizializzazione contenente i nostri messaggi internazionalizzati. Appaiono nuovi messaggi. Non li approfondiremo ulteriormente;
- righe 69–70: il servizio [config] che configura la nostra applicazione. Qui sono state aggiunte nuove chiavi di messaggio. Non le tratteremo ulteriormente;
- righe 318–319: il servizio [utils], che contiene metodi di utilità. Ne sono stati aggiunti di nuovi. Li presenteremo;
- righe 385–386: il servizio [dao] responsabile della comunicazione con il servizio web. È su questo che ci concentreremo;
- righe 467–468: il controller C per la vista V di cui abbiamo appena parlato. Ne parleremo ora perché funge da orchestratore che risponde alle richieste degli utenti;
3.7.6.3. Il controller C
Il codice del controller è il seguente:
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate',
function ($scope, utils, config, dao, $translate) {
// ------------------- model initialization
// model
$scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: undefined};
$scope.waitingTimeText = config.waitingTimeText;
$scope.server = {url: undefined, login: undefined, password: undefined};
$scope.medecins = {title: config.listMedecins, show: false, model: {}};
$scope.errors = {show: false, model: {}};
$scope.urlServerLabel = config.urlServerLabel;
$scope.loginLabel = config.loginLabel;
$scope.passwordLabel = config.passwordLabel;
// asynchronous task
var task;
// execution action
$scope.execute = function () {
// the UI is updated
$scope.waiting.visible = true;
$scope.medecins.show = false;
$scope.errors.show = false;
// simulated waiting
task = utils.waitForSomeTime($scope.waiting.time);
var promise = task.promise;
// waiting
promise = promise.then(function () {
// we ask for the list of doctors;
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrMedecins);
return task.promise;
});
// analyze the result of the previous call
promise.then(function (result) {
// result={err: 0, data: [med1, med2, ...]}
// result={err: n, messages: [msg1, msg2, ...]}
if (result.err == 0) {
// we put the acquired data into the model
$scope.medecins.data = result.data;
// the UI is updated
$scope.medecins.show = true;
$scope.waiting.visible = false;
} else {
// there were errors in obtaining the list of doctors
$scope.errors = { title: config.getMedecinsErrors, messages: utils.getErrors(result), show: true, model: {}};
// the UI is updated
$scope.waiting.visible = false;
}
});
};
// cancel wait
function cancel() {
// complete the task
task.reject();
// the UI is updated
$scope.waiting.visible = false;
$scope.medecins.show = false;
$scope.errors.show = false;
}
}
])
;
- riga 2: il controller ha una nuova dipendenza, ovvero il servizio [dao];
- righe 6–13: il modello M della vista V viene inizializzato la prima volta che la vista viene visualizzata;
- riga 8: [$scope.server] verrà utilizzato per recuperare tre delle quattro informazioni dal modulo V; la quarta è memorizzata in [$scope.waiting.time] (riga 6);
- riga 9: [$scope.doctors] raccoglierà le informazioni necessarie per visualizzare l'elenco dei medici:
<!-- list of doctors -->
<div class="alert alert-success" ng-show="medecins.show">
{{medecins.title|translate:medecins.model}}
<ul>
<li ng-repeat="medecin in medecins.data">{{medecin.titre}}{{medecin.prenom}} {{medecin.nom}}</li>
</ul>
</div>
L'attributo [medecins.title] sarà il titolo del banner. È definito nel servizio [config]. L'attributo [medecins.show] controllerà se il banner verrà visualizzato o meno (attributo ng-show="medecins.show"). L'attributo [medecins.model] è un dizionario vuoto e rimarrà tale. Viene utilizzato semplicemente per illustrare l'uso della variante di traduzione utilizzata nella riga 3. Non ancora definito, l'attributo [medecins.data] conterrà l'elenco dei medici (riga 5).
- Riga 10: [$scope.errors] raccoglierà le informazioni necessarie per visualizzare l'elenco degli errori:
<!-- the error list -->
<div class="alert alert-danger" ng-show="errors.show">
{{errors.title|translate:errors.model}}
<ul>
<li ng-repeat="message in errors.messages">{{message|translate}}</li>
</ul>
</div>
L'attributo [errors.title] sarà il titolo del banner. È definito nel servizio [config]. L'attributo [errors.show] controlla se il banner viene visualizzato o meno (attributo ng-show="errors.show"). L'attributo [errors.model] è un dizionario vuoto e rimarrà tale. Viene utilizzato semplicemente per illustrare l'uso della variante di traduzione utilizzata nella riga 3. Non ancora definito, l'attributo [errors.messages] conterrà l'elenco dei messaggi di errore da visualizzare (riga 5).
- Riga 16: l'attività asincrona. Il controller avvierà successivamente due attività asincrone. I riferimenti a queste attività successive saranno inseriti nella variabile [task]. Ciò consentirà di annullarle (riga 55);
- Riga 19: Il metodo eseguito quando l'utente clicca sul pulsante [Elenco dei medici]:
<button class="btn btn-primary" ng-click="execute()">Liste des médecins</button>
- Righe 21–23: L'interfaccia utente viene aggiornata: viene visualizzato il messaggio di caricamento e tutto il resto viene nascosto;
- riga 25: viene creata l'attività di attesa asincrona. Un segnale (attività completata) verrà ricevuto dopo che sarà trascorso il tempo inserito dall'utente nel modulo;
- riga 26: recuperiamo la promessa dell'attività asincrona. Il programma che avvia l'attività funziona con questa promessa. Tuttavia, dobbiamo avere il riferimento all'attività stessa per poterla annullare (riga 55);
- righe 28–32: definiamo il lavoro da svolgere una volta completata l'attesa;
- riga 30: utilizziamo il metodo [dao.getData] per avviare una nuova attività asincrona. Gli passiamo le informazioni necessarie:
- l'URL radice del servizio web [$scope.server.url], ad esempio [http://localhost:8080];
- il login [$scope.server.login] per l'autenticazione, ad esempio [admin];
- la password [$scope.server.password] per l'autenticazione, ad esempio [admin];
- l'URL che restituisce il servizio richiesto [config.urlSvrMedecins], in questo caso [/getAllMedecins]. In totale, l'URL completo sarà [http://localhost:8080/getAllMedecins];
Il metodo [dao.getData] restituisce un risultato che può assumere due forme:
- (continua)
- {err: 0, data: [med1, med2, ...]} dove [medi] è un oggetto che rappresenta un medico (titolo, nome, cognome),
- {err: n, messages: [msg1, msg2, ...]} dove [msg] è un messaggio di errore e n non è uguale a 0;
- riga 31: restituiamo la promessa dell'attività. Qui c'è qualcosa da capire. Abbiamo due promesse:
- promise.then(): restituisce una prima promessa [promise1];
- return task.promise: restituisce una seconda promessa [promise2];
- In definitiva, promise = promise.then(...; return task.promise) è una catena di due promesse [promise2.promise1]. [promise1] verrà valutata solo una volta che la promessa [promise2] sarà risolta, ovvero quando il task [dao.getData] sarà completato. La promessa [promise1] non dipende da alcuna attività asincrona. Verrà quindi risolta immediatamente;
- righe 34–50: Dalla spiegazione precedente, ne consegue che queste righe verranno eseguite solo una volta completata l'attività [dao.getData]. Il parametro [result] passato alla funzione alla riga 34 viene costruito dal metodo [dao.getData] e passato al codice chiamante tramite l'operazione [task.resolve(result)], dove [result] ha la seguente forma:
- {err: 0, data: [med1, med2, ...]} dove [medi] è un oggetto che rappresenta un medico (titolo, nome, cognome),
- {err: n, messages: [msg1, msg2, ...]} dove [msg1] è un messaggio di errore e n non è uguale a 0;
- riga 37: controlliamo il codice di errore [result.err];
- righe 38–42: se non c'è alcun errore (result.err == 0), allora recuperiamo l'elenco dei medici e lo visualizziamo;
- righe 44–47: se, invece, c'è un errore (result.err != 0), allora recuperiamo l'elenco dei messaggi di errore e lo visualizziamo;
- righe 53–56: il messaggio di caricamento con il pulsante di annullamento rimane visibile fino al completamento di entrambe le operazioni asincrone. Vediamo cosa succede a seconda di quando avviene l'annullamento:
- Innanzitutto, è importante comprendere che le righe 19–50 vengono eseguite tutte in una volta. A questo punto è stata avviata una sola attività asincrona, quella alla riga 25.
- Dopo questa esecuzione iniziale, la vista V viene aggiornata e quindi il banner di attesa e il relativo pulsante di annullamento sono visibili. Se l'utente annulla l'attesa prima che l'attività alla riga 25 sia completata, viene eseguito il metodo alla riga 53 e l'attività viene annullata con un errore (riga 55);
- Righe 56–59: L'interfaccia viene aggiornata: il modulo viene visualizzato nuovamente e tutto il resto viene nascosto,
- Si torna quindi alla vista V e il browser elabora l'evento successivo. Poiché l'operazione è stata completata, la promessa relativa a tale operazione viene risolta, il che fa scattare un evento. Questo viene quindi elaborato;
- vengono quindi eseguite le righe 28–32. Non è definita alcuna funzione per il caso di errore, quindi non viene eseguito alcun codice. Viene ottenuta una nuova promessa, quella sempre restituita da [promise.then] e sempre risolta;
- dato che l'evento è stato gestito, il controllo torna alla vista V e il browser procede a gestire l'evento successivo. Poiché la [promessa] alla riga 28 è stata risolta, verrà risolta quella alla riga 34, il che attiverà un nuovo evento. Viene quindi gestito;
- le righe 34–49 verranno quindi eseguite a turno, poiché la promessa utilizzata alla riga 34 è stata soddisfatta. Anche in questo caso, poiché non è definita alcuna funzione per il caso di errore, non viene eseguito alcun codice,
- e si arriva quindi alla riga 50. Non c'è più alcuna attività in attesa e viene visualizzata la nuova vista V;
- Supponiamo ora che la cancellazione avvenga mentre è in esecuzione la seconda attività asincrona [dao.getData]. Si applica nuovamente il ragionamento precedente. La fine dell'attività innescherà l'esecuzione delle righe 34–50 con un errore dell'attività. Scopriremo presto che il metodo [dao.getData] effettua una chiamata HTTP asincrona al servizio web. Questa chiamata non verrà annullata, ma il suo risultato non verrà utilizzato.
È importante comprendere questo continuo andirivieni tra il rendering della vista V e la gestione degli eventi del browser. Gli eventi sono innescati dall'utente (un clic) o da operazioni di sistema come il completamento di un'operazione asincrona. Lo stato di inattività del browser è il rendering della vista V. Viene fatto uscire da questo stato di inattività da un evento che si verifica, che poi elabora. Non appena l'evento è stato elaborato, ritorna al suo stato di inattività. La vista V viene quindi aggiornata se l'evento elaborato ha modificato il suo modello M. Il browser viene fatto uscire dal suo stato di inattività dall'evento successivo.
Tutto avviene in un unico thread. Due eventi non vengono mai elaborati contemporaneamente. La loro esecuzione è sequenziale. Il browser passa all'evento successivo solo quando quello precedente rilascia il controllo, solitamente perché è stato completamente elaborato.
C'è un altro punto da spiegare. Per visualizzare i messaggi di errore, scriviamo:
$scope.errors = { title: config.getMedecinsErrors, messages: utils.getErrors(result), show: true, model: {}};
L'elenco dei messaggi è fornito dal metodo [utils.getErrors] definito nel servizio [utils]. Questo metodo è il seguente:
// error analysis in server response JSON
function getErrors(data) {
// data {err:n, messages:[]}, err!=0
// errors
var errors = [];
// error code
var err = data.err;
switch (err) {
case 2 :
// not authorized
errors.push('not_authorized');
break;
case 3 :
// forbidden
errors.push('forbidden');
break;
case 4 :
// local error
errors.push('not_http_error');
break;
case 6 :
// document not found
errors.push('not_found');
break;
default :
// other cases
errors = data.messages;
break;
}
// if no msg, we put one
if (! errors || errors.length == 0) {
errors=['error_unknown'];
}
// return the list of errors
return errors;
}
- righe 2-3: il parametro [data] ricevuto è un oggetto con due attributi:
- [err]: un codice di errore;
- [messages]: un elenco di messaggi;
- riga 5: costruiremo un array di messaggi di errore. Questi messaggi sono internazionalizzati. Per questo motivo, non sono i messaggi stessi che inseriamo nell'array, ma le loro chiavi di internazionalizzazione, tranne che alla riga 27. In questo caso, utilizziamo l'attributo [messages] del parametro [data]. Questi messaggi sono messaggi effettivi e non chiavi di messaggio. Tuttavia, la vista V li tratterà come chiavi di messaggio, che quindi non verranno trovate. In questo caso, il modulo [translate] visualizza la chiave del messaggio che non ha trovato — in questo caso, un messaggio vero e proprio. Questo è il risultato desiderato;
- righe 32–34: gestiscono il caso in cui [data.messages] alla riga 27 sia nullo. Ciò si verifica con il servizio web scritto. Questo scenario avrebbe dovuto essere evitato.
3.7.6.4. Il servizio [dao]
![]() |
Il servizio [dao] gestisce gli scambi HTTP con il servizio web / JSON. Il suo codice è il seguente:
angular.module("rdvmedecins")
.factory('dao', ['$http', '$q', 'config', '$base64', 'utils',
function ($http, $q, config, $base64, utils) {
// logs
utils.debug("[dao] init");
// ----------------------------------méthodes privées
// obtain data from the web service
function getData(serverUrl, username, password, urlAction, info) {
// asynchronous operation
var task = $q.defer();
// url request HTTP
var url = serverUrl + urlAction;
// basic authentication
var basic = "Basic " + $base64.encode(username + ":" + password);
// the answer
var réponse;
// all http requests must be authenticated
var headers = $http.defaults.headers.common;
headers.Authorization = basic;
// query HTTP
var promise;
if (info) {
promise = $http.post(url, info, {timeout: config.timeout});
} else {
promise = $http.get(url, {timeout: config.timeout});
}
promise.then(success, failure);
// the task itself is returned so that it can be cancelled
return task;
// success
function success(response) {
// response.data={status:0, data:[med1, med2, ...]} or {status:x, data:[msg1, msg2, ...]
utils.debug("[dao] getData[" + urlAction + "] success réponse", response);
// answer
var payLoad = response.data;
réponse = payLoad.status == 0 ? {err: 0, data: payLoad.data} : {err: 1, messages: payLoad.data};
// we return the answer
task.resolve(réponse);
}
// failure
function failure(response) {
utils.debug("[dao] getData[" + urlAction + "] error réponse", response);
// status analysis
var status = response.status;
var error;
switch (status) {
case 401 :
// unauthorized
error = 2;
break;
case 403:
// forbidden
error = 3;
break;
case 404:
// not found
error = 6;
break;
case 0:
// local error
error = 4;
break;
default:
// something else
error = 5;
}
// we return the answer
task.resolve({err: error, messages: [response.statusText]});
}
}
// --------------------- service instance [dao]
return {
getData: getData
}
}]);
- righe 77-79: il servizio ha un solo campo: il metodo [getData], che recupera informazioni dal servizio web / JSON;
- riga 2: compare una dipendenza [$http] che non abbiamo ancora incontrato. Si tratta di un servizio Angular predefinito che consente la comunicazione HTTP con un'entità remota;
- riga 6: un log per vedere in quale punto del ciclo di vita dell'applicazione viene eseguito il codice;
- riga 10: il metodo [getData] accetta cinque parametri:
- [serverUrl]: l'URL principale del servizio web (http://localhost:8080);
- [urlAction]: l'URL del servizio specifico richiesto (/getAllMedecins);
- [username]: il nome utente;
- [password]: la password dell'utente;
- [info]: un oggetto contenente informazioni aggiuntive quando si accede all'URL del servizio specifico richiesto tramite un'operazione POST. Nel caso dell'URL (/getAllMedecins), questo parametro non è stato passato. È quindi [undefined];
- riga 12: viene creata un'attività asincrona;
- riga 14: l'URL completo del servizio richiesto (http://localhost:8080/getAllMedecins);
- riga 16: l'autenticazione viene eseguita inviando il seguente header HTTP:
dove [codice] è la stringa codificata in Base64 [nome utente:password];
La riga 16 costruisce la parte [Codice Basic] dell'intestazione HTTP;
- riga 18: la risposta del servizio web;
- riga 20: le intestazioni HTTP inviate di default da Angular in una richiesta HTTP sono definite nell'oggetto [$http.defaults.headers.common]. L'intestazione [Authorization:Basic code] non è inclusa;
- riga 21: la aggiungiamo alle intestazioni HTTP da inviare sistematicamente. Sul lato sinistro dell'assegnazione, abbiamo l'intestazione [Authorization] da inizializzare, e sul lato destro, il valore dell'intestazione—in questo caso, il valore definito alla riga 16. Quindi, se scriviamo:
Angular invierà l'intestazione HTTP:
- riga 23: i metodi del servizio [$http] restituiscono delle promesse. Queste verranno memorizzate nella variabile [promise];
- Riga 27: poiché qui il parametro [info] ha il valore [undefined], viene eseguita la riga 27. L'URL (http://localhost:8080/getAllMedecins) viene richiesto tramite una richiesta GET. Per evitare un'attesa troppo lunga, impostiamo un timeout massimo per la ricezione della risposta del server. Per impostazione predefinita, questo timeout è di un secondo;
- riga 29: definiamo i due metodi da eseguire quando la promessa viene soddisfatta:
- [success]: definito alla riga 34, è il metodo da eseguire quando la promessa si risolve al completamento con successo dell'attività;
- [failure]: definito alla riga 45, è il metodo da eseguire quando la promessa viene risolta a causa di un fallimento dell'attività;
- Entrambi i metodi (dovremmo dire funzioni) sono definiti all'interno della funzione [getData]. Questo è possibile in JavaScript. Le variabili definite in [getData] sono accessibili all'interno delle due funzioni interne [success] e [failure];
- riga 31: restituiamo l'attività creata alla riga 12. Qui, dobbiamo richiamare il codice di chiamata:
promise = promise.then(function () {
// we ask for the list of doctors;
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrMedecins);
return task.promise;
});
La riga 3 sopra riporta un'attività.
- Riga 34: La funzione [success] viene eseguita in un secondo momento, una volta che la richiesta HTTP è stata completata con successo. Questo concetto di successo è legato alla prima riga di una risposta HTTP. Ha la forma:
Il codice è un numero a tre cifre che indica se la richiesta ha avuto esito positivo o meno. In generale, i codici 2xx e 3xx sono codici di successo, mentre gli altri sono codici di errore. Il testo è un breve messaggio esplicativo. Ecco due possibili risposte, una per il successo e una per l'errore:
- Riga 36: La risposta del server viene visualizzata sulla console. Nell'errore [404 Not Found], otteniamo qualcosa del tipo:
[dao] getData[/getAllMedecins] error réponse : {"data":"...","status":404,"config":{...},"statusText":"Not Found"}
In questa risposta, useremo solo i campi [data], [status] e [statusText].
- Riga 38: recuperiamo il campo [data] dalla risposta. Assumerà una delle seguenti forme:
- {status: 0, data: [med1, med2, ...]} dove [med] è un oggetto che rappresenta un medico (titolo, nome, cognome),
- {status: n, data: [msg1, msg2, ...]} dove [msg1] è un messaggio di errore e n non è uguale a 0;
![]() |

- Riga 39: Costruiamo la risposta {0,data} o {n,messages}. La prima risposta contiene i medici nel campo [data]. La seconda indica un errore verificatosi sul lato server. Il server ha gestito questo errore, generando un codice di errore in [err] e un elenco di messaggi di errore in [data]. In entrambi i casi, restituisce un codice di stato HTTP 200 che indica che la richiesta HTTP è stata completamente elaborata. Questo è il motivo per cui entrambi i casi vengono gestiti nella stessa funzione [success];
- riga 41: l'attività è completata [task.resolve] e viene restituita una delle due risposte:
- {err: 0, data: [med1, med2, ...]} dove [medi] è un oggetto che rappresenta un medico (titolo, nome, cognome),
- {err: n, messages: [msg1, msg2, ...]} dove [msgi] è un messaggio di errore e n non è uguale a 0;
Questo codice deve essere collegato al modo in cui questa risposta viene recuperata nel codice di chiamata del controller:
// analyze the result of the previous call
promise.then(function (result) {
// result={err: 0, data: [med1, med2, ...]}
// result={err: n, messages: [msg1, msg2, ...]}
...
}
La risposta da [task.resolve(response)] viene memorizzata nella variabile [result] sopra.
- riga 45: la funzione [failure] quando l'attività asincrona termina con un errore. Ci sono due casi possibili:
- il server segnala questo errore restituendo un codice di stato che non è né 2xx né 3xx,
- Angular annulla la richiesta HTTP. In questo caso, non viene effettuata alcuna richiesta. Si verifica un'eccezione Angular, ma il server non restituisce alcun codice di errore HTTP. Ciò accade, ad esempio, se viene fornito un URL non valido a cui non è possibile accedere;
- riga 46: visualizziamo la risposta nella console;
- riga 48: ricordiamo che la risposta del server ha il seguente formato:
{"data":"...","status":404,"config":{...},"statusText":"Not Found"}
Riga 48: recuperiamo l'attributo [status] sopra indicato;
- Righe 50–70: in base al codice di errore HTTP, generiamo un nuovo codice di errore per nascondere la natura HTTP del metodo [dao.getData] al codice chiamante. Possiamo verificare che nel controller che utilizza questo metodo, nulla suggerisca la presenza di una chiamata HTTP all'interno del metodo;
- riga 51: l'errore [401] corrisponde a un'autenticazione non riuscita (ad esempio, password errata);
- riga 55: l'errore [403] corrisponde a una richiesta non autorizzata. L'utente si è autenticato correttamente ma non dispone delle autorizzazioni sufficienti per richiedere l'URL richiesto. Ciò si verificherà con l'utente [user / user]. Questo utente esiste nel database ma non ha l'autorizzazione per utilizzare l'applicazione. Solo l'utente [admin / admin] dispone di questa autorizzazione;
- Riga 59: l'errore [404] indica un URL non trovato. L'errore può avere diverse cause:
- l'utente ha commesso un errore di digitazione nell'URL del servizio;
- il servizio web non è stato avviato;
- il servizio web non ha risposto abbastanza rapidamente (timeout predefinito di un secondo);
- riga 63: il codice di errore HTTP 0 non esiste. Ciò si verifica quando Angular non ha effettuato la chiamata HTTP richiesta perché l'URL inserito dall'utente non è valido e non è accessibile. Incontreremo altri casi in seguito in cui Angular non esegue la chiamata HTTP richiesta;
- riga 72: completiamo con successo l'attività (task.resolve) restituendo una risposta di tipo {err, messages}, dove l'array [messages] è costituito esclusivamente dal messaggio [response.statusText]. Se Angular non ha effettuato la chiamata HTTP richiesta, avremo una stringa vuota;
Ora che abbiamo una visione sia generale che dettagliata dell'applicazione, possiamo iniziare i test.
3.7.6.5. Test dell'applicazione - 1
Iniziamo con input validi:

![]() |
- in [1], inseriamo 0 per evitare qualsiasi ritardo;
- in [2], riceviamo un messaggio di errore anche se gli input sono corretti. Non abbiamo ancora trattato i diversi messaggi di errore. Quello visualizzato in [2] è un messaggio generico associato all'errore 0, che corrisponde a un'eccezione di Angular. Angular ha riscontrato un problema che gli ha impedito di effettuare una richiesta HTTP. In questi casi, è necessario controllare i log della console JavaScript. Ci sono due modi per farlo:
- premere [F12] nel browser Chrome;
- utilizzare la console di WebStorm;
Nella console di WebStorm, troviamo vari messaggi, tra cui questo:
- Riga 1: Angular segnala un errore, su cui torneremo;
- riga 2: il log per il metodo [dao.getData]. Ci sono alcuni dettagli interessanti qui:
- [status] è 0, il che indica che non è stata effettuata alcuna richiesta HTTP. Di conseguenza, [statusText] è vuoto,
- [url] è equivalente a [http://localhost:8080/getAllMedecins], il che è corretto;
- anche l'intestazione di autenticazione HTTP [Authorization":"Basic YWRtaW46YWRtaW4=] è corretta;
Allora perché non ha funzionato? La frase chiave nei log è [Non è presente l'intestazione 'Access-Control-Allow-Origin']. Per capirlo, serve una spiegazione un po' lunga. Cominciamo rivedendo l'architettura generale dell'applicazione client/server:

- le pagine HTML/CSS/JS dell'applicazione Angular provengono dal server [1];
- in [2], il servizio [dao] effettua una richiesta a un altro server, il server [2]. Ebbene, questa viene bloccata dal browser che esegue l'applicazione Angular perché si tratta di una vulnerabilità di sicurezza. L'applicazione può interrogare solo il server da cui proviene, ovvero il server [1];
In realtà, non è corretto dire che il browser impedisce all'applicazione Angular di interrogare il server [2]. In realtà lo interroga per chiedere se permette a un client che non proviene dal proprio dominio di interrogarlo. Questa tecnica di condivisione si chiama CORS (Cross-Origin Resource Sharing). Il server [2] concede l'autorizzazione inviando specifici header HTTP. È proprio perché il nostro server [2] non li ha inviati in questo caso che il browser ha rifiutato di effettuare la richiesta HTTP richiesta dall'applicazione.
Ora entriamo nei dettagli. Esaminiamo il traffico di rete che si è verificato durante la richiesta HTTP. Per farlo, nel browser Chrome, premiamo [F12] per aprire gli strumenti di sviluppo e selezioniamo la scheda [Rete] per visualizzare il traffico di rete:
![]() |
- In [1], selezioniamo la scheda [Rete];
- in [2], richiediamo l'elenco dei medici;
Nella scheda [Rete] otteniamo le seguenti informazioni:
![]() |
- in [1], le informazioni inviate al server;
- in [2], la risposta del server;
In [1] si può notare che il browser ha inviato una richiesta HTTP [OPTIONS] all'URL richiesto. [OPTIONS] è uno dei metodi HTTP, insieme ai più noti [GET] e [POST]. Consente di richiedere informazioni a un server, in particolare riguardo alle opzioni HTTP che supporta, da cui il nome del metodo. Il server risponde in [2]. Per indicare che accetta richieste da client al di fuori del proprio dominio, deve restituire un'intestazione specifica chiamata [Access-Control-Allow-Origin]. E poiché non ha restituito questa intestazione, Angular non ha eseguito la chiamata HTTP richiesta e ha restituito l'errore:
XMLHttpRequest cannot load http://localhost:8080/getAllMedecins. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:63342' is therefore not allowed access.
Dobbiamo quindi modificare il nostro server in modo che invii l'intestazione HTTP prevista.
3.7.6.6. Modifica del server Web/JSON
Torniamo a Eclipse. Per conservare i nostri progressi, duplichiamo la versione attuale del server Web/JSON [rdvmedecins-webapi-v2] in [rdvmedecins-webapi-v3] [1]:
![]() |
Apportiamo una modifica iniziale in [ApplicationModel], che è uno degli elementi di configurazione del servizio web:
package rdvmedecins.web.models;
...
@Component
public class ApplicationModel implements IMetier {
// the [business] layer
@Autowired
private IMetier métier;
// data from the [business] layer
private List<Medecin> médecins;
private List<Client> clients;
private List<String> messages;
// configuration data
private boolean CORSneeded = true;
...
public boolean isCORSneeded() {
return CORSneeded;
}
}
- riga 17: creiamo una variabile booleana che indica se i client esterni al dominio del server sono accettati o meno;
- righe 21–23: il metodo per accedere a queste informazioni;
Quindi creiamo un nuovo controller Spring MVC [3]:
![]() |
La classe [RdvMedecinsCorsController] è la seguente:
package rdvmedecins.web.controllers;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import rdvmedecins.web.models.ApplicationModel;
@Controller
public class RdvMedecinsCorsController {
@Autowired
private ApplicationModel application;
// sending options to the customer
private void sendOptions(HttpServletResponse response) {
if (application.isCORSneeded()) {
// set header CORS
response.addHeader("Access-Control-Allow-Origin", "*");
}
}
// list of doctors
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.OPTIONS)
public void getAllMedecins(HttpServletResponse response) {
sendOptions(response);
}
}
- Righe 28–31: definiscono un controller per l'URL [/getAllMedecins] quando viene richiesto con il metodo HTTP [OPTIONS];
- riga 29: il metodo [getAllMedecins] accetta come parametro l'oggetto [HttpServletResponse], che verrà inviato al client che ha effettuato la richiesta. Questo oggetto viene iniettato da Spring;
- riga 30: la gestione della richiesta viene delegata al metodo privato nelle righe 19–25;
- righe 15–16: viene iniettato l'oggetto [ApplicationModel];
- righe 20–23: se il server è configurato per accettare client al di fuori del proprio dominio, viene inviata l'intestazione HTTP:
Access-Control-Allow-Origin: *
il che significa che il server accetta client da qualsiasi dominio (*).
Ora siamo pronti per ulteriori test. Lanciamo la nuova versione del servizio web e scopriamo che il problema rimane invariato. Non è cambiato nulla. Se aggiungiamo un output della console alla riga 30 sopra, non viene mai visualizzato, indicando che il metodo [getAllMedecins] alla riga 29 non viene mai chiamato.
Dopo alcune ricerche, scopriamo che Spring MVC gestisce autonomamente le richieste HTTP [OPTIONS] con un'elaborazione predefinita. Pertanto, è sempre Spring a rispondere, e mai il metodo [getAllMedecins] alla riga 29. Questo comportamento predefinito di Spring MVC può essere modificato. Introduciamo una nuova classe di configurazione per configurare il nuovo comportamento:
![]() |
La nuova classe di configurazione [WebConfig] è la seguente:
package rdvmedecins.web.config;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
// dispatcherservlet configuration for CORS headers
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet();
servlet.setDispatchOptionsRequest(true);
return servlet;
}
}
- riga 8: la classe è una classe di configurazione Spring. Dichiara i bean che verranno inseriti nel contesto Spring;
- riga 12: il bean [dispatcherServlet] viene utilizzato per definire il servlet che gestisce le richieste dei client. È di tipo [DispatcherServlet]. Questo servlet viene normalmente creato per impostazione predefinita. Se lo creiamo noi stessi, possiamo poi configurarlo;
- riga 14: creiamo un'istanza di tipo [DispatcherServlet];
- riga 15: istruiamo il servlet a inoltrare i comandi HTTP [OPTIONS] all'applicazione;
- riga 16: restituiamo il servlet così configurato;
Dobbiamo ancora modificare la classe [AppConfig]:
package rdvmedecins.web.config;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import rdvmedecins.config.DomainAndPersistenceConfig;
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins.web" })
@Import({ DomainAndPersistenceConfig.class, SecurityConfig.class, WebConfig.class })
public class AppConfig {
}
- Riga 11: viene importata la nuova classe di configurazione [WebConfig];
3.7.6.7. Test dell'applicazione - 2
Avviamo la nuova versione del servizio web / JSON e proviamo a recuperare l'elenco dei medici utilizzando il nostro client Angular. Esaminiamo il traffico di rete nella scheda [Network]:
![]() |
- In [1], possiamo vedere che l'intestazione HTTP [Access-Control-Allow-Origin: *] è ora presente nella risposta del server. Eppure continua a non funzionare. Esaminiamo i log della console in [2]. Lì troviamo il seguente log:
XMLHttpRequest cannot load http://localhost:8080/getAllMedecins. Request header field Authorization is not allowed by Access-Control-Allow-Headers
Possiamo vedere che il browser si aspetta una nuova intestazione HTTP [Access-Control-Allow-Headers] che gli indichi che abbiamo il permesso di inviare l'intestazione di autenticazione:
Questo potrebbe essere un buon segno. Angular potrebbe aver tentato di inviare la richiesta HTTP GET. Tuttavia, poiché questa richiesta include un'intestazione di autenticazione, sta verificando se il server la accetta.
Modifichiamo il nostro server web / JSON per inviare questa intestazione. La classe [RdvMedecinsCorsController] cambia come segue:
// sending options to the customer
private void sendOptions(HttpServletResponse response) {
if (application.isCORSneeded()) {
// set header CORS
response.addHeader("Access-Control-Allow-Origin", "*");
// we authorize the header [Authorization]
response.addHeader("Access-Control-Allow-Headers", "Authorization");
}
- Le righe 6–7 aggiungono l'intestazione mancante.
Riavviamo il server e richiediamo nuovamente l'elenco dei medici utilizzando il client Angular:
![]() |
Questa volta ha funzionato. I log della console mostrano la risposta ricevuta dal metodo [dao.getData]:
[dao] getData[/getAllMedecins] success réponse : {"data":{"status":0,"data":[{"id":1,"version":1,"titre":"Mme","nom":"PELISSIER","prenom":"Marie"},{"id":2,"version":1,"titre":"Mr","nom":"BROMARD","prenom":"Jacques"},{"id":3,"version":1,"titre":"Mr","nom":"JANDOT","prenom":"Philippe"},{"id":4,"version":1,"titre":"Melle","nom":"JACQUEMOT","prenom":"Justine"}]},"status":200,"config":{"method":"GET","transformRequest":[null],"transformResponse":[null],"timeout":1000,"url":"http://localhost:8080/getAllMedecins","headers":{"Accept":"application/json, text/plain, */*","Authorization":"Basic YWRtaW46YWRtaW4="}},"statusText":"OK"}
Possiamo notare che:
- il server ha restituito un codice di errore [status=200] con il messaggio [statusText=OK]. Questo è il motivo per cui ci troviamo nella funzione [success];
- il server ha restituito un oggetto [data] con due campi:
- [status]: (da non confondere con il codice di errore HTTP [status]). Qui, [status=0] indica che l'URL [/getAllMedecins] è stato elaborato senza errori;
- [data]: che contiene l'elenco JSON dei medici;
Vediamo ora alcuni altri casi interessanti:
Inseriamo credenziali errate [login, password]:
![]() |
Effettuiamo l'accesso come [user / user], che non ha accesso all'applicazione (solo [admin] ha accesso):
![]() |
Questa volta, l'errore non è più [Errore di autenticazione] ma [Accesso negato].
3.7.7. Esempio 7: Elenco dei clienti
Useremo l'applicazione precedente per visualizzare l'elenco dei client in un menu a tendina di tipo [Bootstrap select] (vedi sezione 3.6.6).
3.7.7.1. Vista V
La vista iniziale sarà la seguente:
![]() |
Per ottenere la vista V, copiamo il codice da [app-16.html] in [app-17.html] e lo modifichiamo come segue:
<div class="container" >
<h1>Rdvmedecins - v1</h1>
<!-- the waiting message -->
<div class="alert alert-warning" ng-show="waiting.visible" >
...
</div>
<!-- the request -->
<div class="alert alert-info" ng-hide="waiting.visible" >
...
<button class="btn btn-primary" ng-click="execute()">{{clients.title|translate}}</button>
</div>
<!-- customer list -->
<div class="row" style="margin-top: 20px" ng-show="clients.show">
<div class="col-md-3">
<h2 translate="{{clients.title}}"></h2>
<select data-style="btn-primary" class="selectpicker">
<option ng-repeat="client in clients.data" value="{{client.id}}">
{{client.titre}} {{client.prenom}} {{client.nom}}
</option>
</select>
</div>
</div>
<!-- the error list -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
</div>
....
<script type="text/javascript" src="rdvmedecins-05.js"></script>
- righe 5-7: il banner di caricamento non cambia;
- righe 10-13: il modulo non cambia, tranne l'etichetta del pulsante (riga 12);
- righe 28-30: il banner di errore non cambia;
- righe 16-25: i clienti vengono visualizzati in un elenco a discesa con lo stile del componente [Bootstrap-selectpicker] (attributi data-style e class, riga 19);
- riga 20: la direttiva [ng-repeat] viene utilizzata per generare le varie opzioni nell'elenco a discesa. Si noti che l'etichetta di un'opzione è di tipo [Mme Julienne Tatou] e che il valore dell'opzione è di tipo [100], dove 100 è l'ID del cliente visualizzato;
- riga 34: il codice JavaScript viene spostato in un nuovo file [rdvmedecins-05];
3.7.7.2. Il controller C e il modello M
Il codice JavaScript nel file [rdvmedecins-05] viene copiato dal file [rdvmedecins-04]:

Non è cambiato quasi nulla, tranne che nel controller, che ora è adattato per fornire l'elenco dei clienti:
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate',
function ($scope, utils, config, dao, $translate) {
// ------------------- model initialization
// model
$scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: undefined};
$scope.waitingTimeText = config.waitingTimeText;
$scope.server = {url: undefined, login: undefined, password: undefined};
$scope.clients = {title: config.listClients, show: false, model: {}};
$scope.errors = {show: false, model: {}};
$scope.urlServerLabel = config.urlServerLabel;
$scope.loginLabel = config.loginLabel;
$scope.passwordLabel = config.passwordLabel;
// asynchronous task
var task;
// execution action
$scope.execute = function () {
// the UI is updated
$scope.waiting.visible = true;
$scope.clients.show = false;
$scope.errors.show = false;
// simulated waiting
task = utils.waitForSomeTime($scope.waiting.time);
var promise = task.promise;
// waiting
promise = promise.then(function () {
// we ask for the customer list;
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrClients);
return task.promise;
});
// analyze the result of the previous call
promise.then(function (result) {
// result={err: 0, data: [client1, client2, ...]}
// result={err: n, messages: [msg1, msg2, ...]}
if (result.err == 0) {
// we put the acquired data into the model
$scope.clients.data = result.data;
// the UI is updated
$scope.clients.show = true;
$scope.waiting.visible = false;
// style the drop-down list
$('.selectpicker').selectpicker();
} else {
// there were errors in obtaining the customer list
$scope.errors = { title: config.getClientsErrors, messages: utils.getErrors(result), show: true, model: {}};
// the UI is updated
$scope.waiting.visible = false;
}
});
};
// cancel wait
function cancel() {
// complete the task
task.reject();
// the UI is updated
$scope.waiting.visible = false;
$scope.clients.show = false;
$scope.errors.show = false;
}
}
])
;
- Nel controller sono state apportate pochissime modifiche. In precedenza forniva un elenco di medici. Ora fornisce un elenco di clienti;
- riga 9: [$scope.clients] sarà il modello per il banner dei clienti nella vista V;
- riga 30: ora viene utilizzato l'URL [/getAllClients];
- righe 35–36: i due formati di risposta restituiti dal metodo [dao.getData]. Ora abbiamo i clienti al posto dei medici;
- riga 44: un'istruzione piuttosto rara nel codice Angular. Stiamo manipolando direttamente il DOM (Document Object Model). Qui, vogliamo applicare il metodo [selectpicker] (parte di [bootstrap-select.min.js]) agli elementi DOM che hanno la classe [selectpicker] [$('.selectpicker)']. Ce n'è solo uno: l'elenco a discesa:
<select data-style="btn-primary" class="selectpicker" select-enable="">
....
</select>
Nella sezione 3.6.6 abbiamo visto che questo ha applicato lo stile seguente all'elenco a discesa:
![]() | ![]() |
Come fatto per i medici, dobbiamo anche modificare il servizio web.
3.7.7.3. Modifica del servizio web - 1
![]() |
La classe [RdvMedecinsController] è stata arricchita con un nuovo metodo:
package rdvmedecins.web.controllers;
...
@Controller
public class RdvMedecinsCorsController {
@Autowired
private ApplicationModel application;
// sending options to the customer
private void sendOptions(HttpServletResponse response) {
if (application.isCORSneeded()) {
// set header CORS
response.addHeader("Access-Control-Allow-Origin", "*");
// we authorize the header [Authorization]
response.addHeader("Access-Control-Allow-Headers", "Authorization");
}
}
// list of doctors
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.OPTIONS)
public void getAllMedecins(HttpServletResponse response) {
sendOptions(response);
}
// customer list
@RequestMapping(value = "/getAllClients", method = RequestMethod.OPTIONS)
public void getAllClients(HttpServletResponse response) {
sendOptions(response);
}
}
- Righe 29–32: Il metodo [getAllClients] gestirà la richiesta HTTP [OPTIONS] inviata dal browser;
3.7.7.4. Test dell'applicazione – 1
Ora siamo pronti per il test. Avviamo il server web e inseriamo valori validi nel modulo Angular. Otteniamo la seguente risposta:

Questo messaggio di errore viene visualizzato quando Angular non è riuscito a effettuare la richiesta HTTP richiesta. Dobbiamo quindi cercare le cause nei log della console. Lì troviamo il seguente messaggio:
XMLHttpRequest cannot load http://localhost:8080/getAllClients. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:63342' is therefore not allowed access.
Un problema che pensavamo fosse risolto. Diamo ora un'occhiata al traffico di rete che si è verificato:

Vediamo che l'operazione [getAllClients] che utilizza il metodo HTTP [OPTIONS] ha avuto esito positivo, mentre l'operazione [getAllClients] che utilizza il metodo HTTP [GET] è stata annullata. La risposta alla richiesta [OPTIONS] era la seguente:

Le intestazioni HTTP CORS sono effettivamente presenti. Esaminiamo ora gli scambi HTTP durante la richiesta GET:

La richiesta HTTP sembra essere corretta. In particolare, possiamo vedere l'intestazione di autenticazione.
Oltre al messaggio di errore precedente, nei log della console compare il seguente messaggio:
[dao] getData[/getAllClients] error réponse : {"data":"","status":0,"config":{"method":"GET","transformRequest":[null],"transformResponse":[null],"timeout":1000,"url":"http://localhost:8080/getAllClients","headers":{"Accept":"application/json, text/plain, */*","Authorization":"Basic YWRtaW46YWRtaW4="}},"statusText":""}
Questo è il log che il metodo [dao.getData] genera sistematicamente al ricevimento della risposta alla sua richiesta HTTP. Due cose saltano all'occhio:
- [status=0]: significa che Angular ha annullato la richiesta HTTP;
- [method=GET]: ed è stata la richiesta GET ad essere annullata;
Se combinato con il primo messaggio, ciò significa che Angular si aspetta anche le intestazioni CORS per la richiesta GET. Tuttavia, attualmente, il nostro servizio web le invia solo per la richiesta HTTP [OPTIONS]. È molto strano riscontrare questo errore ora e non per l'elenco dei medici. Non ho una spiegazione.
Dobbiamo quindi modificare nuovamente il servizio web.
3.7.7.5. Modifica del servizio web – 2
![]() |
I metodi [GET] e [POST] sono gestiti nella classe [RdvMedecinsController]. Dobbiamo modificarla in modo che questi metodi inviino le intestazioni CORS. Procediamo come segue:
@RestController
public class RdvMedecinsController {
@Autowired
private ApplicationModel application;
@Autowired
private RdvMedecinsCorsController rdvMedecinsCorsController;
...
// customer list
@RequestMapping(value = "/getAllClients", method = RequestMethod.GET)
public Reponse getAllClients(HttpServletResponse response) {
// headers CORS
rdvMedecinsCorsController.getAllClients(response);
// application status
if (messages != null) {
return new Reponse(-1, messages);
}
// customer list
try {
return new Reponse(0, application.getAllClients());
} catch (Exception e) {
return new Reponse(1, Static.getErreursForException(e));
}
}
...
- riga 8: vogliamo riutilizzare il codice che abbiamo inserito nel controller [RdvMedecinsCorsController]. Quindi lo inseriamo qui;
- riga 14: il metodo che gestisce la richiesta [GET /getAllClients]. Apportiamo due modifiche:
- riga 14: iniettiamo l'oggetto [HttpServletResponse] nei parametri del metodo,
- riga 16: utilizziamo i metodi della classe [RdvMedecinsCorsController] per impostare le intestazioni CORS in questo oggetto;
3.7.7.6. Test dell'applicazione – 2
Lanciamo la nuova versione del servizio web e richiediamo nuovamente l'elenco dei clienti. Otteniamo la seguente risposta:
![]() |
- in [1], otteniamo una risposta, ma è vuota [2];
- in [3]: gli scambi di rete sono avvenuti senza intoppi;
Nei log della console, il metodo [dao.getData] ha visualizzato la risposta ricevuta:
[dao] getData[/getAllClients] success réponse : {"data":{"status":0,"data":[{"id":1,"version":1,"titre":"Mr","nom":"MARTIN","prenom":"Jules"},{"id":2,"version":1,"titre":"Mme","nom":"GERMAN","prenom":"Christine"},{"id":3,"version":1,"titre":"Mr","nom":"JACQUARD","prenom":"Jules"},{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"}]},"status":200,"config":{"method":"GET","transformRequest":[null],"transformResponse":[null],"timeout":1000,"url":"http://localhost:8080/getAllClients","headers":{"Accept":"application/json, text/plain, */*","Authorization":"Basic YWRtaW46YWRtaW4="}},"statusText":"OK"}
Quindi il metodo ha effettivamente ricevuto l'elenco dei clienti. Una volta verificato il codice, iniziamo a sospettare che il problema risieda nella seguente istruzione, che non comprendiamo appieno:
// on style la liste déroulante
$('.selectpicker').selectpicker();
Commentiamo la riga 2 e riproviamo. Otteniamo quindi la seguente risposta:
![]() |
Abbiamo quindi individuato il problema. È l'applicazione del metodo [selectpicker] all'elenco a discesa che sta causando il problema. Quando esaminiamo il codice sorgente della pagina con l'errore, vediamo quanto segue:
![]() |
- scopriamo che in [1] l'elenco a discesa è effettivamente presente con i suoi elementi, ma non viene visualizzato [style='display:none'];
- In [2], viene visualizzato il pulsante [bootstrap select]. Gli elementi dell'elenco a discesa dovrebbero apparire nell'elenco <ul role='menu'>. Non ci sono, quindi abbiamo un elenco vuoto. Sembra che quando il metodo [selectpicker] è stato applicato all'elenco a discesa, il suo contenuto fosse vuoto in quel momento;
Cercando una soluzione sul web, abbiamo trovato questa. Sostituiamo il codice:
// on style la liste déroulante
$('.selectpicker').selectpicker();
con quanto segue:
// on style la liste déroulante
$timeout(function(){
$('.selectpicker').selectpicker();
});
Lo stile [bootstrap-select] viene applicato tramite una funzione [$timeout]. Abbiamo già incontrato questa funzione, che consente l'esecuzione di una funzione dopo un certo ritardo. In questo caso, l'assenza di un ritardo significa un ritardo pari a zero. Le righe precedenti inseriscono un evento nella coda degli eventi del browser. Quando l'evento corrente (clic sul pulsante [Client List]) termina l'elaborazione, verrà visualizzata la vista V. Subito dopo, il browser controllerà il proprio elenco di eventi. A causa del ritardo pari a zero, l'evento [$timeout] si troverà in cima all'elenco e verrà elaborato. Lo stile [bootstrap-select] viene quindi applicato a un elenco a discesa popolato. Vediamo il risultato:
![]() |
Se guardiamo di nuovo al codice sorgente della pagina visualizzata, vediamo quanto segue:
![]() |
Il pulsante [bootstrap-select], che prima era vuoto, ora contiene l'elenco dei clienti.
3.7.7.7. Utilizzo di una direttiva
Nel controller C della vista V abbiamo trovato il seguente codice:
// on style la liste déroulante
$('.selectpicker').selectpicker();
Stiamo manipolando un oggetto DOM. Molti sviluppatori Angular sono restii a manipolare il DOM all'interno del codice del controller. Per loro, questa operazione dovrebbe essere eseguita in una direttiva. Una direttiva Angular può essere vista come un'estensione del linguaggio HTML. Ciò rende possibile la creazione di nuovi elementi o attributi HTML. Vediamo un primo esempio:
Creiamo il seguente file JS [selectEnable]:
angular.module("rdvmedecins").directive('selectEnable', ['$timeout', function ($timeout) {
return {
link: function (scope, element, attrs) {
$timeout(function () {
var selectpicker = $('.selectpicker');
selectpicker.selectpicker();
});
}
};
}]);
- La direttiva segue la sintassi del controller che ormai conosciamo bene:
angular.module("rdvmedecins").directive('selectEnable', ['$timeout', function ($timeout)
La direttiva appartiene al modulo [rvmedecins]. È una funzione che accetta due parametri:
- (continua)
- il primo è il nome della direttiva [selectEnable];
- il secondo è un array ['obj1','obj2',..., function(obj1, obj2,...)] dove gli [obj] sono gli oggetti da iniettare nella funzione. Qui, l'unico oggetto iniettato è l'oggetto predefinito [$timeout];
- la funzione [directive] restituisce un oggetto che può avere vari attributi. Qui, l'unico attributo è l'attributo [link] (riga 3). Il suo valore qui è una funzione che accetta tre parametri:
- scope: il modello della vista in cui viene utilizzata la direttiva;
- element: l'elemento della vista, il target della direttiva;
- attrs: gli attributi di questo elemento;
Vediamo un esempio. La direttiva [selectEnable] potrebbe essere utilizzata nel seguente contesto:
Nell'esempio sopra riportato, l'attributo [select-enable] applica la direttiva [selectEnable] all'elemento HTML <div>. Una direttiva [doSomething] può essere applicata a qualsiasi elemento HTML aggiungendovi l'attributo [do-something]. Si noti la differenza ortografica tra il nome della direttiva e il relativo attributo. Si passa da [camelCase] a [camel-case].
La direttiva [selectEnable] potrebbe anche essere utilizzata come segue:
In questo caso, la direttiva [doSomething] viene applicata sotto forma di tag HTML <do-something>.
Torniamo alla sintassi
e ai tre parametri della funzione [link] della direttiva, [scope, element, attrs]:
- scope: è il modello della vista in cui si trova il <div>;
- element: è il tag <div> stesso;
- attrs: è l'array degli attributi per il <div>. Questi possono essere utilizzati per passare informazioni alla direttiva. Nell'esempio sopra, scriviamo attrs['selectEnable'] per recuperare le informazioni [data]. Notare il cambiamento nella notazione [selectEnable] per riferirsi all'attributo [select-enable];
Torniamo al codice della direttiva:
angular.module("rdvmedecins").directive('selectEnable', ['$timeout', function ($timeout) {
return {
link: function (scope, element, attrs) {
$timeout(function () {
$('.selectpicker').selectpicker();
});
}
};
}]);
- Righe 14–16: Qui vediamo il codice che abbiamo inserito in precedenza nel controller. Questo codice viene eseguito quando si incontra la direttiva [select-enable] (come elemento o attributo) durante il rendering della vista V.
Per implementare questa direttiva, copiamo il file [app-17.html] in [app-17B.html] e lo modifichiamo come segue:
<select data-style="btn-primary" class="selectpicker" select-enable="">
<option ng-repeat="client in clients.data" value="{{client.id}}">
{{client.titre}} {{client.prenom}} {{client.nom}}
</option>
</select>
- riga 1: applichiamo la direttiva [selectEnable] all'elemento HTML [select]. Poiché non ci sono informazioni da passare alla direttiva, scriviamo semplicemente [select-enable=""] ;
Modifichiamo anche il controller duplicando il file JS [rdvmedecins-05.js] in [rdvmedecins-05B.js] e facciamo riferimento al nuovo file JS nel file [app-17B.html] e nel file della direttiva [selectEnable.js]. Non dimenticare quest'ultimo punto. Se il file della direttiva manca, l'attributo [select-enable=""] non verrà gestito, ma Angular non segnalerà alcun errore.
<script type="text/javascript" src="rdvmedecins-05B.js"></script>
<script type="text/javascript" src="selectEnable.js"></script>
Nel file JS [rdvmedecins-05B.js], rimuoviamo le seguenti righe dal controller:
// on style la liste déroulante
$timeout(function(){
$('.selectpicker').selectpicker();
});
Questa operazione è ora gestita dalla direttiva.
3.7.7.8. Test dell'applicazione – 3
Durante il test della nuova applicazione [app-17B.html], si ottiene il seguente risultato:
![]() |
- In [1], si ottiene un elenco vuoto.
I log della console visualizzano quanto segue:
- riga 1: inizializzazione del servizio [dao];
- riga 2: alla visualizzazione iniziale della vista V, viene eseguita la direttiva [selectEnable];
- riga 3: questa riga appare quando l'utente fa clic sul pulsante [Client List]. Possiamo vedere che la direttiva [selectEnable] non viene eseguita una seconda volta. In definitiva, è stata eseguita quando l'elenco dei clienti era vuoto, quindi abbiamo un elenco a discesa vuoto;
In altre parole, illustra il funzionamento:
$('.selectpicker').selectpicker();
non si è verificata al momento giusto. Possiamo provare a risolvere il problema in vari modi. Dopo numerosi test senza successo, ci rendiamo conto che l'operazione sopra indicata deve verificarsi una sola volta e solo quando l'elenco a discesa è stato popolato. Per ottenere questo risultato, riscriviamo il tag <select> come segue:
<select data-style="btn-primary" class="selectpicker" select-enable="" ng-if="clients.data">
<option ng-repeat="client in clients.data" value="{{client.id}}">
{{client.titre}} {{client.prenom}} {{client.nom}}
</option>
</select>
Riga 1: Il tag <select> viene generato solo se [clients.data] esiste. Questo non è il caso quando la vista V viene inizialmente visualizzata. Pertanto, il tag <select> non verrà generato e la direttiva [selectEnable] non verrà valutata. Quando l'utente fa clic sul pulsante [Elenco clienti], [clients.data] avrà un nuovo valore nel modello M. Poiché il modello M è cambiato, il tag <select> verrà rivalutato e generato in questo punto. Verrà quindi valutata anche la direttiva [selectEnable]. Al momento della valutazione, le righe 2-4 del tag <select> non sono ancora state valutate. Abbiamo quindi un elenco vuoto di clienti. Se scriviamo la direttiva [selectEnable] come segue:
angular.module("rdvmedecins").directive('selectEnable', ['$timeout', 'utils', function ($timeout, utils) {
return {
link: function (scope, element, attrs) {
utils.debug("directive selectEnable");
$('.selectpicker').selectpicker();
}
}
}]);
La riga 5 verrà eseguita con un elenco vuoto e vedremo quindi un elenco a discesa vuoto sullo schermo. Dobbiamo quindi scrivere:
angular.module("rdvmedecins").directive('selectEnable', ['$timeout', 'utils', function ($timeout, utils) {
return {
link: function (scope, element, attrs) {
utils.debug("directive selectEnable");
$timeout(function () {
$('.selectpicker').selectpicker();
})
}
}
}]);
per ottenere il risultato previsto. A causa del [$timeout] nella riga 5, la riga 6 verrà eseguita solo dopo che la vista V sarà stata completamente renderizzata, ovvero nel momento in cui il tag <select> avrà tutti i suoi elementi.
3.7.8. Esempio 8: L'agenda di un medico
Presentiamo ora un'applicazione che visualizza l'orario di un medico.
3.7.8.1. La vista V dell’applicazione
Presenteremo il seguente modulo:
![]() |
- in [1], richiediamo l'agenda della sig.ra PELISSIER [2] per il 25 giugno 2014 [3];
Si ottiene il seguente risultato [4]:
![]() |
Esamineremo le due visioni separatamente.
3.7.8.2. Il modulo
Duplichiamo il file [app-17.html] in [app-18.html] e poi modifichiamo il codice come segue:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- the waiting message -->
<div class="alert alert-warning" ng-show="waiting.visible">
...
</div>
<!-- the request -->
<div class="alert alert-info" ng-hide="waiting.visible">
<div class="row" style="margin-bottom: 20px">
<div class="col-md-3">
<h2 translate="{{medecins.title}}"></h2>
<select data-style="btn-primary" class="selectpicker">
<option ng-repeat="medecin in medecins.data" value="{{medecin.id}}">
{{medecin.titre}} {{medecin.prenom}} {{medecin.nom}}
</option>
</select>
</div>
<div class="col-md-3">
<h2 translate="{{calendar.title}}"></h2>
<div style="display:inline-block; min-height:290px;">
<datepicker ng-model="calendar.jour" min-date="calendar.minDate" show-weeks="true"
class="well well-sm"></datepicker>
</div>
</div>
</div>
<button class="btn btn-primary" ng-click="execute()">{{agenda.title|translate}}</button>
</div>
<!-- the error list -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
<!-- the diary -->
<div id="agenda" ng-show="agenda.show">
...
</div>
</div>
...
<script type="text/javascript" src="rdvmedecins-06.js"></script>
- righe 5-7: il messaggio di caricamento non cambia;
- righe 12-19: l'elenco dei medici che utilizzano il componente [bootstrap select];
- righe 20-26: il calendario [ui-bootstrap] che abbiamo già presentato. Si noti che il giorno selezionato viene inserito nel modello [calendar.day] (attributo ng-model);
- riga 28: il pulsante che richiama il calendario;
- righe 32–34: l'elenco degli errori rimane invariato;
- righe 37–39: il calendario, che presenteremo più avanti;
- riga 42: il codice JS viene trasferito nel file [rdvmedecins-06.js] copiando il file [rdvmedecins-05.js];
3.7.8.3. Il controller C
Il codice JS dell'applicazione diventa il seguente:

Solo il servizio [utils] e il controller [rdvMedecinsCtrl] saranno interessati dalle modifiche.
Il controller [rdvMedecinsCtrl] diventa il seguente:
// controller
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate', '$timeout', '$filter', '$locale',
function ($scope, utils, config, dao, $translate, $timeout, $filter, $locale) {
// ------------------- model initialization
// model
$scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: 3000};
$scope.server = {url: 'http://localhost:8080', login: 'admin', password: 'admin'};
$scope.errors = {show: false, model: {}};
$scope.medecins = {
data: [
{id: 1, version: 1, titre: "Mme", nom: "PELISSIER", prenom: "Marie"},
{id: 2, version: 1, titre: "Mr", nom: "BROMARD", prenom: "Jacques"},
{id: 3, version: 1, titre: "Mr", nom: "JANDOT", prenom: "Philippe"},
{id: 4, version: 1, titre: "Melle", nom: "JACQUEMOT", prenom: "Justine"}
],
title: config.listMedecins};
$scope.agenda = {title: config.getAgendaTitle, data: undefined, show: false};
$scope.calendar = {title: config.getCalendarTitle, minDate: new Date(), jour: new Date()};
// style the drop-down list
$timeout(function () {
$('.selectpicker').selectpicker();
});
// for the French local calendar
angular.copy(config.locales['fr'], $locale);
...
}
])
;
- riga 7: impostiamo un timeout di 3 secondi prima di effettuare la richiesta HTTP;
- riga 8: gli elementi necessari per la connessione HTTP sono hard-coded;
- righe 10–17: l'elenco dei medici è hard-coded;
- riga 18: il modello [agenda] configura la visualizzazione del calendario nella vista;
- riga 19: il modello [calendar] configura la visualizzazione del calendario nella vista. Impostiamo la data minima [minDate] su oggi e anche la data corrente su oggi;
- righe 21–23: lo stile dell'elenco a discesa viene definito utilizzando il metodo visto in precedenza;
- riga 25: impostiamo la lingua dell'applicazione su 'fr'. Per impostazione predefinita, è 'en';
Il metodo eseguito quando viene richiesto il calendario è il seguente:
// exécution action
$scope.execute = function () {
// les infos du formulaire
var idMedecin = $('.selectpicker').selectpicker('val');
// vérification
utils.debug("[homeCtrl] idMedecin", idMedecin);
utils.debug("[homeCtrl] jour", $scope.calendar.jour);
// on met le jour au format yyyy-MM-dd
var formattedJour = $filter('date')($scope.calendar.jour, 'yyyy-MM-dd');
// mise à jour de la vue
$scope.waiting.visible = true;
$scope.errors.show = false;
$scope.agenda.show = false;
...
};
- Riga 4: Recuperiamo l'attributo [value] del medico selezionato. Anche in questo caso utilizziamo il metodo [selectpicker] dal file [bootstrap-select.min.js]. Ricordate il formato delle opzioni dell'elenco a discesa:
<option ng-repeat="medecin in medecins.data" value="{{medecin.id}}">
{{medecin.titre}} {{medecin.prenom}} {{medecin.nom}}
Il valore (attributo value) dell'opzione è quindi l'[id] del medico.
- riga 11: formattiamo la data selezionata dall'utente come [yyyy-mm-dd], che è il formato di data previsto dal server web;
- righe 13-15: al termine del metodo [execute], verrà visualizzato il banner di caricamento e tutto il resto verrà nascosto;
Il codice prosegue come segue:
// simulated waiting
var task = utils.waitForSomeTime($scope.waiting.time);
// we ask for the doctor's diary
var promise = task.promise.then(function () {
// the URL service path
var path = config.urlSvrAgenda + "/" + idMedecin + "/" + formattedJour;
// we ask for the agenda
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path);
// we return the promise of task completion
return task.promise;
});
// we analyze the result of the call to service [dao]
promise.then(function (result) {
// end of wait
$scope.waiting.visible = false;
// mistake?
if (result.err == 0) {
// we prepare the agenda model
$scope.agenda.data = result.data;
$scope.agenda.show = true;
// timetable display formatting
angular.forEach($scope.agenda.data.creneauxMedecin, function (creneauMedecin) {
creneauMedecin.creneau.text = utils.getTextForCreneau(creneauMedecin.creneau);
});
// we create an evt to style the table after the view is displayed
$timeout(function () {
$("#creneaux").footable();
});
} else {
// mistakes were made in obtaining the agenda
$scope.errors = {
title: config.getAgendaErrors,
messages: utils.getErrors(result),
show: true
};
}
- riga 2: l'attività asincrona che rimane in attesa per 3 secondi;
- righe 5–10: il codice che verrà eseguito al termine dell'attesa;
- riga 6: viene costruito l'URL richiesto [/getAgendaMedecinJour/1/2014-06-25];
- riga 8: l'URL viene interrogato. Viene avviata un'attività asincrona;
- Riga 10: rendiamo questa attività asincrona;
- righe 14–38: il codice che verrà eseguito una volta che la richiesta HTTP avrà restituito la sua risposta;
- riga 13: [result] è la risposta inviata dal metodo [dao.getData]. Qui, dobbiamo ricordare il formato della risposta del server web:
![]() |
Il parametro [result.data] alla riga 19 è l'attributo [data] [1] menzionato sopra. Questo attributo, a sua volta, contiene l'attributo [creneauxMedecin] [2] menzionato sopra. Si tratta di un array di fasce orarie, ciascuna contenente due informazioni:
- [rv]: la rappresentazione JSON di un appuntamento, oppure [null] se non è stato programmato alcun appuntamento per quella fascia oraria;
- [hDeb, mDeb, hFin, mFin]: le informazioni temporali relative alla fascia oraria;
Torniamo al codice del controller:
- riga 15: l'attesa è finita;
- riga 19: popoliamo il modello [$scope.agenda], che controlla la visualizzazione del calendario;
- riga 20: il calendario viene reso visibile;
- righe 22–24: iteriamo attraverso ciascun elemento C nell'array [creneauxMedecin] di cui abbiamo appena parlato;
- riga 23: ogni elemento C ha un attributo [slot] che rappresenta la fascia oraria. Questo è arricchito da un attributo [text] che sarà la rappresentazione testuale della fascia oraria nel formato [10:20–10:40];
- righe 26–28: rendiamo reattiva la tabella HTML utilizzata per visualizzare le fasce orarie del calendario. Abbiamo trattato questo concetto nella sezione 3.6.7;
![]() |
- riga 27: per rendere la tabella responsive, dobbiamo applicarle il metodo [footable]. Qui incontriamo la stessa difficoltà che abbiamo avuto con il componente [bootstrap-select]. Se scriviamo semplicemente la riga 17, vediamo che la tabella non è responsive. Risolviamo questo problema allo stesso modo utilizzando la funzione [$timeout] (riga 26);
- righe 31–34: il caso in cui la richiesta HTTP non va a buon fine. Vengono quindi visualizzati dei messaggi di errore;
3.7.8.4. Visualizzazione del calendario
Torniamo ora al codice del calendario nel file [app-18.html]. È il seguente:
<!-- the diary -->
<div id="agenda" ng-show="agenda.show">
<!-- case of a doctor without consultation slots -->
<h4 class="alert alert-danger" ng-if="agenda.data.creneauxMedecin.length==0"
translate="agenda_medecinsanscreneaux"></h4>
<!-- doctor's diary -->
<div class="row tab-content alert alert-warning" ng-if="agenda.data.creneauxMedecin.length!=0">
<div class="tab-pane active col-md-6">
<table creneaux-table id="creneaux" class="table">
<thead>
<tr>
<th data-toggle="true">
<span translate="agenda_creneauhoraire"></span>
</th>
<th>
<span translate="agenda_client">Client</span>
</th>
<th data-hide="phone">
<span translate="agenda_action">Action</span>
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="creneauMedecin in agenda.data.creneauxMedecin">
<td>
<span
ng-class="! creneauMedecin.rv ? 'status-metro status-active' : 'status-metro status-suspended'">
{{creneauMedecin.creneau.text}}
</span>
</td>
<td>
<span>{{creneauMedecin.rv.client.titre}} {{creneauMedecin.rv.client.prenom}} {{creneauMedecin.rv.client.nom}}</span>
</td>
<td>
<a href="" ng-if="!creneauMedecin.rv" translate="agenda_reserver" class="status-metro status-active">
</a>
<a href="" ng-if="creneauMedecin.rv" translate="agenda_supprimer" class="status-metro status-suspended">
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
- Righe 4-5: Ricordiamo che [agenda.data] è il calendario e che [agenda.data.creneauxMedecin] è un array di oggetti di tipo [creneauMedecin]. Ogni elemento di questo tipo ha un attributo [creneauMedecin.creneau] che rappresenta una fascia oraria. Ogni fascia oraria presenta due elementi di nostro interesse:
- [doctorSlot.slot.appointment], che è l'appuntamento (se presente; appointment ≠ null) programmato per lo slot;
- [doctorSlot.slot.text], che è il testo [inizio:fine] della fascia oraria;
- riga 4: visualizza un messaggio speciale se il medico non ha intervalli di tempo. È improbabile, ma risulta che il nostro database sia incompleto e questo scenario si verifichi effettivamente. Il fatto che il messaggio venga visualizzato in HTML o meno è controllato dalla direttiva [ng-if];

La direttiva [ng-if] è diversa dalle direttive [ng-show, ng-hide]. Queste ultime nascondono semplicemente un'area presente nel documento. Se [ng-if='false'], l'area viene rimossa dal documento. L'abbiamo utilizzata qui a scopo illustrativo;
- Riga 9: L'attributo [id='creneaux'] è importante. Viene utilizzato nella seguente istruzione:
$("#creneaux").footable();
- righe 10–22: visualizzano le intestazioni della tabella [1];
- righe 23–45: visualizzano il contenuto della tabella [2];
![]() |
- riga 24: si esegue un'iterazione sull'array [agenda.data.creneauxMedecin];
- righe 26–29: il testo viene visualizzato [3]. La direttiva [ng-class] viene utilizzata per generare l'attributo [class] dell'elemento. Qui, se [creneauMedecin.rv == null], significa che lo slot è disponibile e al testo viene assegnato uno sfondo verde. Altrimenti, gli viene assegnato uno sfondo rosso;
- riga 32: scriviamo il nome del cliente per il quale è stato fissato l'appuntamento [4]. Se [rv==null], questa informazione non esiste, ma Angular gestisce correttamente questo caso e non genera un errore;
- Righe 34–39: visualizza uno dei due pulsanti: [Prenota] o [Elimina]. L'esistenza o meno di un appuntamento determina quale pulsante viene selezionato;
3.7.8.5. Modifica del server web
Come negli esempi precedenti, il server web deve essere modificato in modo che l'URL [/getAgendaMedecinJour] invii le intestazioni CORS:
![]() |
Nella classe [RdvMedecinsCorsController], aggiungere un nuovo metodo:
// doctor's diary
@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.OPTIONS)
public void getAgendaMedecinJour(HttpServletResponse response) {
sendOptions(response);
}
Questo metodo invierà le intestazioni CORS per la richiesta HTTP [OPTIONS]. Dobbiamo fare lo stesso per la richiesta HTTP [GET] nella classe [RdvMedecinsController]:
@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
public Reponse getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour, HttpServletResponse response) {
// headers CORS
rdvMedecinsCorsController.getAgendaMedecinJour(response);
...
}
3.7.8.6. Utilizzo delle direttive
Come abbiamo fatto in precedenza, sposteremo la manipolazione del DOM nelle direttive. Abbiamo due operazioni di manipolazione del DOM:
- quando la vista viene visualizzata inizialmente:
// on style la liste déroulante
$timeout(function () {
$('.selectpicker').selectpicker();
});
- Quando viene visualizzato il calendario:
// we create an evt to style the table after the view is displayed
$timeout(function () {
$("#creneaux").footable();
});
Per il primo caso, useremo la direttiva [selectEnable] già presentata. Per il secondo caso, creiamo la direttiva [ footable] nel seguente file JS [footable.js]:
angular.module("rdvmedecins").directive('footable', ['$timeout', 'utils', function ($timeout, utils) {
return {
link: function (scope, element, attrs) {
utils.debug("directive footable");
$timeout(function () {
$("#creneaux").footable();
})
}
}
}]);
Utilizziamo quindi la stessa tecnica della direttiva [selectEnable].
Il codice HTML [app-18.html] viene duplicato in [app-18B.html]. Quindi lo modifichiamo come segue:
<select data-style="btn-primary" class="selectpicker" select-enable="">
<option ng-repeat="medecin in medecins.data" value="{{medecin.id}}">
{{medecin.titre}} {{medecin.prenom}} {{medecin.nom}}
</option>
</select>
- Riga 1: applicare la direttiva [selectEnable] (tramite l'attributo [select-enable]) al tag <select> per i medici;
<div class="row tab-content alert alert-warning" ng-if="agenda.data.creneauxMedecin.length!=0">
<div class="tab-pane active col-md-6">
<table id="creneaux" class="table" footable="">
<thead>
<tr>
- riga 3: la direttiva [footable] (tramite l'attributo [footable]) viene applicata alla tabella HTML del calendario;
<script type="text/javascript" src="rdvmedecins-06B.js"></script>
<!-- directives -->
<script type="text/javascript" src="selectEnable.js"></script>
<script type="text/javascript" src="footable.js"></script>
- righe 3-4: fanno riferimento ai file JS per entrambe le direttive;
- riga 1: il codice JS da [app-18B.html] è il codice JS da [app-18.html] duplicato nel file [rdvmedecins-06B.js];
Il file [rdvmedecins-06B.js] è identico al file [rdvmedecins-06.js] tranne che per due dettagli. Le righe che manipolano il DOM sono state rimosse:
// on style la liste déroulante
$timeout(function () {
$('.selectpicker').selectpicker();
});
// we create an evt to style the table after the view is displayed
$timeout(function () {
$("#creneaux").footable();
});
Una volta fatto ciò, l'esecuzione dell'applicazione [app-18B.html] produce gli stessi risultati dell'esecuzione di [app-18.html].
3.7.9. Esempio 9: Creazione e cancellazione di prenotazioni
Presentiamo ora un'applicazione che consente di creare e cancellare prenotazioni.
3.7.9.1. Vista V dell'applicazione
Presenteremo il seguente modulo:
![]() |
- In [1] è possibile effettuare una prenotazione. La prenotazione verrà effettuata per un cliente casuale;
- In [2], è possibile cancellare le prenotazioni effettuate;
Duplichiamo il file [app-18.html] come [app-19.html], quindi modifichiamo il codice come segue:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- the waiting message -->
<div class="alert alert-warning" ng-show="waiting.visible">
...
</div>
<!-- the error list -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
<!-- the diary -->
<div id="agenda" ng-show="agenda.show">
..
<!-- doctor's diary -->
<div class="row tab-content alert alert-warning" ng-if="agenda.data.creneauxMedecin.length!=0">
<div class="tab-pane active col-md-6">
<table id="creneaux" class="table" footable="">
...
<tbody>
<tr ng-repeat="creneauMedecin in agenda.data.creneauxMedecin">
...
<td>
<a href="" ng-if="!creneauMedecin.rv" translate="agenda_reserver" class="status-metro status-active" ng-click="reserver(creneauMedecin.creneau.id)">
</a>
<a href="" ng-if="creneauMedecin.rv" translate="agenda_supprimer" class="status-metro status-suspended" ng-click="supprimer(creneauMedecin.rv.id)">
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
....
<script type="text/javascript" src="rdvmedecins-07.js"></script>
<script type="text/javascript" src="footable.js"></script>
- righe 5-7: il messaggio di caricamento è lo stesso della versione precedente;
- righe 10-12: il messaggio di errore è lo stesso della versione precedente;
- righe 15-36: il calendario è lo stesso della versione precedente, con due eccezioni:
- riga 26: il clic sul pulsante [prenota] (attributo ng-click) viene gestito dal metodo [reserve] del modello M nella vista V. Viene passato il numero della fascia oraria di prenotazione;
- riga 26: cliccando sul pulsante [delete] viene gestito dal metodo [reserve] del modello M nella vista V. Viene passato il numero dell'appuntamento da cancellare;
- riga 39: il codice JavaScript che gestisce l'applicazione si trova nel file [rdvmedecins-07.js];
- riga 40: il codice JS per la direttiva [footable] applicata alla riga 20;
3.7.9.2. Il controller C
Il codice JavaScript per [rdvmedecins-07.js] viene prima creato copiando il file [rdvmedecins-06.js]. Viene poi modificato. I soliti grandi blocchi di codice rimangono. Le modifiche vengono apportate principalmente nel controller:

Descriveremo il controller C per la vista V in diversi passaggi.
3.7.9.3. Inizializzazione del controller C
Il codice di inizializzazione del controller è il seguente:
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate', '$timeout', '$filter', '$locale',
function ($scope, utils, config, dao, $translate, $timeout, $filter, $locale) {
// ------------------- model initialization
// model
$scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: 3000};
$scope.server = {url: 'http://localhost:8080', login: 'admin', password: 'admin'};
$scope.errors = {show: false, model: {}};
$scope.medecins = {
data: [
{id: 1, version: 1, titre: "Mme", nom: "PELISSIER", prenom: "Marie"},
{id: 2, version: 1, titre: "Mr", nom: "BROMARD", prenom: "Jacques"},
{id: 3, version: 1, titre: "Mr", nom: "JANDOT", prenom: "Philippe"},
{id: 4, version: 1, titre: "Melle", nom: "JACQUEMOT", prenom: "Justine"}
],
title: config.listMedecins
};
var médecin = $scope.medecins.data[0];
var clients = [
{id: 1, version: 1, titre: "Mr", nom: "MARTIN", prenom: "Jules"},
{id: 2, version: 1, titre: "Mme", nom: "GERMAN", prenom: "Christine"},
{id: 3, version: 1, titre: "Mr", nom: "JACQUARD", prenom: "Maurice"},
{id: 4, version: 1, titre: "Melle", nom: "BISTROU", prenom: "Brigitte"}
];
// for the date
angular.copy(config.locales['fr'], $locale);
var today = new Date();
var formattedDay = $filter('date')(today, 'yyyy-MM-dd');
var fullDay = $filter('date')(today, 'fullDate');
$scope.agenda = {title: config.agendaTitle, data: undefined, show: false, model: {titre: médecin.titre, prenom: médecin.prenom, nom: médecin.nom, jour: fullDay}};
// ---------------------------------------------------------------- agenda initial
// the global asynchronous task
var task;
// we ask for the agenda
getAgenda();
// ------------------------------------------------------------------ réservation
$scope.reserver = function (creneauId) {
....
};
// ------------------------------------------------------------ suppression RV
$scope.supprimer = function (idRv) {
...
};
// obtaining the agenda
function getAgenda() {
...
}
// cancel wait
function cancel() {
...
}
} ]);
- riga 6: configurazione del messaggio di attesa. Per impostazione predefinita, si attenderanno 3 secondi prima di effettuare una richiesta HTTP;
- riga 7: informazioni richieste per le richieste HTTP;
- riga 8: configurazione del messaggio di errore;
- righe 9–17: medici predefiniti;
- riga 18: un medico specifico. Verranno effettuate prenotazioni per le fasce orarie di questo medico;
- righe 19–24: clienti hard-coded;
- riga 26: vogliamo gestire le date in formato francese;
- riga 27: gli appuntamenti saranno programmati per la data odierna;
- riga 28: il servizio di prenotazione online richiede date nel formato 'aaaa-mm-gg';
- riga 29: la data odierna nel formato [giovedì 26 giugno 2014];
- riga 30: configurazione del calendario. L'attributo [model] contiene i parametri del messaggio internazionalizzato da visualizzare:
agenda_title: "Agenda de {{titre}} {{prenom}} {{nom}} le {{jour}}"
- riga 35: la variabile globale [task] rappresenta l'attività asincrona attualmente in esecuzione in un dato momento;
- riga 37: viene richiesto il calendario iniziale;
Questo è tutto ciò che viene fatto durante il caricamento iniziale della pagina. Se tutto va bene, la vista mostra il calendario della signora PELISSIER per la giornata.

3.7.9.4. Recupero del calendario
Il calendario viene recuperato utilizzando il seguente metodo [getAgenda]:
// obtaining the agenda
function getAgenda() {
// the URL service path
var path = config.urlSvrAgenda + "/" + médecin.id + "/" + formattedDay;
// we ask for the agenda
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path);
// waiting msg
$scope.waiting.visible = true;
// we analyze the result of the call to service [dao]
task.promise.then(function (result) {
// end of wait
$scope.waiting.visible = false;
// mistake?
if (result.err == 0) {
// we prepare the agenda model
$scope.agenda.data = result.data;
$scope.agenda.show = true;
// timetable display formatting
angular.forEach($scope.agenda.data.creneauxMedecin, function (creneauMedecin) {
creneauMedecin.creneau.text = utils.getTextForCreneau(creneauMedecin.creneau);
});
} else {
// mistakes were made in obtaining the agenda
$scope.errors = {title: config.getAgendaErrors, messages: utils.getErrors(result), show: true};
}
});
}
Questo codice è identico a quello studiato nell'applicazione precedente. Ci sono due modifiche:
- non c'è un'attesa simulata prima della chiamata HTTP;
- riga 4: utilizziamo il medico creato durante l'inizializzazione del controller e il giorno formattato che è stato costruito;
Questo codice è stato isolato in una funzione perché viene utilizzato anche dalle funzioni [reserve] e [delete].
3.7.9.5. Prenotazione di una fascia oraria
![]() | ![]() |
Ricorda che i clienti vengono scelti a caso.
Il codice di prenotazione è il seguente:
$scope.reserver = function (creneauId) {
utils.debug("réservation du créneau", creneauId);
// we create a RV with a random customer in the slot identified by [id]
var idClient = clients[Math.floor(Math.random() * clients.length)].id;
utils.debug("réservation du créneau pour le client", idClient);
// simulated waiting
$scope.waiting.visible = true;
var task = utils.waitForSomeTime($scope.waiting.time);
// we add the
var promise = task.promise.then(function () {
// the URL service path
var path = config.urlSvrResaAdd;
// data to be sent to the service
var post = {jour: formattedDay, idCreneau: creneauId, idClient: idClient};
// start the asynchronous task
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path, post);
// we return the promise of task completion
return task.promise;
});
// task result analysis
promise = promise.then(function (result) {
if (result.err != 0) {
// there were errors in validating the appointment
$scope.errors = {title: config.postResaErrors, messages: utils.getErrors(result, $filter), show: true};
} else {
// we ask for the new agenda
getAgenda();
}
});
};
- riga 1: si noti che il parametro della funzione [reserve] è il numero dello slot (attributo id);
- riga 4: viene selezionato casualmente un cliente dall'elenco dei clienti hardcoded nel codice di inizializzazione. Conserviamo il suo identificatore [id];
- righe 7–8: l'attesa di 3 secondi;
- righe 11–18: queste righe vengono eseguite solo dopo che sono trascorsi i 3 secondi;
- riga 12: l'URL del servizio di prenotazione [/ajouterRv]. Questo URL è diverso da quelli che abbiamo incontrato finora. È definito come segue nel servizio web:
@RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse ajouterRv(@RequestBody PostAjouterRv post, HttpServletResponse response) {
- (continua)
- riga 1: l'URL non ha parametri ed è richiesto tramite un POST;
- riga 2: i parametri inviati sono sotto forma di oggetto JSON. Questo verrà deserializzato nel parametro [post] (@RequestBody);
Abbiamo visto un esempio di questo POST (sezione 2.12.2):
![]() |
- in [0], l'URL del servizio web;
- in [1], viene utilizzato il metodo POST;
- in [2], il testo JSON delle informazioni inviate al servizio web nel formato {day, clientId, slotId};
- in [3], il client comunica al servizio web che sta inviando dati JSON;
Torniamo al codice JS per la funzione [reserve]:
- riga 14: creiamo il valore da inviare sotto forma di oggetto JS. Angular lo serializzerà in JSON al momento dell'invio;
- riga 16: viene effettuata la richiesta HTTP. Il valore da inviare è l'ultimo parametro della funzione [dao.getData]. Quando questo parametro è presente, la funzione [dao.getData] esegue un POST invece di un GET (vedi il codice nella sezione 3.7.6.4);
- Riga 18: viene restituita la promessa dalla chiamata HTTP;
- righe 23–29: vengono eseguite solo quando la chiamata HTTP ha restituito la sua risposta;
- riga 23: il parametro [result] è nella forma [err,data] o [err,messages], dove [err] è un codice di errore;
- righe 23–26: se ci sono stati errori, viene visualizzato il messaggio di errore;
- riga 28: se la prenotazione è andata a buon fine, viene visualizzato nuovamente il nuovo calendario;
3.7.9.6. Modifica del server
![]() |
Nella classe [RdvMedecinsCorsController], aggiungiamo il seguente metodo:
// sending options to the customer
private void sendOptions(HttpServletResponse response) {
if (application.isCORSneeded()) {
// set header CORS
response.addHeader("Access-Control-Allow-Origin", "*");
// we authorize the header [authorization]
response.addHeader("Access-Control-Allow-Headers", "authorization");
}
@RequestMapping(value = "/ajouterRv", method = RequestMethod.OPTIONS)
public void ajouterRv(HttpServletResponse response) {
sendOptions(response);
}
L'aggiunta viene effettuata alle righe 10–13. Le intestazioni alle righe 2–8 verranno inviate per l'URL [/addAppt] (riga 10) e il metodo HTTP [OPTIONS] (riga 10).
La classe [RdvMedecinsController] viene modificata come segue:
@RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse ajouterRv(@RequestBody PostAjouterRv post, HttpServletResponse response) {
// headers CORS
rdvMedecinsCorsController.ajouterRv(response);
...
Per il metodo [POST] (riga 1) e l'URL [/addAppointment] (riga 1), viene chiamato il metodo appena aggiunto a [RdvMedecinsCorsController] (riga 4), restituendo così le stesse intestazioni HTTP del metodo HTTP [OPTIONS].
3.7.9.7. Test
Eseguiamo un test iniziale in cui prenotiamo una qualsiasi fascia oraria disponibile:
![]() |
Come sempre in questi casi, dobbiamo controllare i log della console:
[dao] getData[/ajouterRv] error réponse : {"data":"","status":0,"config":{"method":"POST","transformRequest":[null],"transformResponse":[null],"timeout":1000,"url":"http://localhost:8080/ajouterRv","data":{"jour":"2014-06-30","idCreneau":1,"idClient":4},"headers":{"Accept":"application/json, text/plain, */*","Authorization":"Basic YWRtaW46YWRtaW4=","Content-Type":"application/json;charset=utf-8"}},"statusText":""}
Il metodo [dao.getData] ha generato un errore con [status=0], il che significa che Angular ha annullato la richiesta. La causa dell'errore è riportata nei log:
XMLHttpRequest cannot load http://localhost:8080/ajouterRv. Request header field Content-Type is not allowed by Access-Control-Allow-Headers.
Se osserviamo il traffico di rete, vediamo quanto segue:
![]() |
- in [1] e [2]: c'era una sola richiesta HTTP, la richiesta [OPTIONS];
- in [3], il client Angular richiede due autorizzazioni:
- l'autorizzazione a inviare le intestazioni HTTP [accept, authorization, content-type];
- l'autorizzazione a inviare una richiesta POST;
- in [4]: il server autorizza l'intestazione [authorization]. Ricordate che sul lato server, siamo noi a inviare questa autorizzazione;
La novità è che, per un'operazione POST, il client Angular richiede ulteriori autorizzazioni al server. Dobbiamo quindi modificare il server per concederle:
![]() |
Nella classe [RdvMedecinsCorsController], modifichiamo il metodo privato che genera le intestazioni HTTP inviate per le richieste OPTIONS, GET e POST:
// sending options to the customer
private void sendOptions(HttpServletResponse response) {
if (application.isCORSneeded()) {
// set header CORS
response.addHeader("Access-Control-Allow-Origin", "*");
// certain headers are allowed
response.addHeader("Access-Control-Allow-Headers", "accept, authorization, content-type");
// the POST is authorized
response.addHeader("Access-Control-Allow-Methods", "POST");
}
}
- riga 7: abbiamo aggiunto un'autorizzazione per le intestazioni HTTP [accept, content-type];
- riga 9: abbiamo aggiunto un'autorizzazione per il metodo POST;
Eseguiamo nuovamente il test dopo aver riavviato il server:
![]() |
Questa volta la prenotazione è andata a buon fine.
3.7.9.8. Eliminazione di un appuntamento
![]() | ![]() |
Il codice per la funzione [delete] è il seguente:
$scope.supprimer = function (idRv) {
utils.debug("suppression rv n°", idRv);
// simulated waiting
$scope.waiting.visible = true;
task = utils.waitForSomeTime($scope.waiting.time);
// we add the
var promise = task.promise.then(function () {
// the URL service path
var path = config.urlSvrResaRemove;
// data to be sent to the service
var post = {idRv: idRv};
// start the asynchronous task
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path, post);
// we return the promise of task completion
return task.promise;
});
// task result analysis
promise = promise.then(function (result) {
if (result.err != 0) {
// there have been errors deleting the rv
$scope.errors = {title: config.postRemoveErrors, messages: utils.getErrors(result, $filter), show: true};
// the UI is updated
$scope.waiting.visible = false;
} else {
// we ask for the new agenda
getAgenda();
}
});
};
- riga 1: ricorda che il parametro della funzione è l'ID dell'appuntamento da eliminare. Questo codice è molto simile a quello della prenotazione. Commenteremo solo le differenze;
- riga 9: l'URL del servizio qui è [/deleteAppointment] e, come prima, vi si accede tramite una richiesta POST:
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse supprimerRv(@RequestBody PostSupprimerRv post, HttpServletResponse response) {
Il parametro inviato viene nuovamente trasmesso in formato JSON. Nella sezione 2.12.17 abbiamo illustrato la natura del POST eseguito manualmente:
![]() |
- in [1], l'URL del servizio web;
- in [2], viene utilizzato il metodo POST;
- in [3], il testo JSON delle informazioni inviate al servizio web nella forma {idRv};
- in [4], il client informa il servizio web che sta inviando dati JSON;
Torniamo al codice JS per la funzione [delete]:
- riga 11: creiamo l'oggetto inviato. Angular lo serializzerà automaticamente in JSON;
Il resto del codice è simile a quello della prenotazione.
3.7.9.9. Modifiche lato server
Sul lato server, apportiamo le seguenti modifiche:
![]() |
Nella classe [RdvMedecinsCorsController], aggiungiamo il seguente metodo:
// sending options to the customer
private void sendOptions(HttpServletResponse response) {
if (application.isCORSneeded()) {
// set header CORS
response.addHeader("Access-Control-Allow-Origin", "*");
// certain headers are allowed
response.addHeader("Access-Control-Allow-Headers", "accept, authorization, content-type");
// the POST is authorized
response.addHeader("Access-Control-Allow-Methods", "POST");
}
}
...
@RequestMapping(value = "/supprimerRv", method = RequestMethod.OPTIONS)
public void supprimerRv(HttpServletResponse response) {
sendOptions(response);
}
L'aggiunta è stata effettuata alle righe 13–16. Le intestazioni delle righe 2–10 verranno inviate per l'URL [/deleteAppointment] (riga 13) e il metodo HTTP [OPTIONS] (riga 13).
La classe [RdvMedecinsController] viene modificata come segue:
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse supprimerRv(@RequestBody PostSupprimerRv post, HttpServletResponse response) {
// headers CORS
rdvMedecinsCorsController.supprimerRv(response);
...
Per il metodo [POST] (riga 1) e l'URL [/deleteAppointment] (riga 1), viene chiamato il metodo appena aggiunto a [RdvMedecinsCorsController] (riga 4), restituendo così le stesse intestazioni HTTP del metodo HTTP [OPTIONS].
3.7.10. Esempio 10: Creazione e cancellazione di appuntamenti - 2
Presentiamo ora la stessa applicazione di prima, ma invece di prenotare per un cliente casuale, il cliente verrà selezionato da un elenco a discesa.
3.7.10.1. La vista V dell'applicazione
Presenteremo il seguente modulo:
![]() |
I clienti saranno selezionati in [1].
Il codice è simile a quello dell'applicazione precedente, quindi presenteremo solo le differenze principali.
Duplichiamo il file [app-19.html] in [app-20.html], quindi creiamo il codice per l'elenco a discesa dei clienti [1]:
<!-- customer list -->
<div class="alert alert-info">
<h3>{{agenda.title|translate:agenda.model}}</h3>
<div class="row" ng-show="clients.show">
<div class="col-md-3">
<h2 translate="{{clients.title}}"></h2>
<select data-style="btn-primary" class="selectpicker" select-enable="" ng-if="clients.data">
<option ng-repeat="client in clients.data" value="{{client.id}}">
{{client.titre}} {{client.prenom}} {{client.nom}}
</option>
</select>
</div>
</div>
</div>
- righe 8–12: l'elenco a discesa verrà implementato utilizzando il componente [bootstrap-select];
- riga 1: la direttiva [selectEnable] viene applicata tramite l'attributo [select-enable];
- riga 1: il tag <select> viene generato solo se [clients.data] esiste (# null, undefined). Questo punto è importante ed è stato spiegato nella sezione 3.7.7.8;
Inoltre, importiamo nuovi file JS:
<script type="text/javascript" src="rdvmedecins-08.js"></script>
<!-- directives -->
<script type="text/javascript" src="selectEnable.js"></script>
<script type="text/javascript" src="footable.js"></script>
- Riga 1: il file [rdvmedecins-08.js] viene creato copiando il file [rdvmedecins-0.js];
- Righe 3-4: Vengono importati i file per entrambe le direttive;
3.7.10.2. Controller C
Il codice per il controller C si evolve come segue:
// controller
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate', '$timeout', '$filter', '$locale',
function ($scope, utils, config, dao, $translate, $timeout, $filter, $locale) {
// ------------------- model initialization
...
// our customers
$scope.clients = {title: config.listClients, show: false, model: {}};
//------------------------------------------- initilisation vue
// the global asynchronous task
var task;
// we ask for the customers, then the agenda
getClients().then(function () {
getAgenda();
});
...
// execution action
function getClients() {
....
};
} ]);
- riga 8: l'oggetto [$scope.clients] configura l'elenco a discesa dei clienti nella vista V;
- righe 14–16: in modo asincrono, richiediamo prima l'elenco dei clienti, poi, una volta ottenuto, richiediamo l'agenda della signora PELISSIER per oggi. La sintassi qui utilizzata funziona solo perché la funzione [getClients] restituisce una promessa;
Il metodo [getClients] recupera l'elenco dei clienti:
function getClients() {
// the UI is updated
$scope.waiting.visible = true;
$scope.clients.show = false;
$scope.errors.show = false;
// we ask for the customer list;
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrClients);
var promise = task.promise;
// analyze the result of the previous call
promise = promise.then(function (result) {
// result={err: 0, data: [client1, client2, ...]}
// result={err: n, messages: [msg1, msg2, ...]}
if (result.err == 0) {
// we put the acquired data into the model
$scope.clients.data = result.data;
// the UI is updated
$scope.clients.show = true;
$scope.waiting.visible = false;
} else {
// there were errors in obtaining the customer list
$scope.errors = { title: config.getClientsErrors, messages: utils.getErrors(result), show: true, model: {}};
// the UI is updated
$scope.waiting.visible = false;
}
});
// we return the promise
return promise;
};
Questo è un codice che abbiamo già incontrato e discusso. La parte importante da notare è la riga 31:
- riga 27: restituiamo la promessa della riga 10, ovvero l'ultima promessa ottenuta nel codice. Questa promessa verrà soddisfatta solo una volta che la richiesta HTTP avrà restituito la sua risposta;
Il metodo [reserve] cambia leggermente:
$scope.reserver = function (creneauId) {
utils.debug("réservation du créneau", creneauId);
// on crée un RV pour le client sélectionné
var idClient = $(".selectpicker").selectpicker('val');
...
});
- Riga 4: Non effettuiamo più prenotazioni per un cliente a caso, ma per il cliente selezionato dall'elenco dei clienti.
3.7.11. Esempio 11: una direttiva [selectEnable2]
Questo esempio riprende il tema delle direttive.
3.7.11.1. La vista V
L'applicazione visualizza la seguente vista:
![]() |
3.7.11.2. Il codice HTML della vista
Il codice HTML per la vista [app-21.html] è il seguente:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- the waiting message -->
<div class="alert alert-warning" ng-show="waiting.visible">
...
</div>
<!-- the error list -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
<!-- customer list -->
<div class="alert alert-info">
<div class="row" ng-show="clients.show">
<div class="col-md-4">
<h2 translate="{{clients.title}}"></h2>
<select data-style="btn-primary" id="selectpickerClients" select-enable2="" ng-if="clients.data">
<option ng-repeat="client in clients.data" value="{{client.id}}">
{{client.titre}} {{client.prenom}} {{client.nom}}
</option>
</select>
</div>
</div>
</div>
<!-- list of doctors -->
<div class="alert alert-info">
<div class="row" ng-show="medecins.show">
<div class="col-md-4">
<h2 translate="{{medecins.title}}"></h2>
<select data-style="btn-primary" id="selectpickerMedecins" select-enable2="" ng-if="medecins.data">
<option ng-repeat="medecin in medecins.data" value="{{medecin.id}}">
{{medecin.titre}} {{medecin.prenom}} {{medecin.nom}}
</option>
</select>
</div>
</div>
</div>
</div>
...
<script type="text/javascript" src="rdvmedecins-09.js"></script>
<!-- guidelines -->
<script type="text/javascript" src="selectEnable2.js"></script>
- righe 19–23: l'elenco a discesa del client;
- riga 19: viene applicata la direttiva [selectEnable2] (attributo [select-enable2]);
- riga 19: solo se [clients.data] non è vuoto;
- riga 19: l'elenco a discesa è identificato dall'attributo [id="selectpickerClients"]
- righe 33-37: l'elenco a discesa dei medici;
- riga 33: viene applicata la direttiva [selectEnable2] (attributo [select-enable2]);
- riga 33: solo se [doctors.data] non è vuoto;
- riga 33: l'elenco a discesa è identificato dall'attributo [id="selectpickerMedecins"];
- riga 43: viene importato un nuovo file JS [rdvmedecins-09.js];
- riga 45: viene importato il file JS per la nuova direttiva;
3.7.11.3. La direttiva [selectEnable2]
Il codice per la direttiva [selectEnable2] è il seguente:
angular.module("rdvmedecins").directive('selectEnable2', ['$timeout', 'utils', function ($timeout, utils) {
return {
link: function (scope, element, attrs) {
utils.debug("directive selectEnable2 attrs", attrs);
$timeout(function () {
$('#' + attrs['id']).selectpicker();
})
}
}
}]);
- riga 4: visualizziamo il valore del parametro [attrs] per aiutare a comprendere il funzionamento del codice. Vedremo che attrs['id']='selectpickerClients' per l'elenco dei clienti;
- riga 6: per individuare un elemento con [id='x'] nel DOM, scriviamo [$('#x')]. Pertanto, dobbiamo scrivere [$('#selectpickerClients')] per individuare l'elenco dei clienti. Ciò si ottiene utilizzando la sintassi [$('#' + attrs['id'])];
La direttiva [selectEnable2] utilizza quindi le informazioni contenute in uno degli attributi dell'elemento HTML a cui viene applicata.
3.7.11.4. Controller C
Il controller C si trova nel file JS [rdvmedecins-09.js] e presenta la seguente struttura:
// controller
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao',
function ($scope, utils, config, dao) {
// ------------------- model initialization
// the waiting msg
$scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: 3000};
// login information
$scope.server = {url: 'http://localhost:8080', login: 'admin', password: 'admin'};
// errors
$scope.errors = {show: false, model: {}};
// the doctors
$scope.medecins = {title: config.listMedecins, show: false, model: {}};
// our customers
$scope.clients = {title: config.listClients, show: false, model: {}};
// the global asynchronous task
var task;
// ---------------------------------------------------- initialisation vue
// the UI is updated
$scope.waiting.visible = true;
$scope.clients.show = false;
$scope.medecins.show = false;
$scope.errors.show = false;
// we ask for customers, then doctors
getClients().then(function () {
getMedecins();
});
// customer list
function getClients() {
...
}
// list of doctors
function getMedecins() {
...
}
// cancel wait
function cancel() {
...
}
} ]);
- righe 26–28: prima interroghiamo i clienti, poi i medici;
3.7.11.5. Test
Prova questa nuova versione.
3.7.12. Esempio 12: Una direttiva [list]
Useremo lo stesso esempio di prima, ma vogliamo semplificare il codice HTML utilizzando una direttiva. Attualmente, abbiamo il seguente codice HTML:
<!-- customer list -->
<div class="alert alert-info">
<div class="row" ng-show="clients.show">
<div class="col-md-4">
<h2 translate="{{clients.title}}"></h2>
<select data-style="btn-primary" id="selectpickerClients" select-enable2="" ng-if="clients.data">
<option ng-repeat="client in clients.data" value="{{client.id}}">
{{client.titre}} {{client.prenom}} {{client.nom}}
</option>
</select>
</div>
</div>
</div>
<!-- list of doctors -->
<div class="alert alert-info">
<div class="row" ng-show="medecins.show">
<div class="col-md-4">
<h2 translate="{{medecins.title}}"></h2>
<select data-style="btn-primary" id="selectpickerMedecins" select-enable2="" ng-if="medecins.data">
<option ng-repeat="medecin in medecins.data" value="{{medecin.id}}">
{{medecin.titre}} {{medecin.prenom}} {{medecin.nom}}
</option>
</select>
</div>
</div>
</div>
Le righe 14–26 sono identiche alle righe 1–13. Si applicano ai medici invece che ai clienti. Vorremmo poter scrivere quanto segue:
<!-- la liste des clients -->
<list model="clients" ng-if="clients.show"></list>
<!-- la liste des médecins -->
<list model="medecins" ng-if="medecins.show"></list>
Questo codice utilizza una nuova direttiva [list], che creeremo ora.
3.7.12.1. La direttiva [list]
La direttiva [list] è inserita nel file JS [list.js]. Il suo codice è il seguente:
angular.module("rdvmedecins")
.directive("list", ['utils', '$timeout', function (utils, $timeout) {
// instance de la directive retournée
return {
// élément HTML
restrict: "E",
// url du fragment
templateUrl: "list.html",
// scope unique à chaque instance de la directive
scope: true,
// fonction lien avec le document
link: function (scope, element, attrs) {
utils.debug("directive list attrs", attrs);
scope.model = scope[attrs['model']];
utils.debug("directive list model", scope.model);
$timeout(function () {
$('#' + scope.model.id).selectpicker();
})
}
}
}]);
- riga 2: definisce una direttiva denominata 'list';
- riga 6: l'attributo [restrict] specifica come può essere utilizzata la direttiva. [restrict: "E"] significa che la direttiva [list] può essere utilizzata come elemento HTML <list ...>...</list>. [restrict: "A"] significa che la direttiva [list] può essere utilizzata come attributo, ad esempio <div ... list='...'>. [restrict: "AE"] significa che la direttiva [list] può essere utilizzata sia come attributo che come elemento;
- riga 8: l'attributo [templateUrl] specifica il nome del frammento HTML da utilizzare quando si incontra il tag. Questo frammento costituirà il corpo del tag;
- riga 10: l'attributo [scope] imposta l'ambito del modello della direttiva. [scope: true] significa che due elementi <list> avranno ciascuno il proprio modello. Per impostazione predefinita (scope non inizializzato), condividono i loro modelli;
- riga 12: la funzione [link], che abbiamo già utilizzato diverse volte;
Per comprendere il codice sopra riportato, è necessario ricordare come verrà utilizzata la direttiva:
<!-- la liste des clients -->
<list model="clients" ng-if="clients.show"></list>
<!-- la liste des médecins -->
<list model="medecins" ng-if="medecins.show"></list>
La direttiva [list] viene utilizzata come un elemento HTML <list>. Questo elemento ha due attributi:
- [model]: il cui valore sarà l'elemento del modello M della vista V in cui si trova la direttiva [list]. Questo elemento popolerà il modello della direttiva;
- [ng-if]: che garantisce che il codice HTML della direttiva non venga generato se non c'è nulla da visualizzare;
Torniamo al codice della funzione [link] della direttiva:
link: function (scope, element, attrs) {
utils.debug("directive list attrs", attrs);
scope.model = scope[attrs['model']];
utils.debug("directive list model", scope.model);
$timeout(function () {
$('#' + scope.model.id).selectpicker();
})
}
Combiniamo questo codice JS con il codice HTML che utilizza la direttiva:
<list model="clients" ng-if="clients.show"></list>
- riga 3: qui attrs['model'] ha il valore 'clients';
- riga 3: scope[attrs['model']] ha il valore scope['clients'] e quindi rappresenta [$scope.clients], ovvero il campo [clients] del modello di vista. Questo campo avrà il valore {id:'...', data:[client1, client2, ...], show:..., title:'...'};
- riga 3: aggiungiamo un campo [model] al modello della direttiva. Questo campo eredita dal modello della vista in cui si trova. Dobbiamo quindi evitare conflitti con qualsiasi campo [model] che la vista potrebbe avere. Qui non ci saranno conflitti;
- riga 4: visualizziamo [scope.model] per comprendere meglio il codice;
- righe 5-7: vediamo del codice che abbiamo già incontrato in precedenza. La differenza è che l'ID del componente veniva precedentemente recuperato da un attributo attrs['id']. Qui, verrà recuperato da [scope.model.id];
Ora, diamo un'occhiata al codice HTML generato dalla direttiva. A causa dell'attributo [templateUrl: "list.html"] della direttiva, dobbiamo cercarlo nel file [list.html]:
<!-- a list of customers or doctors -->
<div class="alert alert-info" ng-show="model.show">
<div class="row">
<div class="col-md-4">
<h2 translate="{{model.title}}"></h2>
<select data-style="btn-primary" id="{{model.id}}" ng-if="model.data">
<option ng-repeat="element in model.data" value="{{element.id}}">
{{element.titre}} {{element.prenom}} {{element.nom}}
</option>
</select>
</div>
</div>
</div>
- La prima cosa da ricordare quando si legge questo codice è che la direttiva ha creato un oggetto [scope.model] della forma [{id:'...', data:[client1, client2, ...], show:..., title:'...'}]. Questo oggetto [model] (scope è implicito nel codice HTML) viene utilizzato dal codice HTML della direttiva;
- riga 2: uso di [model.show] per mostrare/nascondere la vista generata dalla direttiva;
- riga 5: uso di [model.title] per impostare un titolo;
- riga 6: uso di [model.id] per assegnare un ID al tag <select>. Questo ID viene utilizzato dal codice JavaScript della direttiva;
- riga 6: uso di [model.data] per generare il tag <select> solo se ci sono dati da visualizzare;
- righe 7–9: uso di [model.data] per generare le voci dell'elenco a discesa;
3.7.12.2. Il codice HTML
Il codice HTML per l'applicazione [app-22.html] è il seguente:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- the waiting message -->
<div class="alert alert-warning" ng-show="waiting.visible">
...
</div>
<!-- the error list -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
<!-- customer list -->
<list model="clients" ng-if="clients.show"></list>
<!-- list of doctors -->
<list model="medecins" ng-if="medecins.show"></list>
</div>
...
<script type="text/javascript" src="rdvmedecins-10.js"></script>
<!-- guidelines -->
<script type="text/javascript" src="list.js"></script>
- riga 22: non dimenticare di includere il codice JS per la direttiva;
3.7.12.3. Il controller C
Il controller C cambia molto poco:
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao',
function ($scope, utils, config, dao) {
// ------------------- model initialization
...
// the doctors
$scope.medecins = {title: config.listMedecins, show: false, id: 'medecins'};
// our customers
$scope.clients = {title: config.listClients, show: false, id: 'clients'};
...
- Righe 7 e 9: aggiungiamo l'attributo [id] ai modelli doctors e clients;
3.7.12.4. I test
I test danno gli stessi risultati dell'esempio precedente.
3.7.13. Esempio 13: Aggiornamento del modello di una direttiva
Continuiamo il nostro studio sulle direttive e restiamo sull'esempio dell'elenco a discesa. Qui, vogliamo esaminare il comportamento della direttiva [list] quando cambia il contenuto dell'elenco a discesa.
3.7.13.1. Le viste V
Le diverse viste sono le seguenti:
![]() |
- in [1], richiediamo l'elenco dei clienti per la prima volta;
![]() |
- in [2], richiediamo l'elenco dei clienti una seconda volta. Questo secondo elenco viene quindi combinato con il primo [3]. È l'aggiornamento del componente [Bootstrap select] che vogliamo esaminare in questo esempio.
3.7.13.2. La pagina HTML
La pagina HTML [app-23.html] viene creata copiando [app-22.html] e poi modificata come segue:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- le message d'attente -->
<div class="alert alert-warning" ng-show="waiting.visible">
...
</div>
<!-- la liste d'erreurs -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
<!-- le bouton -->
<div class="alert alert-warning">
<button class="btn btn-primary" ng-click="getClients()">{{clients.title|translate}}</button>
</div>
<!-- la liste des clients -->
<list2 model="clients" ng-if="clients.show"></list2>
</div>
...
<script type="text/javascript" src="rdvmedecins-11.js"></script>
<!-- directives -->
<script type="text/javascript" src="list2.js"></script>
Le modifiche rispetto all'applicazione precedente sono le seguenti:
- righe 15–17: aggiunta di un pulsante;
- riga 20: utilizzo di una nuova direttiva [list2];
- riga 23: utilizzo di un nuovo file JS;
- Riga 25: importazione del file JS dalla direttiva [list2];
3.7.13.3. La direttiva [list2]
La direttiva [list2] in [list2.js] è la seguente:
angular.module("rdvmedecins")
.directive("list2", ['utils', '$timeout', function (utils, $timeout) {
// instance de la directive retournée
return {
// élément HTML
restrict: "E",
// url du fragment
templateUrl: "list.html",
// scope unique à chaque instance de la directive
scope: true,
// fonction lien avec le document
link: function (scope, element, attrs) {
utils.debug('directive list2');
scope.model = scope[attrs['model']];
$timeout(function () {
$('#' + scope.model.id).selectpicker('refresh');
})
}
}
}]);
L'unica differenza rispetto alla direttiva [list] è la riga 16: con il metodo [selectpicker('refresh')], diciamo al componente [Bootstrap-select] di aggiornarsi. L'idea alla base è che ogni volta che l'utente richiede un nuovo elenco di clienti, l'elenco a discesa verrà aggiornato. Non funzionerà, ma questa è l'idea di base.
3.7.13.4. Il controller C
Il controller si trova nel file [rdvmedecins-11.js], creato copiando il file [rdvmedecins-10.js]:
// our customers
$scope.clients = {title: config.listClients, show: false, id: 'clients', data: []};
...
// customer list
$scope.getClients = function getClients() {
// the UI is updated
$scope.waiting.visible = true;
$scope.errors.show = false;
// we ask for the customer list;
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrClients);
var promise = task.promise;
// analyze the result of the previous call
promise = promise.then(function (result) {
// result={err: 0, data: [client1, client2, ...]}
// result={err: n, messages: [msg1, msg2, ...]}
if (result.err == 0) {
// put the acquired data into a new model to force the view to refresh
$scope.clients = {title: $scope.clients.title, data: $scope.clients.data.concat(result.data), show: $scope.clients.show, id: $scope.clients.id};
// the UI is updated
$scope.clients.show = true;
$scope.waiting.visible = false;
} else {
// there were errors in obtaining the customer list
$scope.errors = { title: config.getClientsErrors, messages: utils.getErrors(result), show: true, model: {}};
// the UI is updated
$scope.waiting.visible = false;
}
});
}
- riga 1: per consentire la concatenazione degli array in [clients.data], questo oggetto viene inizializzato con un array vuoto;
- riga 18: concateniamo il nuovo elenco di clienti con quelli già presenti nell'array [clients.data];
In precedenza avevamo scritto:
Ora scriviamo:
Per comprendere questo codice, è necessario ricordare come viene utilizzato il modello M nella vista V nel caso della direttiva [list2]:
<!-- la liste des clients -->
<list2 model="clients" ng-if="clients.show"></list2>
Il modello utilizzato dalla direttiva [list2] è [clients]. Verrà rivalutato nella vista V solo se [clients] cambia nel modello M della vista. La prima idea che viene in mente per la modifica è scrivere:
per tenere conto del fatto che il nuovo elenco di clienti deve essere aggiunto a quelli precedenti. In questo modo si modifica [clients.data] ma non [clients]. Non ho familiarità con le complessità di JavaScript, ma non sarebbe sorprendente se [clients] fosse un puntatore, come lo è [clients.data]. Il puntatore [clients] non cambia quando modifichiamo il puntatore [clients.data]. La direttiva [list2] non viene quindi rivalutata. Questo è effettivamente ciò che osserviamo durante il debug dell'applicazione (F12 in Chrome).
Scrivendo:
$scope.clients = {title: $scope.clients.title, data: $scope.clients.data.concat(result.data), show: $scope.clients.show, id: $scope.clients.id};
Ci assicuriamo che [$scope.clients] riceva effettivamente un nuovo valore. Il puntatore [$scope.clients] punta a un nuovo oggetto. La direttiva [list2] dovrebbe quindi essere rivalutata. Tuttavia, non otteniamo il risultato desiderato. Esaminiamo gli screenshot quando richiediamo due volte l'elenco dei clienti:
![]() |
- in [1], abbiamo solo quattro elementi invece di otto;
- in [2], questi quattro elementi si trovano in un elemento [select], ma sono nascosti (style='display: none');
![]() |
- in [3], troviamo i quattro clienti in un layout HTML diverso, ed è questo ciò che l'utente vede quando clicca sull'elenco a discesa;
Infine, i log della console mostrano quanto segue:
- riga 1: viene istanziato il servizio [dao];
- riga 2: il servizio [dao] recupera un elenco iniziale di clienti;
- riga 3: viene eseguita la direttiva [list2];
- riga 4: il servizio [dao] recupera un secondo elenco di client;
L'output alla riga 2 proviene dal seguente codice nella direttiva:
link: function (scope, element, attrs) {
utils.debug('directive list2');
...
}
Esaminiamo il ciclo di vita della direttiva [list2]:
- Tra le righe 1 e 2, non viene attivata anche se la vista è stata visualizzata per la prima volta. Ciò è dovuto al suo attributo [ng-if="clients.show"] nella vista V:
<list2 model="clients" ng-if="clients.show"></list2>
- riga 3: dopo aver recuperato il primo elenco di medici, [clients.show] diventa vero e la direttiva viene attivata;
- dopo aver recuperato il secondo elenco di clienti, vediamo che il codice della direttiva [list2] non viene chiamato. Questo è il motivo per cui non vediamo il secondo elenco;
Per risolvere questo problema, modifichiamo la direttiva [list2] come segue:
angular.module("rdvmedecins")
.directive("list2", ['utils', '$timeout', function (utils, $timeout) {
// instance de la directive retournée
return {
// élément HTML
restrict: "E",
// url du fragment
templateUrl: "list.html",
// scope unique à chaque instance de la directive
scope: true,
// fonction lien avec le document
link: function (scope, element, attrs) {
// à chaque fois que attrs["model"] change, le modèle de la directive doit changer également
scope.$watch(attrs["model"], function (newValue) {
utils.debug("directive list2 newValue", newValue);
// on met à jour le modèle de la directive
scope.model = newValue;
$timeout(function () {
$('#' + scope.model.id).selectpicker('refresh');
})
});
}
}
}]);
- Riga 14: La funzione [scope.$watch] consente di monitorare un valore nel modello. La sua sintassi è [scope.$watch('var'), f], dove [var] è l'identificatore di una variabile nel modello e f è la funzione da eseguire quando il valore di quella variabile cambia. In questo caso, vogliamo monitorare la variabile [clients]. Quindi dobbiamo scrivere [scope.$watch('clients')]. Poiché abbiamo attrs['model']='clients', scriviamo [scope.$watch(attrs["model"], function (newValue)];
- riga 14: il secondo parametro della funzione [scope.$watch] è la funzione da eseguire quando la variabile monitorata cambia valore. Il parametro [newValue] è il nuovo valore della variabile, quindi, nel nostro caso, il nuovo valore della variabile [clients] nel modello;
- riga 17: questo nuovo valore viene assegnato al campo [model] del modello della direttiva;
Una volta apportata questa modifica, i log cambiano:
![]() |
Sopra, vediamo che dopo aver ottenuto il secondo elenco di client, la direttiva [list2] viene effettivamente eseguita di nuovo, come confermato dal risultato [2].
3.7.14. Esempio 14: le direttive [waiting] e [errors]
Torniamo al codice HTML dell'applicazione precedente:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- le message d'attente -->
<div class="alert alert-warning" ng-show="waiting.visible">
...
</div>
<!-- la liste d'erreurs -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
<!-- le bouton -->
<div class="alert alert-warning">
<button class="btn btn-primary" ng-click="getClients()">{{clients.title|translate}}</button>
</div>
<!-- la liste des clients -->
<list2 model="clients" ng-if="clients.show"></list2>
</div>
- righe 5-7: il messaggio di caricamento;
- righe 10-12: il messaggio di errore;
Decidiamo di inserire il codice HTML per questi due messaggi all'interno delle direttive.
3.7.14.1. Il nuovo codice HTML
Il nuovo codice HTML [app-24.html] è il seguente:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- le message d'attente -->
<waiting model="waiting"></waiting>
<!-- la liste d'erreurs -->
<errors model="errors"></errors>
<!-- le bouton -->
<div class="alert alert-warning">
<button class="btn btn-primary" ng-click="getClients()">{{clients.title|translate}}</button>
</div>
<!-- la liste des clients -->
<list2 model="clients" ng-if="clients.show"></list2>
</div>
...
<script type="text/javascript" src="rdvmedecins-12.js"></script>
<!-- directives -->
<script type="text/javascript" src="list2.js"></script>
<script type="text/javascript" src="errors.js"></script>
<script type="text/javascript" src="waiting.js"></script>
- riga 5: la direttiva per il messaggio di attesa;
- riga 8: la direttiva per il messaggio di errore;
- riga 19: il nuovo file JS associato all'applicazione;
- righe 21–23: i file JS per le tre direttive;
3.7.14.2. La direttiva [waiting]
Il codice JS per la direttiva [waiting] si trova nel seguente file [waiting.js]:
angular.module("rdvmedecins")
.directive("waiting", ['utils', function (utils) {
// returned directive instance
return {
// element HTML
restrict: "E",
// fragment url
templateUrl: "waiting.html",
// scope unique to each directive instance
scope: true,
// function link to document
link: function (scope, element, attrs) {
// each time attr["model"] changes, the page model must also change
scope.$watch(attrs["model"], function (newValue) {
utils.debug("[waiting] watch newValue", newValue);
scope.model = newValue;
});
}
}
}]);
Questo codice segue la stessa logica della direttiva [list2] già discussa.
Alla riga 8, facciamo riferimento al seguente file [waiting.html]:
<div class="alert alert-warning" ng-show="model.show">
<h1>{{ model.title.text | translate:model.title.values}}
<button class="btn btn-primary pull-right" ng-click="model.cancel()">{{'cancel'|translate}}</button>
<img src="assets/images/waiting.gif" alt=""/>
</h1>
</div>
Nel codice JS dell'applicazione, il modello [$scope.waiting] per questo codice HTML sarà definito come segue:
// the waiting msg
$scope.waiting = {title: {text: config.msgWaiting, values: {}}, show: false, cancel: cancel, time: 3000};
3.7.14.3. La direttiva [errors]
Il codice JS per la direttiva [errors] si trova nel seguente file [errors.js]:
angular.module("rdvmedecins")
.directive("errors", ['utils', function (utils) {
// returned directive instance
return {
// element HTML
restrict: "E",
// fragment url
templateUrl: "errors.html",
// scope unique to each directive instance
scope: true,
// function link to document
link: function (scope, element, attrs) {
// each time attr["model"] changes, the page model must also change
scope.$watch(attrs["model"], function (newValue) {
utils.debug("[errors] watch newValue", newValue);
scope.model = newValue;
});
}
}
}]);
Questo codice segue la stessa logica della direttiva [list2] già discussa.
Alla riga 8, facciamo riferimento al seguente file [errors.html]:
<div class="alert alert-danger" ng-show="model.show">
{{model.title.text|translate:model.title.values}}
<ul>
<li ng-repeat="message in model.messages">{{message|translate}}</li>
</ul>
</div>
Nel codice JS dell'applicazione, il modello [$scope.errors] per questo codice HTML sarà definito come segue:
// there were errors in obtaining the customer list
$scope.errors = { title: { text: config.getClientsErrors, values: {}}, messages: utils.getErrors(result), show: true, model: {}};
3.7.15. Esempio 15: Navigazione
Finora abbiamo utilizzato applicazioni a pagina singola. In questo esempio tratteremo le applicazioni multipagina e la navigazione tra di esse.
3.7.15.1. Le viste V dell'applicazione
![]() |
- in [1], l'URL della vista n. 1;
- in [2], il suo contenuto;
- in [3], si passa alla pagina 2;
- in [4], la vista n. 2;
- in [5], andiamo alla pagina 3;
![]() |
- in [6], visualizza n. 3;
- in [7], andiamo alla pagina 1;
- in [8], torniamo alla vista n. 1;
3.7.15.2. Organizzazione del codice
Stiamo avviando una nuova organizzazione del codice:
![]() |
- le viste dell'applicazione saranno collocate nella cartella [views];
- il modulo dell'applicazione verrà inserito nella cartella [modules];
- I controller dell'applicazione saranno collocati nella cartella [controllers];
Analogamente, nella versione finale:
- i servizi saranno collocati nella cartella [services];
- le direttive saranno collocate nella cartella [directives];
3.7.15.3. Il contenitore delle viste
Le viste nella cartella [views] verranno visualizzate nel seguente contenitore [app-25.html]:
<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
...
</head>
<body>
<div class="container" ng-controller="mainCtrl">
<!-- the navigation bar -->
<ng-include src="'views/navbar.html'"></ng-include>
<!-- the current view -->
<ng-view></ng-view>
</div>
...
<!-- the module -->
<script type="text/javascript" src="modules/rdvmedecins-13.js"></script>
<!-- controllers -->
<script type="text/javascript" src="controllers/mainController.js"></script>
<script type="text/javascript" src="controllers/page1Controller.js"></script>
<script type="text/javascript" src="controllers/page2Controller.js"></script>
<script type="text/javascript" src="controllers/page3Controller.js"></script>
</body>
</html>
- riga 7: il corpo del contenitore è controllato da [mainCtrl];
- riga 9: la direttiva [ng-include] consente di includere un file HTML esterno, in questo caso una barra di navigazione;
- riga 12: le diverse viste visualizzate dal contenitore vengono renderizzate all'interno della direttiva [ng-view]. In definitiva, abbiamo un contenitore che visualizza:
- sempre la stessa barra di navigazione (riga 9);
- viste diverse alla riga 12;
- righe 16–22: importiamo i file JS dal modulo dell'applicazione [rdvmedecins-13.js] e dai suoi controller;
3.7.15.4. Il modulo dell'applicazione
Il file [rdvmedecins-13.js] definisce il modulo dell'applicazione e il routing tra le viste:
// --------------------- Angular module
angular.module("rdvmedecins", [ 'ngRoute' ]);
angular.module("rdvmedecins").config(["$routeProvider", function ($routeProvider) {
// ------------------------ routage
$routeProvider.when("/page1",
{
templateUrl: "views/page1.html",
controller: 'page1Ctrl'
});
$routeProvider.when("/page2",
{
templateUrl: "views/page2.html",
controller: 'page2Ctrl'
});
$routeProvider.when("/page3",
{
templateUrl: "views/page3.html",
controller: 'page3Ctrl'
});
$routeProvider.otherwise(
{
redirectTo: "/page1"
});
}]);
- riga 1: definisce il modulo [rdvmedecins]. Dipende dal modulo [ngRoute] fornito dalla libreria [angular-route.min.js]. Questo modulo abilita il routing definito nelle righe 6–24;
- riga 4: definisce la funzione [config] del modulo [rdvmedecins]. Si noti che questa funzione viene eseguita prima che qualsiasi servizio venga istanziato. Si tratta di una funzione di configurazione del modulo. Qui viene configurato il suo routing. Ciò avviene utilizzando l'oggetto [$routeProvider] fornito dal modulo [ngRoute];
- Righe 6–10: definiscono la vista da visualizzare quando l'utente richiede l'URL [/page1]. Si tratta di un routing interno all'applicazione. L'URL è in realtà [/rdvmedecins-angular-v1/app-21.html#/page1]. Possiamo notare che viene ancora utilizzato l'URL del contenitore [/rdvmedecins-angular-v1/app-21.html], ma con informazioni aggiuntive che seguono il carattere #. È proprio questa informazione aggiuntiva che viene gestita dal routing di Angular;
- riga 8: specifica il frammento HTML da inserire nella direttiva [ng-view] del contenitore:
- riga 9: specifica il nome del controller per questo frammento;
- righe 11–15: definiscono la vista da visualizzare quando l'utente richiede l'URL [/page2];
- righe 16–20: definiscono la vista da visualizzare quando l'utente richiede l'URL [/page3];
- righe 21–24: definiscono il routing da eseguire quando l'URL richiesto non è uno dei tre precedenti (altrimenti, riga 21);
- riga 23: reindirizza all'URL [/page1] e quindi alla vista definita nelle righe 6–10;
3.7.15.5. Il controller del contenitore delle viste
Abbiamo visto che il contenitore delle viste ha dichiarato un controller:
<div class="container" ng-controller="mainCtrl">
Il controller [mainCtrl] è definito nel file [mainController.js]:
// controller
angular.module("rdvmedecins")
.controller('mainCtrl', ['$scope', '$location',
function ($scope, $location) {
// page templates
$scope.page1 = {};
$scope.page2 = {};
$scope.page3 = {};
// global model
var main = $scope.main = {};
main.text = "[Modèle global]";
// methods exposed to view
main.showPage1 = function () {
$location.path("/page1");
};
main.showPage2 = function () {
$location.path("/page2");
};
main.showPage3 = function () {
$location.path("/page3");
}
}]);
- riga 3: il controller [mainCtrl] necessita dell'oggetto [$location] fornito dal modulo di routing [ngRoute]. Questo oggetto consente di cambiare le viste (righe 16, 19, 22);
Torniamo al codice del contenitore:
<div class="container" ng-controller="mainCtrl">
<!-- the navigation bar -->
<ng-include src="'views/navbar.html'"></ng-include>
<!-- the current view -->
<ng-view></ng-view>
</div>
- Il controller [mainCtrl] crea il modello per l'area 1-7;
- anche la vista inclusa alla riga 6 ha un controller. Ad esempio, la vista [page1] ha il controller [page1Ctrl]. Questo controller costruisce il modello per l'area visualizzata alla riga 6. Abbiamo quindi due modelli in quest'area:
- il modello creato dal controller [mainCtrl];
- il modello creato dal controller [page1Ctrl];
Esiste una convenzione di denominazione per i modelli. Nella vista mostrata alla riga 6, sono visibili entrambi i modelli per i controller [mainCtrl] e [pagexCtrl]. Se due variabili in questi modelli hanno lo stesso nome, una sovrascriverà l'altra. Per evitare questo conflitto di denominazione, creiamo quattro modelli con quattro nomi diversi:
contenitore | mainCtrl | main | 11 |
pagina1 | pagina1Ctrl | pagina1 | 7 |
pagina2 | pagina2Ctrl | pagina2 | 8 |
pagina3 | pagina3Ctrl | pagina3 | 9 |
- riga 12: definisce un elemento [text] nel modello [main];
Le righe 7–11 hanno un effetto molto specifico: definiscono lo [$scope] del controller [mainCtrl] e, al suo interno, creano quattro variabili [main, page1, page2, page3]. Queste quattro variabili saranno utilizzate come rispettivi modelli per il contenitore e le tre viste che esso conterrà a sua volta.
3.7.15.6. La barra di navigazione
La barra di navigazione è definita come segue nel contenitore:
<div class="container" ng-controller="mainCtrl">
<!-- the navigation bar -->
<ng-include src="'views/navbar.html'"></ng-include>
<!-- the current view -->
<ng-view></ng-view>
</div>
La barra di navigazione è definita alla riga 3. Ciò significa che conosce solo il modello [main]. Il suo codice è il seguente:
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<div class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li class="active">
<a href="">
<span ng-click="main.showPage1()">Page 1</span>
</a>
</li>
<li class="active">
<a href="">
<span ng-click="main.showPage2()">Page 2</span>
</a>
</li>
<li class="active">
<a href="">
<span ng-click="main.showPage3()">Page 3</span>
</a>
</li>
</ul>
</div>
</div>
</div>
- Alle righe 16, 21 e 26 vengono utilizzati metodi del modello [main];
- riga 16: cliccando sul link [Page1] si attiverà l'esecuzione del metodo [$scope.main.showPage1]. Questo è definito nel controller [mainCtrl] come segue:
// global model
var main = $scope.main = {};
main.text = "[Modèle global]";
// methods exposed to view
main.showPage1 = function () {
$location.path("/page1");
};
- Riga 6: dal codice sopra riportato, possiamo vedere che il metodo [main.showPage1] è in realtà il metodo [$scope.main.showPage1]. Quindi è questo che verrà eseguito;
- riga 7: modifichiamo l'URL dell'applicazione in [/page1]. Torniamo al routing definito nel modulo principale:
$routeProvider.when("/page1",
{
templateUrl: "views/page1.html",
controller: 'page1Ctrl'
});
Possiamo vedere che il frammento [views/page1.html] verrà inserito nel contenitore e che il suo controller è [page1Ctrl].
3.7.15.7. La vista [/page1] e il suo controller
Il frammento [views/page1.html] è il seguente:
<h1>Page 1</h1>
<div class="alert alert-info">
<ul>
<li>Modèle global : {{main.text}}</li>
<li>Modèle local : {{page1.text}}</li>
</ul>
</div>
Ricordiamo che nella vista inserita nel contenitore è visibile il modello [main]. È questo che vogliamo verificare alla riga 4. Inoltre, il controller [page1Ctrl] per il frammento [views/page1.html] definisce un modello [page1]. È questo quello utilizzato alla riga 5.
Il codice per il controller [page1Ctrl] è il seguente:
angular.module("rdvmedecins")
.controller('page1Ctrl', ['$scope',
function ($scope) {
// page 1 template
var page1=$scope.page1;
page1.text="[Modèle local dans page 1]";
}]);
- Riga 2: Il [$scope] inserito qui non è vuoto. Poiché il controller [page1Ctrl] controlla un'area inserita in un contenitore controllato da [mainCtrl], il [$scope] nella riga 2 contiene gli elementi del [$scope] definito dal controller [mainCtrl]. È importante comprendere questo aspetto. Il [$scope] definito dal controller [mainCtrl] contiene i seguenti elementi: [main, page1, page2, page3]. Ciò significa che abbiamo accesso ai modelli di tutte le viste. Questo non è necessariamente auspicabile, ma in questo caso è così. Nella versione finale del client Angular, useremo questa funzionalità per memorizzare nel modello [main] le informazioni che devono essere condivise tra le viste. Ciò sarà analogo al concetto di "sessione" lato server;
- riga 6: recuperiamo il modello [page1] per la pagina 1 da [$scope] e poi lo utilizziamo (riga 7). Otteniamo quindi la seguente visualizzazione:
![]() |
Le viste [/page2] e [/page3] sono costruite sullo stesso modello della vista [/page1] (vedi le schermate a pagina 240).
3.7.15.8. Controllo di navigazione
Ora vogliamo impostare la navigazione nel modo seguente [pagina1 --> pagina2 --> pagina3 --> pagina1]. Pertanto, se l'utente si trova sulla pagina 1 [/pagina1] e digita l'URL [/pagina3] nel browser, questa navigazione non dovrebbe essere accettata e l'utente dovrebbe rimanere sulla pagina 1.
Per ottenere questo risultato, modifichiamo i controller delle pagine come segue:
angular.module("rdvmedecins")
.controller('page1Ctrl', ['$scope', '$location',
function ($scope, $location) {
// authorized navigation?
var main = $scope.main;
if (main.lastUrl && main.lastUrl != '/page3') {
// we return to the last URL
$location.path(main.lastUrl);
return;
}
// we store the URL of the page
main.lastUrl = '/page1';
// page template
var page1 = $scope.page1;
page1.text = "[Modèle local dans page 1]";
}]);
- riga 12: quando viene visualizzata una pagina, memorizziamo il suo URL nel modello [main.lastUrl]. Qui stiamo utilizzando il concetto di cui abbiamo parlato in precedenza: l'uso del modello [main] per memorizzare le informazioni condivise da tutte le viste. In questo caso, si tratta dell'ultimo URL visitato;
- Il codice nelle righe 4–12 viene duplicato e adattato per le tre viste. Qui ci troviamo nella vista [/page1];
- riga 5: recuperiamo il modello [main];
- riga 6: se il modello [main.lastUrl] esiste ed è diverso da [/page3], allora la navigazione è vietata (l'ultimo URL visitato esiste e non è /page3);
- riga 8: torniamo quindi all'ultimo URL visitato;
Proviamo:
![]() |
- in [1], ci troviamo alla pagina 1 e digitiamo l'URL della pagina 3 in [2];
- in [3], la navigazione non è avvenuta e siamo tornati all'URL della pagina 1;
3.7.16. Conclusione
Abbiamo trattato tutti i casi d'uso che incontreremo nella versione finale del client Angular. Quando lo presenteremo, ci concentreremo più sulle funzionalità dell'applicazione che sui dettagli di implementazione. Per questi ultimi, faremo semplicemente riferimento all'esempio che illustra il caso d'uso in esame.
3.8. Il client Angular finale
3.8.1. Struttura del progetto
Il progetto finale si presenta così:
![]() |
![]() |
- in [1], l'intero progetto. [app.html] è la pagina master dell'applicazione;
- in [2], i controller;
- in [3], le direttive;
- in [4], i servizi e il modulo Angular [main.js] dell'applicazione;
- in [5], le varie viste inserite nella pagina master [app.html];
3.8.2. Dipendenze del progetto
Le dipendenze del progetto sono le seguenti:
![]() |
Il ruolo di questi vari elementi è stato spiegato nella Sezione 3.4, a pagina 134.
3.8.3. La pagina master [app.html]
La pagina master è la seguente:
<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
<title>RdvMedecins</title>
<!-- META -->
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Angular client for RdvMedecins">
<meta name="author" content="Serge Tahé">
<!-- on CSS -->
<link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.min.css"/>
<link href="bower_components/bootstrap/dist/css/bootstrap-theme.min.css" rel="stylesheet"/>
<link href="bower_components/bootstrap-select/bootstrap-select.min.css" rel="stylesheet"/>
<link href="assets/css/rdvmedecins.css" rel="stylesheet"/>
<link href="assets/css/footable.core.min.css" rel="stylesheet"/>
</head>
<!-- controller [appCtrl], model [app] -->
<body ng-controller="appCtrl">
<div class="container">
...
</div>
<!-- Bootstrap core JavaScript ================================================== -->
<script type="text/javascript" src="bower_components/jquery/dist/jquery.min.js"></script>
<script type="text/javascript" src="bower_components/bootstrap/dist/js/bootstrap.min.js"></script>
<script type="text/javascript" src="bower_components/bootstrap-select/bootstrap-select.min.js"></script>
<script src="bower_components/footable/js/footable.js" type="text/javascript"></script>
<!-- angular js -->
<script type="text/javascript" src="bower_components/angular/angular.min.js"></script>
<script type="text/javascript" src="bower_components/angular-ui-bootstrap-bower/ui-bootstrap-tpls.min.js"></script>
<script type="text/javascript" src="bower_components/angular-route/angular-route.min.js"></script>
<script type="text/javascript" src="bower_components/angular-translate/angular-translate.min.js"></script>
<script type="text/javascript" src="bower_components/angular-base64/angular-base64.min.js"></script>
<!-- modules -->
<script type="text/javascript" src="modules/main.js"></script>
<!-- services -->
<script type="text/javascript" src="services/config.js"></script>
<script type="text/javascript" src="services/dao.js"></script>
<script type="text/javascript" src="services/utils.js"></script>
<!-- guidelines -->
<script type="text/javascript" src="directives/waiting.js"></script>
<script type="text/javascript" src="directives/errors.js"></script>
<script type="text/javascript" src="directives/footable.js"></script>
<script type="text/javascript" src="directives/debug.js"></script>
<script type="text/javascript" src="directives/list.js"></script>
<!-- controllers -->
<script type="text/javascript" src="controllers/appController.js"></script>
<script type="text/javascript" src="controllers/loginController.js"></script>
<script type="text/javascript" src="controllers/homeController.js"></script>
<script type="text/javascript" src="controllers/agendaController.js"></script>
<script type="text/javascript" src="controllers/resaController.js"></script>
</body>
</html>
- riga 18: si noti che [appCtrl] è il controller della pagina master;
- righe 19–21: il contenuto della pagina master;
Questo contenuto è il seguente:
<div class="container">
<!-- navigation bars -->
<ng-include src="'views/navbar-start.html'" ng-show="app.navbarstart.show"></ng-include>
<ng-include src="'views/navbar-run.html'" ng-show="app.navbarrun.show"></ng-include>
<!-- the jumbotron -->
<ng-include src="'views/jumbotron.html'"></ng-include>
<!-- page title -->
<div class="alert alert-info" ng-show="app.titre.show" translate="{{app.titre.text}}"
translate-values="{{app.titre.model}}"></div>
<!-- page errors -->
<errors model="app.errors" ng-show="app.errors.show"></errors>
<!-- the waiting message -->
<waiting model="app.waiting" ng-show="app.waiting.show"></waiting>
<!-- the current view -->
<ng-view></ng-view>
<!-- debug -->
<debug model="app" ng-show="app.debug.on"></debug>
</div>
Indipendentemente dalla vista visualizzata, questa conterrà sempre i seguenti elementi:
- righe 3-4: una barra dei comandi. Le due barre alle righe 3 e 4 si escludono a vicenda;
![]()
![]()
- riga 6: un logo/testo dell'applicazione:

- riga 8: un titolo

- riga 11: un messaggio di errore:

- riga 13: un messaggio di caricamento:

- riga 17: informazioni di debug:

Tutti gli elementi sopra indicati sono controllati da una direttiva [ng-show / ng-hide], il che significa che, anche se presenti, non sono necessariamente visibili.
3.8.4. Le viste dell'applicazione
Nel codice della pagina master abbiamo:
<div class="container">
...
<!-- the current view -->
<ng-view></ng-view>
...
</div>
La riga 4 riceve le varie viste dell'applicazione. Queste sono definite nel modulo [main.js]:

Il ruolo della configurazione dei diversi percorsi è stato spiegato nella sezione 3.7.15.4, a pagina 242.
La vista [login.html] è vuota, il che significa che non aggiunge alcun elemento a quelli già presenti nella pagina master.
La vista [home.html] aggiunge il seguente elemento alla pagina master:

La vista [agenda.html] aggiunge il seguente elemento alla pagina master:

La vista [resa.html] aggiunge il seguente elemento alla pagina master:

3.8.5. Funzionalità dell'applicazione
Le viste client Angular sono già state presentate nella Sezione 1.3.3, a pagina 7. Per rendere questo nuovo capitolo più facile da leggere, le ripetiamo qui. La prima vista è la seguente:
![]() |
- [6], la pagina di accesso all'applicazione. Si tratta di un'applicazione per la prenotazione di appuntamenti medici;
- in [7], una casella di controllo che consente all'utente di abilitare o disabilitare la modalità [debug]. Questa modalità è caratterizzata dalla presenza del pannello [8], che mostra il modello della vista corrente;
- in [9], un tempo di attesa artificiale in millisecondi. Il valore predefinito è 0 (nessuna attesa). Se N è il valore di questo tempo di attesa, qualsiasi azione dell'utente verrà eseguita dopo un tempo di attesa di N millisecondi. Ciò consente di osservare la gestione dell'attesa implementata dall'applicazione;
- in [10], l'URL del server Spring 4. In base a quanto preceduto, questo è [http://localhost:8080];
- in [11] e [12], il nome utente e la password dell'utente che desidera utilizzare l'applicazione. Ci sono due utenti: admin/admin (login/password) con un ruolo (ADMIN) e user/user con un ruolo (USER). Solo il ruolo ADMIN ha il permesso di utilizzare l'applicazione. Il ruolo USER è incluso esclusivamente per dimostrare la risposta del server in questo caso d'uso;
- in [13], il pulsante che consente di connettersi al server;
- in [14], la lingua dell'applicazione. Ce ne sono due: francese (predefinita) e inglese.
![]() |
- in [1], si effettua l'accesso;
![]() |
- una volta effettuato l'accesso, puoi scegliere il medico con cui desideri fissare un appuntamento [2] e la data dell'appuntamento [3];
- In [4], richiedi di visualizzare l'orario del medico selezionato per il giorno scelto;
![]() |
- Una volta visualizzato l'orario del medico, puoi prenotare una fascia oraria [5];
![]() |
- In [6], selezionare il paziente per l'appuntamento e confermare la selezione in [7];
![]() |
Una volta confermato l'appuntamento, si torna automaticamente al calendario, dove il nuovo appuntamento è ora elencato. Questo appuntamento può essere cancellato in un secondo momento [7].
Le funzionalità principali sono state descritte. Sono semplici. Quelle non descritte sono funzioni di navigazione per tornare a una vista precedente. Concludiamo con le impostazioni della lingua:
![]() |
- In [1], la lingua passa dal francese all'inglese;
![]() |
- in [2], la visualizzazione passa all'inglese, compreso il calendario;
3.8.6. Il modulo [main.js]
Il modulo [main.js] definisce il modulo Angular che controllerà l'applicazione:
![]() |
- riga 4: il modulo si chiama [rdvmedecins];
- riga 5: il modulo [ngRoute] viene utilizzato per il routing degli URL;
- riga 6: il modulo [translate] viene utilizzato per l'internazionalizzazione del testo;
- riga 7: il modulo [base64] viene utilizzato per codificare la stringa «login:password» in Base64;
- riga 8: il modulo [ngLocale] viene utilizzato per internazionalizzare il calendario;
- riga 9: il modulo [ui.bootstrap] viene utilizzato per il calendario;
- riga 12: configurazione del percorso;
- riga 40: internazionalizzazione dei messaggi;
3.8.7. Il controller della pagina master
Rivediamo il codice HTML della pagina master [app.html]:
<body ng-controller="appCtrl">
<div class="container">
...
Riga 1: L'intero corpo della pagina master è controllato dal controller [appCtrl]. Data la sua posizione, questo lo rende il controller generale e principale dell'applicazione. Come spiegato nella Sezione 3.7.15, il modello creato da questo controller viene ereditato da tutte le viste che verranno inserite nella pagina master.
Il suo codice è il seguente:
angular.module("rdvmedecins")
.controller("appCtrl", ['$scope', 'config', 'utils', '$location', '$locale',
function ($scope, config, utils, $location, $locale) {
// debug
utils.debug("[app] init");
// ----------------------------------------initialisation page
// templates for # pages
$scope.app = {waitingTimeBeforeTask: config.waitingTimeBeforeTask};
$scope.login = {};
$scope.home = {};
$scope.agenda = {};
$scope.resa = {};
// current page template
var app = $scope.app;
...
// ---------------------------------- méthodes
// cancel current job
app.cancel = function () {
...
};
// disconnect
app.deconnecter = function () {
...
};
// this code must remain here as it refers to the preceding [cancel] function
app.waiting = {title: {text: config.msgWaitingInit, values: {}}, cancel: app.cancel, show: true};
}])
;
Le righe 10–14 definiscono i cinque modelli utilizzati nell'applicazione:
app.html | appCtrl | |
login.html | loginCtrl | |
home.html | homeCtrl | |
prenotazione.html | resaCtrl | |
agenda.html | agendaCtrl |
È importante comprendere che l'oggetto [$scope], essendo il modello del controller della pagina master, viene ereditato da tutte le viste e da tutti i controller. Pertanto, il controller [loginCtrl] ha accesso agli elementi [$scope.app, $scope.login, $scope.home, $scope.resa, $scope.agenda]. In altre parole, un controller ha accesso agli ambiti di altri controller. L'applicazione in esame evita accuratamente di utilizzare questa funzionalità. Pertanto, ad esempio, il controller [loginCtrl] opera solo con due ambiti:
- il proprio [$scope.login];
- e quello del controller padre [$scope.app];
Lo stesso vale per tutti gli altri controller. Il modello [$scope.app] verrà utilizzato come memoria condivisa tra i diversi controller. Quando un controller C1 deve passare informazioni al controller C2, viene seguita la seguente procedura:
In [C1]:
In [C2]:
In entrambi i casi, $scope viene ereditato dal controller [appCtrl] ed è quindi identico (è un puntatore) in [C1] e [C2]. L'oggetto [$scope.app], che funge da memoria condivisa tra i controller, viene spesso indicato come "sessione" nei commenti, imitando la sessione utilizzata nelle applicazioni web tradizionali, che si riferisce alla memoria condivisa tra richieste HTTP successive.
Torniamo al codice del controller [appCtrl]:
// templates for # pages
$scope.app = {waitingTimeBeforeTask: config.waitingTimeBeforeTask};
$scope.login = {};
$scope.home = {};
$scope.agenda = {};
$scope.resa = {};
// current page template
var app = $scope.app;
// [app.debug] and [utils.verbose] must always be synchronized
app.debug = utils.verbose;
app.debug.on = config.debug;
// no page title for the moment
app.titre = {show: false};
// no navigation bars
app.navbarrun = {show: false};
app.navbarstart = {show: false};
// no errors
app.errors = {show: false};
// local default
angular.copy(config.locales['fr'], $locale);
// the current view
app.view = {url: undefined, model: {}, done: false};
// the current task
app.task = app.view.model.task = {action: utils.waitForSomeTime(app.waitingTimeBeforeTask), isFinished: false};
- riga 8: [$scope.app] sarà il modello per la pagina master. Fungerà anche da memoria condivisa tra i vari controller. Anziché scrivere [$scope.app.field=value] ovunque, il puntatore [$scope.app] viene assegnato alla variabile [app], quindi scriviamo [app.field=value]. Basta ricordare che [app] è il modello esposto alla pagina master;
- riga 11: [app.debug.on] è un valore booleano che controlla la modalità di debug dell'applicazione. Per impostazione predefinita, è impostato su true. Il suo valore è collegato alla casella di controllo [debug] nelle barre di navigazione;
- riga 15: [app.navbarrun.show] controlla la visualizzazione della seguente barra di navigazione:
![]()
- riga 16: [app.navbarstart.show] controlla la visualizzazione della seguente barra di navigazione:
![]()
- riga 18: [app.errors] è il modello per il banner di errore;

- Riga 22: [app.view] conterrà informazioni sulla vista corrente, ovvero quella attualmente visualizzata dal tag [ng-view] nella pagina master. Includeremo lì le seguenti informazioni:
- [url]: l'URL della vista corrente, ad esempio [/agenda];
- [model]: il modello della vista corrente, ad esempio [$scope.agenda];
- [done]: se vero, indica che la vista corrente ha terminato il suo lavoro e che stiamo passando a un'altra vista;
Queste informazioni vengono utilizzate per controllare la navigazione.
- riga 24: avvia un'attività asincrona, un'attesa simulata. L'attività asincrona è indicata da due puntatori [app.view.model.task.action] e [app.task];
Due metodi sono stati integrati nel controller [appCtrl]:
// cancel current job
app.cancel = function () {
...
};
// disconnect
app.deconnecter = function () {
...
};
- riga 2: la funzione [app.cancel] viene utilizzata per annullare l'operazione corrente per la quale è attualmente visualizzato un messaggio di caricamento. Tutte le viste mostrano questo messaggio, quindi l'operazione verrà annullata qui;
- riga 7: la funzione [app.logout] riporta l'utente alla pagina di login. Tutte le viste, eccetto la vista [/login], offrono questa opzione;
La funzione [app.deconnecter] è la seguente:
// disconnect
app.deconnecter = function () {
// we return to the login page
$location.path(config.urlLogin);
};
- riga 4: ritorno alla pagina di login all'URL [/login];
3.8.8. Gestione delle attività asincrone
Nella nostra applicazione, in un dato momento, verrà eseguita una sola attività asincrona. È possibile avere più attività in esecuzione. Ad esempio, all'avvio, l'applicazione richiede l'elenco dei medici dal servizio web e poi l'elenco dei clienti con due richieste HTTP successive. Potremmo fare la stessa cosa con due richieste HTTP simultanee. Angular fornisce gli strumenti per farlo. In questo caso, non abbiamo scelto quell'approccio.
L'attività attualmente in esecuzione viene annullata con il seguente codice nel controller [appCtrl]:
// cancel current job
app.cancel = function () {
utils.debug("[app] cancel task");
// cancel the current view's asynchronous task
var task = app.view.model.task;
task.isFinished = true;
task.action.reject();
...
};
- riga 5: l'attività viene recuperata da [app.view.model.task]. Pertanto, tutti i controller si assicureranno che le loro attività asincrone siano referenziate da questo oggetto;
- riga 6: per indicare che l'attività è terminata;
- riga 7: per terminare l'attività con un errore. Questa notazione differisce da quella utilizzata negli esempi Angular studiati:
- negli esempi, l'oggetto [task] era un oggetto [$q.defer()] che poteva essere terminato;
- nella versione finale, l'oggetto [task] è un oggetto con i campi [action, isFinished], dove [action] è l'oggetto [$q.defer()] che può essere terminato e [isFinished] è un valore booleano che indica che l'azione è completa;
Esaminiamo il ciclo di vita dell'oggetto [task] utilizzando un esempio. All'avvio, dopo il controller [appCtrl], il controller [loginCtrl] prende il controllo per visualizzare la vista [views/login.html]. Il suo codice di inizializzazione è il seguente:
// retrieve the parent model
var login = $scope.login;
var app = $scope.app;
// current view
app.view = {url: config.urlLogin, model: login, done: false};
Alla riga 5 abbiamo [model=login]. Ciò significa che quando modifichiamo l'oggetto [login], modifichiamo l'oggetto [app.view.model], ovvero [$scope.app.view.model]. Quando vogliamo simulare un'attesa nel controller [loginCtrl], scriviamo:
// simulated waiting
var task = login.task = {action: utils.waitForSomeTime(app.waitingTimeBeforeTask), isFinished: false};
Aggiungendo il campo [task] all'oggetto [login], questo viene quindi aggiunto all'oggetto [$scope.app.view.model]. Se l'utente annulla l'attesa, il codice in [appCtrl.cancel]:
// current page template
var app = $scope.app;
...
var task = app.view.model.task;
task.isFinished = true;
task.action.reject();
completerà con successo l'attesa simulata (righe 4–6).
3.8.9. Controllo della navigazione
Le regole di navigazione utilizzate nell'applicazione sono le seguenti:
qualsiasi | sì | |
/login | sì se il controller [loginCtrl] ha segnalato di aver terminato il proprio lavoro | |
/home | sì | |
/calendario | sì | |
/home | sì se il controller [homeCtrl] ha segnalato di aver terminato il proprio lavoro | |
/reset | sì | |
/agenda | sì | |
/calendario | sì se il controller [homeCtrl] ha segnalato di aver terminato il proprio lavoro | |
/reset | sì |
Questo viene implementato con il seguente codice:
Per [agendaCtrl]:

- righe 11–20: implementazione della regola di navigazione;
- riga 26: nuova vista corrente;
Per [resaCtrl]:

- righe 12–20: implementazione della regola di navigazione:
- riga 27: nuova vista corrente;
Per [loginCtrl]:

- Qui non c'è alcun controllo di navigazione poiché la regola stabilisce che l'URL [/login] è accessibile da qualsiasi punto. Pertanto, se l'utente digita questo URL nel proprio browser, funzionerà indipendentemente dalla vista corrente;
- riga 16: la nuova vista corrente;
Il codice per il controller [homeCtrl] è stato fornito nella sezione 3.8.7.
Infine, per una regola del tipo:
/home | sì, se il controller [homeCtrl] ha indicato di aver terminato il proprio lavoro |
Ecco un esempio di codice che naviga dall'URL [/home] all'URL [/agenda]:
![]() |
Sopra, ci troviamo nel metodo [displayCalendar] del controller [homeCtrl]. L'utente ha richiesto il calendario di un medico.
- riga 107: la promessa dell'attività HTTP;
- riga 109: la variabile [app] è stata inizializzata con [$scope.app]. Come abbiamo visto, questo oggetto viene utilizzato come modello per la vista [app.html]. Questo modello [$scope.app] viene utilizzato anche per memorizzare le informazioni che devono essere condivise tra le viste;
- riga 111: viene analizzato il codice di errore restituito dall'attività;
- riga 113: il risultato [result.data] viene inserito nel modello [app];
- riga 116: il controller [homeCtrl] passerà il testimone al controller [agendaCtrl]. Indica che ha terminato il suo lavoro con il codice alla riga 115. Questo codice verrà utilizzato dal controller [agendaCtrl] come segue:

- riga 11: viene recuperato l'oggetto [$scope.app.view];
- riga 15: elaborazione del campo [$scope.app.view.done] inizializzato da [homeCtrl];
3.8.10. Servizi
![]() |
I servizi [config, utils, dao] sono quelli già descritti nella panoramica su Angular:
- il servizio [config] è stato presentato nella sezione 3.7.4;
- il servizio [utils] è stato presentato nella sezione 3.7.5;
- il servizio [dao] è stato presentato nella sezione 3.7.6;
Come promemoria, ecco la struttura di questi servizi:
Servizio [config]
![]() |
- in [1]: vediamo che il codice è lungo circa 250 righe. La maggior parte di questo codice riguarda l'esternalizzazione delle chiavi per i messaggi internazionalizzati [2]. Evitiamo di hard-codificare queste chiavi direttamente nel codice;
servizio [utils]
![]() |
- Riga 8: Non abbiamo ancora incontrato la variabile [verbose]. Essa controlla la funzione [debug] come segue:
![]() |
- Righe 22–25: La funzione [utils.debug] non fa nulla se [verbose.on] restituisce false. Questa variabile è associata a una variabile nel controller [appCtrl]:
![]() |
- riga 21: [app.debug] assume il valore del puntatore [utils.verbose]. Pertanto, qualsiasi modifica apportata a [app.debug] verrà applicata anche a [utils.verbose];
- riga 22: il valore iniziale di [app.debug.on] viene prelevato dal file di configurazione. Per impostazione predefinita, è impostato su true. Questo valore può cambiare nel tempo. L'utente può modificarlo tramite le barre di navigazione:
![]() |
- riga 45: una casella di controllo (type=checkbox) consente di modificare il valore di [app.debug.on] (attributo ng-model);
[dao] Servizio
![]() |
3.8.11. Direttive
![]() |
Le direttive [errors, footable, list, waiting] sono quelle già descritte nella panoramica su Angular:
- la direttiva [footable] è stata introdotta nella sezione 3.7.8.6;
- la direttiva [list] è stata introdotta nella sezione 3.7.12;
- le direttive [errors] e [waiting] sono state introdotte nella sezione 3.7.14;
Non avevamo ancora incontrato la direttiva [debug]. È la seguente:
![]() |
Il file [debug.html] a cui si fa riferimento alla riga 11 è il seguente:
- riga 2: la direttiva [debug] visualizza il proprio modello in formato JSON in un banner Bootstrap (riga 1);
Questa direttiva viene utilizzata solo nella pagina master [app.html]:
![]() |
- La direttiva [debug] viene utilizzata alla riga 35. Visualizza quindi la rappresentazione JSON del modello [$scope.app] quando si è in modalità debug (attributo ng-show). Questo produce un output simile al seguente:
![]() |
Ciò richiede una buona comprensione del codice da interpretare, ma una volta acquisita, le informazioni di cui sopra diventano utili per il debug. Qui abbiamo evidenziato gli elementi del modello [$scope.app] visualizzato. Ricordiamo che [$scope.app] è la memoria condivisa tra i controller;
- [waitingBeforeTask]: il tempo di attesa simulato prima di qualsiasi richiesta HTTP;
- [debug]: modalità debug — è necessariamente vero se viene visualizzato questo banner;
- [navbarrun]: un valore booleano che controlla la visualizzazione della seguente barra di navigazione:
![]()
- [navbarstart]: un valore booleano che controlla la visualizzazione della seguente barra di navigazione:
![]()
- [errors]: modello per la direttiva [errors];
- [view]: incapsula le informazioni relative alla vista attualmente visualizzata;
- [waiting]: modello per la direttiva [waiting];
- [serverUrl, username, password]: credenziali di accesso per il servizio web;
- [doctors]: modello per la direttiva [list] applicata ai medici;
- [clients]: come sopra per i clienti;
- [menu]: controlla le opzioni del menu visualizzate. Queste sono definite in [navbar-run.html]:

Le opzioni del menu si trovano alle righe 16, 23, 29 e 36.
- [formattedDay]: il giorno selezionato nel calendario nel formato 'yyyy-mm-dd';
- [agenda]: l'agenda del medico. Contiene gli slot disponibili (rv==null) e quelli prenotati. Per questi ultimi, include il nome del cliente che ha effettuato la prenotazione;
- [selectedCreneau]: la fascia oraria selezionata per effettuare una prenotazione;
3.8.12. Il controller [loginCtrl]
![]() |
Il controller [loginCtrl] è associato alla vista [views/login.html], che, una volta combinata con la pagina master, genera la seguente pagina:

Il controller [loginCtrl] è il seguente:

- riga 13: [login] sarà il modello per la vista corrente;
- riga 14: [app] è la memoria condivisa tra i controller;
- riga 16: [app.view] viene popolato con le informazioni della vista corrente;
Questo codice di inizializzazione si troverà in ogni controller. Per il controller C1 di una vista V1 con modello M1, avremo il seguente codice di inizializzazione:
- riga 18: ricorderete che [appCtrl] ha avviato un'attesa simulata a cui fa riferimento l'oggetto [app.task.action]. Usiamo la [promise] di questa attività per attendere che finisca;
- riga 39: il metodo [login.setLang] gestisce il cambio di lingua;
- riga 47: il metodo [login.authenticate] gestisce l'autenticazione dell'utente;
Diamo un'occhiata alle fasi principali del metodo di autenticazione:

- righe 50–51: [app.waiting] è il modello per il banner di caricamento;
- riga 53: [app.errors] è il modello per il banner di errore;
- riga 55: viene avviata un'attesa simulata. L'oggetto [action, isFinished] è referenziato da [login.task] e quindi, poiché [app.view.model=login], da [app.view.model.task]. Ricordiamo che questa è la condizione affinché l'attività venga annullata;
- riga 57: al termine dell'attesa simulata, vengono caricati i medici;
- riga 62: una volta recuperati i medici, viene analizzata la loro richiesta. Se i medici sono stati ottenuti, vengono quindi richiesti i clienti;
- riga 83: la risposta viene analizzata e viene visualizzata la vista finale. Ciò avviene con il seguente codice:

- Riga 87: il valore booleano [task.isFinished] viene impostato su true nei seguenti casi:
- l'utente ha annullato l'attesa;
- la richiesta dei medici si è conclusa con un errore;
- righe 91–98: il caso in cui abbiamo i clienti;
- riga 93: [app.clients] è il modello per la direttiva [list] che visualizzerà i clienti in un elenco a discesa;
- righe 97–98: ci prepariamo a cambiare vista (riga 98) ma prima indichiamo che il controller ha terminato il suo lavoro (riga 97). Ricordiamo che [$scope.app.view.done] è utilizzato per il controllo della navigazione;
Il punto importante da notare qui è che i medici e i clienti sono stati memorizzati nella cache del browser. Non saranno più richiesti al servizio web.
3.8.13. Il controller [homeCtrl]
![]() |
Il controller [homeCtrl] è associato alla vista [views/home.html], che, una volta combinata con la pagina master, produce la seguente pagina:

La struttura del controller [homeCtrl] è la seguente:

- righe 12–20: questo è il controllo di navigazione. Tutti i controller lo possiedono tranne [loginCtrl] poiché la pagina [/login.html] è accessibile senza condizioni;

- righe 25–28: qui troviamo righe simili a quelle nel controller [loginCtrl]. [home] è quindi il modello di vista associato al controller;
- riga 33: un attributo che non abbiamo ancora incontrato. Questo è il modello per la barra di intestazione della vista:
![]()
- riga 36: [home.datepicker] è il modello per il calendario;
- riga 38: [app.menu] è il modello per il menu della barra di navigazione. Qui sarà presente l'opzione [Schedule]. È ciò che consente di richiedere l'orario di un medico;
Infine, il controller ha due metodi:

La visualizzazione dell'agenda (riga 51) è stata trattata nella sezione 3.7.8.
3.8.14. Il controller [agendaCtrl]
![]() |
Il controller [agendaCtrl] è associato alla vista [views/agenda.html], che, una volta combinata con la pagina master, produce la seguente pagina:

La struttura del controller [agendaCtrl] è la seguente:

- le righe 10–20 gestiscono il controllo della navigazione;

- righe 23–26: [agenda] sarà il modello di visualizzazione associato al controller [agendaCtrl];
- righe 36–44: [app.title] è il modello per la seguente barra del titolo:

- riga 46: il menu avrà l'opzione [Home]:
![]()
I metodi del controller sono i seguenti:

- riga 95: il metodo [agenda.delete] è stato discusso nella sezione 3.7.9;
Il metodo [agenda.home] è un metodo di navigazione puro:

Il metodo [agenda.reserve] è il seguente:

- riga 73: il parametro della funzione [reserve] è il numero dello slot (id);
- righe 77–86: cercano di trovare la fascia oraria con questo identificatore;
- riga 82: lo slot temporale trovato viene inserito nella memoria condivisa [app]. Il controller [resaCtrl], che subentrerà (riga 90), utilizzerà queste informazioni per visualizzare la propria barra del titolo;
- righe 89-90: si passa a [/resa.html];
3.8.15. Il controller [resaCtrl]
![]() |
Il controller [resaCtrl] è associato alla vista [views/resa.html], che, una volta combinata con la pagina master, produce la seguente pagina:

La struttura del controller [resaCtrl] è la seguente:

- righe 12–20: controllo della navigazione;

- righe 24-27: [resa] sarà il modello per la vista corrente;
- righe 38–45: [app.titre] è il modello per la barra del titolo seguente:

- riga 47: vengono visualizzate due opzioni di menu:
![]()
I metodi del controller sono i seguenti:

Il metodo [resa.valider] è stato trattato nella sezione 3.7.9.
3.8.16. Gestione della lingua
Tutti i controller forniscono il seguente metodo [setLang]:

Avrebbe potuto essere integrato nel controller [appCtrl].




























































































































