Dove sono stato nell’ultimo anno? Sembra ieri che parlavamo di Angular 2 ancora alla RC1 e, il tempo di distrarsi un attimo, quel 2 è divento un 4!! In realtà, c’è una spiegazione: con l’adozione del Semantic Versioning, ogni release che provoca rottura col passato richiede il salto di una major version.. e di rotture ce ne sono state! Già con il rifacimento (o gli n rifacimenti) del componente di routing si parlava “per scherzo” di Angular 3 tanto era diverso. Adesso, con questo sistema di versionamento, è ufficiale che seguirà una release major ogni circa 6 mesi: il numero accanto ad Angular quindi comincerà ben presto a perdere senso (ai livelli di quello di Chrome??), e si parlerà semplicemente di Angular (il JS era già sparito l’anno scorso…).
Vediamo quindi più da vicino cosa è cambiato rispetto a quello che avevamo visto appena un anno fa con Angular 2 quando ancora era in RC1!
Disclaimer
Prima di procedere, è consigliabile dare un’occhiata ai post precedenti (parte 1 e parte 2) perché ci sono molti riferimenti e confronti che vengono dati per scontati, oltre ai concetti principali che sono rimasti invariati. Questo post è indicato soprattutto per chi ha trascurato (come me) Angular nell’ultimo anno e si ritrova sintatticamente e concettualmente molte differenze che deve recuperare. Molte delle cose che vedremo non sono nate con la versione 4: faremo infatti una panoramica, anche piuttosto teorica, di cosa offre il framework oggi, per cui si dà per scontato che lo si conosca già un po’.
Per fare i confronti, sull’account GitHub di CNJ trovate la classica applicazione TODO List aggiornata ad Angular 4, facilmente confrontabile con la versione scritta in Angular 2 (RC1).
Ritornano i moduli
Nelle prime versioni di Angular 2 era scomparso il concetto esplicito di Modulo Angular (che già conoscevamo in AngularJS). In pratica bastava definire una gerarchia di componenti (come app.component.ts), con le sue dipendenze, ed eseguire il “bootstrap” del componente radice (come in main.ts): adesso il modulo ritorna protagonista, definito dal decoratore @NgModule
, con una serie di attributi importanti, dichiarato sopra una classe TypeScript (aggiornato ad oggi versione 2.2), come per esempio app.module.ts.
La nuova versione dell’applicazione TodoList aggiornata ad Angular 4 ci viene in aiuto: vediamo da vicino gli elementi salienti.
@NgModule({ declarations: [ AppComponent, TodoFormComponent, TodosComponent, TodoListComponent ], imports: [ BrowserModule, FormsModule, HttpModule ], providers: [TodoListService], bootstrap: [AppComponent] }) export class AppModule { }
@NgModule
riceve come argomento un oggetto di “metadati” con questa semantica:
- declarations
- elenca le cosiddette view class che fanno parte del modulo, ovvero: componenti, direttive, pipes (concetti rimasti invariati)
- imports
- dipendenze di altri moduli
- providers
- sono i provider dei servizi disponibili nell’applicazione
- bootstrap
- il componente radice dell’albero dei componenti applicativi. Solo il modulo che a sua volta è la radice dell’applicazione definisce questo parametro: esso sta tra il “bootstrapper” e i componenti.
- exports
- (non presente in questo caso) sono quel sottoinsieme di elementi del modulo “pubblici“, ovvero riusabili nei template di altri moduli. Solitamente sono i moduli non di bootstrap (definiti Feature Modules) a dichiarare gli exports.
Il fatto quindi di aver definito bootstrap e l’import di BrowserModule
, identifica subito questo modulo come radice, candidato ad essere il modulo di bootstrap appunto, importato a sua volta in un file solitamente chiamato main.ts che avvia l’applicazione.
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; platformBrowserDynamic().bootstrapModule(AppModule);
Prima quindi si avviava un componente che implicitamente veniva identificato come radice; adesso invece si avvia un modulo che dichiara chi è il componente principale. Si recupera quindi la visione di insieme di un modulo e delle sue dipendenze.
Nella versione Angular 2, era tutto più diluito tra i primi due livelli di componenti (app.component.ts e todos.component.ts). Adesso invece è ben chiaro il confine di un modulo, perché dichiarato tutto in un file (app.module.ts): è costituito non solo da componenti, direttive, servizi e pipes, ma anche da altri moduli, chiamati Feature Modules.
Questa distinzione è principalmente a livello logico, anche se si distinguono visivamente per il fatto di importare il CommonModule
invece del BrowserModule
, di non dichiarare il bootstrap e di definire gli exports. Di fatto un Root Module esegue una applicazione, un Feature Module invece ne estende le funzionalità.
AOT vs JIT: bootstrap statico contro bootstrap dinamico
Come abbiamo detto, un Root Module è responsabile di avviare l’applicazione, ma come si fa? Quello che abbiamo appena visto tramite la funzione platformBrowserDynamic
viene definito bootstrap dinamico, dove l’applicazione viene compilata al momento (JIT – Just-In-Time) nel browser dal compilatore Angular e poi viene avviata. Cosa fa il compilatore Angular? In poche parole legge i template e li combina con quanto dichiarato nella classe del componente per generare un Module Factory e tanti Components Factories in memoria, ovvero una rappresentazione in puro JavaScript dei componenti, incluso l’HTML, il CSS e i vari binding.
In ambiente di produzione, soprattutto su hardware poco performante (il mobile per esempio) o reti lente, il download anche del compiler e della compilazione dell’app su browser è un’inutile perdita di tempo che può essere evitata con una compilazione preventiva (AOT – Ahead-Of-Time): eseguita in fase di build del progetto, essa produce una factory per il modulo root della nostra applicazione chiamato AppModuleNgFactory
, proprio perché il modulo si chiamava AppModule
. Il bootstrap statico viene eseguito in modo molto simile, grazie alla funzione platformBrowser
:
import { platformBrowser } from '@angular/platform-browser'; import { AppModuleNgFactory } from './app/app.module.ngfactory'; platformBrowser().bootstrapModuleFactory(AppModuleNgFactory);
Dal momento che non è più necessario compilare l’applicazione nel browser, il compiler non è incluso negli artefatti finali, che di conseguenza saranno più piccoli rispetto alla versione dinamica e avranno tempi di boot più veloci.
Per fortuna nel mondo reale non dobbiamo preoccuparci di questi dettagli se lasciamo gestire il nostro progetto ad Angular CLI (che vedremo prossimamente in un post a parte).
I nuovi moduli principali
Nel codice di AppModule avevamo importato tre moduli di Angular di fondamentale importanza: BrowserModule
, FormsModule
e HttpModule
, che sostituiscono gli import di “gruppo” come FORMS_DIRECTIVES o HTTP_PROVIDERS (che in realtà esistono ancora, ma è meglio importare direttamente il modulo). Ma a cosa servono?
-
BrowserModule
è il componente base per lavorare con i template HTML nel browser. Grazie a questo modulo è possibile compilare direttive comeNgIf
eNgFor
(anche se in realtà fanno parte del moduloCommonModule
che è importato e riesportato come dipendenza). -
FormsModule
abilitangModel
eRouterLink
e permette di creare form nello stile “template-driven” (cioè il modo che abbiamo sempre usato in AngularJS). E’ stato introdotto un nuovo tipo di form chiamato “reactive” o “model-driven“, ovvero form create dinamicamente a partire da un modello di metadati che le descrivono. Da tenere sott’occhio perché evolverà parecchio nelle prossime versioni di Angular. -
HttpModule
: definisce il provider dei servizi per le chiamate HTTP. Ancora marcato come “experimental”, indica che potrebbe cambiare in futuro. Sta di fatto che rispetto alla versione di un anno fa, il servizioHttp
ha fatto qualche passo avanti: basta vedere come viene usato in todo-list.service.ts nella versione 2 rispetto alla 4. Serializzare manualmente il payload in JSON o specificare manualmente il content type mi sembrava esagerato…
Moduli e providers
Il concetto di “provider” è rimasto invariato, compreso lo scope dei servizi forniti. Già in Angular 2 avevamo visto una differenza rispetto ad AngularJS: quando si definiva un servizio “provided” da un certo componente, l’istanza era la stessa da lì in giù nel DOM, non a livello applicativo come in AngularJS. Con Angular 4 rimane vero in principio, ma non è più un componente a fornire un servizio, bensì un modulo, come abbiamo già visto dal codice poco sopra.
Il provider del nostro servizio TodoListService è stato spostato quindi da app.component.ts a app.module.ts
Adesso, se il servizio viene fornito a livello di Modulo Radice, è singleton per tutta l’applicazione. Spesso però si vuole dei provider di servizi configurabili, come fare quindi?
Una delle best practices è creare un modulo “core” che fornisce i servizi condivisi in tutta l’applicazione, nonché la possibilità di configurarli tramite un metodo chiamato forRoot
(per convenzione) che restituisca un ModuleWithProviders
.
L’esempio riportato nella documentazione ufficiale chiarisce il concetto:
@NgModule({ providers: [ UserService ] }) export class CoreModule { static forRoot(config: UserServiceConfig): ModuleWithProviders { return { ngModule: CoreModule, providers: [ {provide: UserServiceConfig, useValue: config } ] }; } }
UserService
è il servizio che vogliamo esporre e possibilmente configurare tramite un altra classe UserServiceConfig
. Ecco le loro implementazioni:
export class UserServiceConfig { userName = 'Philip Marlowe'; } @Injectable() export class UserService { private _userName = 'Sherlock Holmes'; constructor(@Optional() config: UserServiceConfig) { if (config) { this._userName = config.userName; } } }
e soprattutto come si usano:
@NgModule({ imports: [ BrowserModule, CoreModule.forRoot({userName: 'Miss Marple'}), AppRoutingModule ], declarations: [ AppComponent ], bootstrap: [ AppComponent ] }) export class AppModule { }
Non bisogna quindi importare direttamente il modulo CoreModule
, ma il risultato della chiamata a forRoot
! Questo è un pattern che ritroveremo piuttosto comunemente.
Routing
Il componente di Routing è stata la causa principale del repentino versionamento di Angular, viste le innumerevoli modifiche che ha subito nell’arco di nemmeno un anno, trasformandosi quasi completamente. Cerchiamo quindi di capire come funziona adesso, evidenziando cosa è cambiato dove possibile.
L’architettura applicativa che si sposa bene con Angular 4 e il suo sistema di routing prevede la suddivisione dell’applicazione in diversi “Feature Modules“, attivabili appunto tramite il routing applicativo, ognuno dei quali può essere a sua volta regolato dal proprio routing “interno”. Ma come decidere come raccogliere i componenti in un modulo? Come prima approssimazione possiamo pensare ad un nuovo modulo ogni volta che creiamo una nuova pagina nell’applicazione (o magari una coppia di pagine master/detail che sono strettamente correlate).
Con questa architettura in mente, il sistema di routing di Angular permette di dichiarare dei “moduli principali” che verranno caricati subito all’avvio dell’applicazione, mentre altri verranno caricati solo all’occorrenza, rendendo più veloce il bootstrap applicativo. Questo sistema viene definito “lazy loading” dei moduli.
Routing: concetti base
Esplorare le potenzialità del routing di Angular 4 richiederebbe un post ad hoc: in questo caso ci occuperemo solo di capire gli elementi fondamentali e cosa è cambiato rispetto a quello che conoscevamo nemmeno un anno fa.
Innanzitutto, il decoratore @Routes
si è trasformato in una classe che descrive l’insieme delle rotte (Route
) di un modulo: al cambio di una view, il servizio Router è responsabile della gestione del cambio vista.
const routes: Routes = [ { path: 'todos', component: TodosComponent }, { path: 'done', component: DoneListComponent }, { path: '', redirectTo: '/todos', pathMatch: 'full' }, { path: '**', component: NotFoundComponent } ]; @NgModule({ imports: [ RouterModule.forRoot(routes), ... ] }) export class AppModule { }
Tutto ha inizio con l’import del risultato alla chiamata a RouterModule.forRoot, a cui viene passato un vettore di configurazione di rotte. Questo metodo accetta come secondo argomento un oggetto che permette di attivare la HashLocationStrategy, ovvero la gestione degli url applicativi separati dal cancelletto (#, come la conosciamo da AngularJS). Adesso invece lo stile di default (e anche quello consigliato) è PathLocationStrategy, ovvero URL in stile HTML5 (modificati lato client tramite history.pushState).
Tornando al codice, la classe Routes
è un alias per Route[]
(vettore di rotte), dove Route
ha diversi attributi, alcuni dei quali si evincono dal frammento di codice sovrastante.
- path: definisce il percorso (assoluto o relativo) della rotta
- componente: componente corrispondente alla rotta
- redirectTo: insieme ad un path vuoto, definisce la rotta di default a cui indirizzare l’utente
- pathMatch: si usa solitamente con i redirect. Può essere full, ovvero che redirectTo deve matchare completamente un path (è quindi assoluto), oppure prefix, usato per matchare rotte annidate.
Nel scegliere la rotta, il servizio Router segue la regola de “il primo che matcha vince“: l’ordine con cui si dichiarano le rotte è quindi importante! E’ molto importante quindi usare per ultime le wildcard per gestire le pagine non trovate (NotFoundComponent
).
Lato HTML invece non è cambiato molto: è sempre necessario specificare l’url base dell’applicazione (
Troviamo qualcosa di diverso nella definizione dei link di navigazione, che si dichiarano così:
La direttiva RouterLink
dichiara il path della rotta, RouterLinkActive
invece aggiunge la classe “active” quando la rotta è attiva (da notare che può andare sull’ancora che sui tag padre come in questo caso).
All’interno del componente attivato, è possibile ottenere informazioni sulla rotta corrente tramite il servizio ActivatedRoute
. Per esempio, possiamo estrarre i “query parameters” o i “route parameters” sottoscrivendo rispettivamente l’observable queryParams o params del servizio initettato:
export class DoneComponent implements OnInit { private id: number; constructor(private activatedRoute: ActivatedRoute) { } ngOnInit() { this.activatedRoute.params .subscribe(params => { this.id = params.id; }); } }
Se devo essere sincero all’inizio capivo come mai scomodare gli observable per dei valori che sono lì nell’url: la documentazione ufficiale fortunatamente lo spiega. In poche parole, i componenti non vengono ricreati se si naviga sulla stessa rotta con parametri diversi (l’id in questo caso) senza prima passare da un’altra (per ovvii motivi di ottimizzazione), per cui ngOnInit
non verrebbe richiamato. E’ qui che l’observable entra in gioco aggiornando il valore dell’id nell’istanza della classe!
Dal momento che non sempre la logica applicativa di navigazione prevede il caso di ricaricare un componente (con parametri diversi) senza passare da un altro, fortunatamente abbiamo a disposizione una modalità più semplice di recupero dei parametri tramite l’oggetto Snapshot
:
export class DoneComponent implements OnInit { private id: number; constructor(private activatedRoute: ActivatedRoute) { } ngOnInit() { this.id = this.activatedRoute.snapshot.params.id; } }
Se si ha necessità di forzare la navigazione da codice invece, possiamo iniettare direttamente il servizio Router
e chiamare il metodo navigate.
Feature Routes
Dal momento che l’architettura di una applicazione dovrebbe essere costituita da un modulo per pagina (o sotto-pagine), per rendere il modulo consistente è bene associarvi le rotte e (ovviamente) i componenti che lo riguardano: in questo caso si parla di Feature Routes, cioè di rotte “figlie”, legate ai Feature Modules.
Prendiamo quindi per esempio la pagina dei “todo”: può essere trasformata in un modulo TodosModule
che raccoglie i componenti usati nella pagina stessa, compresa la rotta per accedervi.
const todosRoutes:Routes = [ { path: 'todos', component: TodosComponent } ]; @NgModule({ imports: [ CommonModule, FormsModule, RouterModule.forChild(todosRoutes), ], declarations: [ TodoFormComponent, TodosComponent, TodoListComponent, ] }) export class TodosModule { }
Stessa cosa possiamo fare per la pagina “done list”, creando il modulo DoneListModule
: alla fine, il modulo che avvia l’applicazione si semplifica notevolmente, e fa da aggregatore di tutti i feature modules che costituiscono l’applicazione.
const routes:Routes = [ { path: '', redirectTo: '/todos', pathMatch: 'full' }, { path: '**', component: NotFoundComponent } ]; @NgModule({ declarations: [ AppComponent, NotFoundComponent ], imports: [ RouterModule.forRoot(routes), TodosModule, DoneListModule, BrowserModule, HttpModule ], providers: [TodoListService], bootstrap: [AppComponent] }) export class AppModule { }
Nel modulo applicativo rimangono solo le rotte base per la gestione del default e degli errori, mentre sono spariti dalle declarations i componenti che adesso sono forniti direttamente dai due nuovi moduli.
Rimanendo aderenti all’architettura modulo-pagina, diventa molto facile abilitarne il lazy loading dei moduli. Normalmente infatti, all’avvio dell’applicazione, vengono scaricati tutti i moduli applicativi, ma spesso può essere un lavoro e un’attesa superflua. Se riteniamo che certe rotte possano venir visitate poche volte, vale la pena caricarle solo a richiesta, cioè quando vengono visitate. Guarda cosa bisogna aggiungere (in verde) e togliere (in rosso) per far diventare il modulo DoneListModule lazy!
Quando invece un modulo si fa più complesso, è possibile creare una vera e propria “sotto-navigazione” del Feature Module, definendo delle Child Routes. Questa può essere fatta aggiungendo un nuovo router-outlet anonimo o con nome, detto Named Outlet. Ogni named outlet ha il suo set di regole di navigazione e componenti, proprio come per gli outlet primary (anonimi).
Approfondire ulteriormente le rotte non è lo scopo di questo post. E’ possibile ritrovare questo argomento in modo esaustivo, insieme a tutti i dettagli sulla gestione delle rotte, direttamente dalla documentazione ufficiale.
Direttive
Rispetto ad un anno fa, finalmente la documentazione sulle direttive è degna di questo nome! La distinzione tra Componenti (ovvero direttive con template), Structural directives (che manipolano il DOM) e Attribute directives (attributi che cambiano il comportamento ad un elemento) è rimasta la stessa. Adesso però la documentazione fornisce degli esempi sui quali si può ragionare.
In un momento in cui vi sentite particolarmente kitsch, decidere di creare una direttiva (colorful.directive.ts) che fa cambiare colore al titolo dell’applicazione Todo List con i colori dell’arcobaleno, quando si passa sopra col mouse:
@Directive({ selector: '[colorful]' }) export class ColorfulDirective { private rainbowColors = [ ... ] constructor(private el: ElementRef) { } @HostListener('mouseenter') onMouseEnter() { const newColorIndex = Math.round(Math.random() * (this.rainbowColors.length)); this.el.nativeElement.style.color = this.rainbowColors[newColorIndex]; } }
Vediamo tutti gli elementi evidenziati:
- dal momento che si tratta di una “direttiva attributo”, il selettore CSS deve contenere le parentesi quadre (come da standard CSS) e verrà usata come un normale attributo HTML:
{{title}}
-
ElementRef
è un riferimento all’elemento del DOM al quale è stata applicata la direttiva -
@HostListener
permette di definire dei listener sui normali eventi del DOM. Perché usare questo decoratore invece di lavorare direttamente sull’elemento del DOM nativo? La motivazione principale è che altrimenti ci dovremmo ricordare di rimuovere il listener manualmente dal DOM per non creare memory leak. In questo modo lo fa Angular per noi. - è possibile accedere all’elemento nativo JavaScript richiamando
nativeElement
sul wrapper.
Non dimentichiamo di dichiarare in un modulo l’esistenza di questa direttiva. Conviene raggruppare tutte le direttive in un modulo a sé stante (per esempio common-directives.module.ts), in modo da essere usato in tutte i feature modules della nostra applicazione: se scegliamo questa strada, è necessario anche esportare la direttiva per renderla pubblica.
@NgModule({ imports: [ CommonModule ], exports: [ ColorfulDirective ], declarations: [ ColorfulDirective ] }) export class CommonDirectivesModule { }
E’ possibile inoltre passare dei valori costanti alla direttiva:
{{title}}
oppure, in caso di variabili, è necessario usare la notazione del property binding (cioè con parentesi quadre), come si fa normalmente per i componenti:
{{title}}
Per “ricevere” il valore, la classe della direttiva deve contenere un attributo chiamato colorful
, come il nome della direttiva, oppure un alias, come per esempio:
@Input('colorful') fixedColor;
siamo quindi liberi di usare il nome fixedColor
come variabile interna alla classe.
Questi concetti valgono anche per le direttive strutturali, ovvero quelle che manipolano il DOM, ma con qualche differenza: si riconoscono intanto perché la dichiarazione è preceduta da un asterisco (*), (niente parentesi quadre!) come ngIf
:
La prima vera differenza è che ad un elemento HTML si può associare una ed una sola direttiva strutturale (perché appunti ridisegna il DOM), a differenza delle direttive attributo che ne cambiano solo il comportamento, quindi ne possono essere applicate più d’una. La ragione è la semplicità: applicare più direttive che modificano il DOM sullo stesso elemento può portare a situazioni inattese. Se si ha però questa necessità (e sicuramente si avrà!), la documentazione ufficiale consiglia di modificare il template HTML aggiungendo una gerarchia di tag “contenitori” ng-container
per ogni direttiva strutturale da applicare: questo tag non aggiunge veramente nodi al DOM perché non viene renderizzato da Angular, serve solo come nodo raggruppatore. Questo tag è molto utile quando aggiungere elementi al DOM per ovviare questo limite può rompere il CSS.
Ma perché l’asterisco? Questo semplice carattere in realtà è “zucchero sintattico” che nasconde due trasformazioni eseguite internamente dal compilatore.
In pratica la direttiva strutturale internamente diventa una direttiva attributo sull’elemento TemplateRef
e ViewContainerRef
. Pensiamo quindi alla versione “renderizzata” della direttiva:
ViewContainerRef
punterà all’elemento TemplateRef
all’elemento , che è il nostro template originale. Questi due servizi sono disponibili per l’injection quando si crea una direttiva strutturale, come per esempio with-delay.directive.ts, che aggiunge un ritardo nella visualizzazione di ogni elemento della Todo List, in modo da fare l’effetto “tendina”:
import {Directive, Input, TemplateRef, ViewContainerRef} from '@angular/core'; @Directive({ selector: '[withDelay]' }) export class WithDelayDirective { constructor( private templateRef: TemplateRef, private viewContainerRef: ViewContainerRef ) { } @Input() set withDelay(time: number) { setTimeout(()=>{ this.viewContainerRef.createEmbeddedView(this.templateRef); }, time); } }
Il template (this.templateRef
) viene applicato alla view (this.viewContainerRef
) tramite il metodo createEmbeddedView
, dopo un certo tempo. Da notare che il decoratore @Input
è usato insieme al setter della property withDelay
(che coincide con il nome della direttiva perché non abbiamo definito alias). Solitamente le direttive di questo tipo devono reagire al cambiamento dell’input, per cui si usa direttamente il setter.
Come si usa questa direttiva? Dal momento che va applicata ad ogni riga della tabella dei Todo, ma non può andare insieme ad ngFor
per i limiti sopra citati, è l’occasione giusta di usare ng-container:
... il codice completo è todo-list.component.html. Non scordiamoci l’asterisco, altrimenti riceveremo un misterioso errore del tipo “No provider for TemplateRef!“!
Conclusioni
Abbiamo quindi visto con qualche esempio e tanti confronti quali sono le differenze nate nell’ultimo anno e che ha portato Angular da balzare dalla versione 2 alla 4! Nel frattempo si anche stabilizzato un ottimo tool di supporto allo sviluppo, ovvero Angular CLI, senza il quale diventa inutilmente complesso gestire lo startup di un progetto nonché la sua manutenzione. Prossimamente quindi vedremo nel dettaglio come usarlo e come aggiungere quelle piccole personalizzazioni che lo hanno portato a diventare uno strumento insostituibile nello sviluppo delle applicazioni con Angular.
Pingback: CodingJam » WebVR con A-Frame()
Pingback: CodingJam » Introduzione ad Angular CLI()