Introduzione ad Angular 2 – Parte 1

angular2

Introduzione ad Angular 2

E’ tanto che se ne parla e finalmente Angular 2 è arrivato alla versione beta! Possiamo considerare le API ormai stabili dal punto di vista sintattico (visto che dalla alpha qualcosa è cambiato), per cui è il momento di cominciare a studiarlo senza aspettarsi straordinari cambiamenti da qui alla release ufficiale. Quei rumors che dicevano che Angular 2 era del tutto diverso dalla versione 1 erano veri? Ebbene si, Angular 2 fa un grande passo a vanti, si trasforma e diventa… React!!! Apparte gli scherzi, a prima vista sembra che Angular 2 sia più parente del framework di Facebook che del suo predecessore, almeno per quanto riguarda i concetti base. Scavando un po’ però si riconosce il nostro vecchio amico AngularJS…

Di fatto, entrambe i framework strizzano l’occhio ai Web Components, che, a quanto pare, saranno il futuro dello sviluppo web.

C’era una volta AngularJS

Certe volte la vita è strana… abbiamo lavorato per anni con jQuery manipolando il DOM come giocolieri, poi ci è stato detto che jQuery era il male perché lento e che Angular era molto più veloce, poi è arrivato React che additava Angular di essere lento, ma essendo una libreria per gestire solamente la view si finiva nuovamente su jQuery per mille altri motivi… ma non avevamo detto che jQuery era lento?! Morale della favola: meglio non fidarsi delle mode, ma della propria voglia di sperimentare con mano quello che si vocifera.

Perché Angular 2? Perché TypeScript?

La potenza di AngularJS, ma anche il suo tallone d’Achille è stato il two-way databinding che rende il framework affascinante: vista e modello sempre sincronizzati “automagically”. Questa magia la si paga purtroppo in termini di prestazioni quando il modello diventa estremamente grande e, lavorando con single page webapp, i memory leak sono sempre in agguato. Per superare questo problema quindi non resta che… riscrivere tutto da capo! E nel ricominciare, ad un certo punto dello sviluppo è stato scelto di migrare a TypeScript. Perché proprio TypeScript? Secondo Victor Savkin (che lavora nel team di sviluppo di Angular) ci sono diversi vantaggi: in primo luogo hai un linguaggio type safe (anche se non è l’unico che puoi compilare in JavaScript), ma soprattutto esistono degli strumenti potenti che supportano il linguaggio in fase di refactoring, nell’autocompletamento o nella navigazione del codice (come per esempio, aggiungo io, WebStorm o VS Code), per non parlare della semantica del codice visto che il linguaggio supporta interfacce e classi.

Effettivamente per quel che ho visto fino adesso la curva di apprendimento è velocissima: sembra di scrivere JavaScript con un tocco di Java e Scala qua e là (così noi di cnj siamo tutti contenti!). In realtà al momento somiglia tantissimo a ES6, anche se TypeScript rimane una sorta di “estensione” per via dei tipi e dei decoratori. L’unica cosa che al momento mi lascia perplesso (se ho capito bene) è che per usare le librerie in JavaScript bisogna istruire il compilatore TypeScript su come maneggiarle attraverso dei “type definition files” (*.d.ts): molti si trovano su GitHub, spero solo che questa peculiarità un giorno non ci “ritorni tra i denti” come si suol dire…

Nonostante Angular 2 sia stato scritto in TypeScript, possiamo decidere di usare liberamente JavaScript o Dart: visto il codice che è necessario scrivere in JavaScript, TypeScript è di gran lunga più chiaro! Non mi sembra infatti un caso che sia sparito il suffisso JS dal nome del framework…

Ma torniamo al codice! In questa introduzione vedremo come è fatta una semplice applicazione “Todo List” che tratta i concetti base di Angular 2, rappresentati, come da documentazione ufficiale, dal seguente schema:

Introduzione ad Angular 2: Architecture

Il codice sorgente dell’applicazione si trova su GitHub

Componenti

Dimenticate i controller, dimenticate lo scope (per quelli che vengono da AngularJS): il nuovo concetto fondamentale è il Componente!! Un componente è un insieme di funzionalità esportate da un “modulo” (implementato per esempio come una classe TypeScript), con un template HTML su cui si applicano queste funzionalità e, all’occorrenza, un foglio di stile associato. E’ quindi paragonabile ad una direttiva AngularJS con un template e un controller.

Prendiamo per esempio:

import {Component} from "angular2/core";
import {TodoFormComponent} from "./todo-form.component";
import {TodoListComponent} from "./todo-list.component";
import {Todo} from "./models/todo";

@Component({
    selector: 'todo-list-app',
    template: `
        <div class="container">
            <h1 class="text-center">{{title}}</h1>
            <todo-form (onNewElement)="addNewElement($event)"></todo-form>
            <hr/>
            <todo-list [todos]="todoList"></todo-list>
    	</div>
    `,
    directives: [
        TodoFormComponent,
        TodoListComponent
    ]
})
export class AppComponent { 
   public title = 'TodoApp';
   public todoList: Todo[] = [];
   addNewElement(element: string) {
      let todo = {id: this.todoList.length + 1, text: element, done: false};
      this.todoList.push(todo);
   }
}

A prima vista il codice è piuttosto chiaro, TypeScript non ha bisogno di grandi spiegazioni se si conosce JavaScript: il componente todo-list-app definisce una porzione di HTML (template) sulla quale gestisce un modello di dati (quello che un tempo si definiva sullo scope) e regola su di essi un comportamento (tramite i metodi della classe, che fa le veci del controller).

Template

Rappresenta la view del componente, ovvero come deve essere renderizzato. E’ composto non solo da elementi HTML, ma anche da tutte le direttive Angular o dai nostri componenti customizzati. In effetti, con Angular 2 tutta l’applicazione è un componente che include e gestisce altri componenti in cascata tramite i template HTML.

Prendiamo per esempio il template dell’applicazione “Todo List”:


<div class="container">
   <h1 class="text-center">{{title}}</h1>
   <todo-form (onNewElement)="addNewElement($event)"></todo-form>
   <hr/>
   <todo-list [todos]="todoList"></todo-list>
</div>

che renderizza questa interfaccia:

Introduzione ad Angular 2: Todo-App

I due tag “sconosciuti” todo-form e todo-list sono a sua volta due componenti che definiscono un proprio template HTML e gestiscono rispettivamente il campo di input e la lista dei todo (che vedremo più avanti). Il modello dei dati è invece orchestrato dalla classe associata a questo template (sostanzialmente il controller) che riceve da todo-form il nuovo elemento da aggiungere, aggiorna la lista e la passa a todo-list che sa come renderizzarla. Il flusso di dati quindi “rimbalza” sempre tra la classe e la vista (e viceversa) all’interno di un componente come schematizza la documentazione ufficiale:

Introduzione ad Angular 2: Architecture Detail

Data Binding

Il buon vecchio “two-way databinding” in Angular 2 in realtà è costituito da due tipologie differenti di “one-way databinding” in direzioni opposte, risultando così predicibile e molto performante:

  • da codice a template: può avvenire in due modalità, indicate da sintassi diverse:

    <h1 class="text-center">{{title}}</h1>
    <todo-list [todos]="todoList"></todo-list>
    

    title e todoList sono attributi della classe AppComponent e rappresentano il modello: il primo viene “scritto” nel DOM attraverso doppie parentesi graffe ed è detto “Interpolazione“; il secondo invece viene chiamato “Property Binding” (attraverso parentesi quadre) e consiste nel passare all’attributo todos (chiamato target property o input property) del componente figlio todo-list il valore di todoList del componente padre.

  • da template a codice:

    <todo-form (onNewElement)="addNewElement($event)"></todo-form>
    

    grazie all’Event Binding todo-form lancia l’evento onNewElement (definito source property o output property, indicato tra parentesi tonde) così che venga eseguita la funzione addNewElement definita nel componente padre AppComponent: l’oggetto $event conterrà il nuovo elemento da aggiungere alla todo list.

Il data binding interessa quindi anche la comunicazione tra componenti, in particolare il flusso dati si propaga secondo questo schema ad albero (tratto da SitePoint):

Introduzione ad Angular 2: Components inputs and outputs. One-way data flow down hierarchy

Un componente quindi può ricevere dati dal proprio padre: il normale flusso di dati padre-figlio è quello di tipo Property Binding ed è definito come un “downwards data flow“. Nel mondo reale però un figlio può aver la necessità di comunicare dei dati al padre, come nel caso della nostra “Todo List”, todo-form deve poter comunicare al padre il nuovo elemento da aggiungere. In questo caso Angular 2 definisce un altro meccanismo definito “upwards data flow“, ovvero dal figlio al padre, attraverso gli Eventi (chiamato appunto Event Binding). Lo scambio di dati quindi passa sempre da un template HTML, visto che sono coinvolte le due tipologie di binding.

Introduzione ad Angular 2: parent-child binding

Questo è il nodo fondamentale per capire il nuovo meccanismo di databindig di Angular 2. La documentazione ufficiale fornisce uno schema interessante che riassume una volta per tutte la tipologia di databinding e la relativa sintassi:

Introduzione ad Angular 2: databinding

In questo schema viene presentato anche una quarta sintassi che è l’unione del property e dell’event binding e che realizza in una sola direttiva il two-way databinding. Prendiamo per esempio il template di todo-form:


<form (ngSubmit)="addNewElement()">
  <div class="row">
    <div class="col-xs-10">
      <input ref="todoInput" type="text" class="form-control" id="textInput" placeholder="Add Todo..." autocomplete="off" [(ngModel)]="element" />
    </div>
    <div class="col-xs-2">
      <input type="submit" class="btn btn-default" value="Save"/>
    </div>
  </div>
</form>

al campo di input viene associato il modello element (attributo della classe associata): la variabile deve essere aggiornata al variare del valore dell’input e viceversa. Questo è possibile tramite la sintassi “banana in a box” [()], che non è altro che una scorciatoia per:

<input [ngModel]="element" (ngModelChange)="element = $event" />

In generale, per ogni [(varible)], Angular si aspetta una input property [variable] e una output property (variableChange).

Input e Output

Grazie al property binding abbiamo visto come è possibile passare dei valori tra un componente padre e un componente figlio: è proprio così che il vettore todos (del template di todo-list) viene popolato:

<todo-list [todos]="todoList"></todo-list>

Il componente figlio sa che i valori di todos (diventata una target property) provengono da un input esterno e sono di tipo Todo:

@Component({
    selector: 'todo-list',
    templateUrl: 'app/todo-list.component.html'
})
export class TodoListComponent {
   @Input() todos: Todo[];
}

Tramite la chiamata al decoratore @Input(), Angular sa di dover assegnare il valore popolato dal padre attraverso il template (todoList -> todos). Da non dimenticare che i decoratori sono funzioni, quindi le parentesi tonde in fondo al nome vanno specificate affinché il decoratore venga invocato!
Una sintassi alternativa a @Input() è quella si specificare l’attributo input nell’oggetto di configurazione passato a @Component:

@Component({
    selector: 'todo-list',
    templateUrl: 'app/todo-list.component.html'
    input: ['todos']
})

Come è arrivato però il nuovo task all’interno del vettore todos? Come abbiamo visto in precedenza, nella nostra app il componente todo-form è quello che genera i nuovi task da aggiungere alla lista todos. I passi da fare sono quindi:

  • todo-form riceve l’input utente e passa il valore al componente padre tramite event binding registrato sulla variabile onNewElement:

    @Component({
        selector: 'todo-form',
        template: `
            <form (ngSubmit)="addTodo()">
              <div class="row">
                <div class="col-xs-10">
                    <input ref="todoInput" type="text" class="form-control" id="textInput" placeholder="Add Todo..." [(ngModel)]="element" autocomplete="off"/>
                </div>
                <div class="col-xs-2">
                    <input type="submit" class="btn btn-default" value="Save"/>
                </div>
              </div>
            </form>
        `,
    })
    export class TodoFormComponent {
    
        @Output() onNewElement: EventEmitter = new EventEmitter<string>();
        private element = '';
        addTodo() {
            this.onNewElement.emit(this.element);
            this.element = '';
        }
    }
    

    Il metodo addTodo, chiamato al submit, emette un evento in uscita dal componente (decorato con @Output()) a cui passa il nuovo elemento da aggiungere alla lista (popolato grazie a ngModel). onNewElement diventa quindi una source property (o output property) alla quale il componente padre si può agganciare. Come nel caso precedente, è possibile sostituire @Output() con:

    @Component({
        selector: 'todo-form',
        outputs: ['onNewElement']
    ...
    })
    export class TodoFormComponent {
        onNewElement: EventEmitter = new EventEmitter<string>();
    ...
    }
    

  • il componente padre todo-list-app sta in ascolto sull’evento onNewElement di todo-form e reagisce invocando la funzione addNewElement

    <todo-form (onNewElement)="addNewElement($event)"></todo-form>
    

    sarà quindi il controller del componente padre a ricevere il nuovo elemento e ad aggiungerlo alla lista dei todo todoList:

    export class AppComponent {
    
        public title = 'TodoApp';
        public todoList: Todo[] = [];
        addNewElement(element: string) {
            let todo = {id: this.todoList.length + 1, text: element, done: false};
            this.todoList.push(todo);
        }
    }
    

    addNewElement, tramite $event, riceve il valore emesso dal componente figlio e costruisce un oggetto di tipo Todo da aggiungere alla lista: questa modifica fa scattare l’aggiornamento dei componenti figli così da fare comparire il nuovo elemento nella lista sottostante.

Metadati

La classe AppComponent sa di essere parte di un componente Angular 2 perché gli vengono associati dei metadati che definiscono il template HTML, il CSS (eventuale) e le varie dipendenze che gli necessitano per la renderizzazione.

L’aggiunta di metadati in TypeScript si traduce nel “decorare” la classe che rappresenta la logica del componente con il decoratore @Component, che non è altro che una funzione che prende come argomento un oggetto di configurazione e istruisce la classe componente su come interagire sul template:

@Component({
    selector: 'todo-list-app',
    templateUrl: 'app/app.component.html',
    directives: [
        TodoFormComponent,
        TodoListComponent
    ]
})

Questo oggetto di configurazione ha due attributi obbligatori:

  • selector: definisce il nome del componente che verrà usato come se fosse un nuovo tag HTML
  • template/templateUrl: definisce il template HTML che costituisce il componente. Può essere scritto direttamente nel codice come “stringa template” (per evitare la concatenazione, supportate da ES6 e TypeScript), o definisce l’url ad una risorsa HTML esterna (per migliorare la leggibilità).

Anche l’attributo directives è molto importante, perché definisce delle dipendenze. Come abbiamo visto il template usa due nuovi componenti todo-form e todo-list: coma fa il componente principale a capire come renderizzarli? L’attributo directives specifica appunto i componenti da cui si dipende (TodoFormComponent e TodoListComponent) i quali, tramite i loro selector, sapranno come applicare il proprio template sul tag:

@Component({
    selector: 'todo-form',
    templateUrl: 'app/todo-form.component.html'
})
export class TodoFormComponent {
...
}

@Component({
    selector: 'todo-list',
    templateUrl: 'app/todo-list.component.html'
})
export class TodoListComponent {
...
}

Direttive

Come mai ci si riferisce a TodoFormComponent e TodoListComponent come direttive? Un Componente infatti non è altro che una direttiva con un template: in generale quindi, si parla di direttive quando si ha a che fare con funzioni che modificano il DOM (proprio come nella prima versione di Angular). In TypeScript, questo concetto si implementa come una classa annotata con il decoratore @Directive.

In particolare, si parla di due tipi di direttive (oltre ai componenti ovviamente):

  • Strutturali: sono tutte quelle direttive che manipolano il DOM, creando o rimuovendo elementi. Un esempio? Il nostro componente todo-list, per creare la tabella dei todo da mostrare, sicuramente avrà bisogno di ripetere la porzione di DOM che crea la singola riga, la quale poi mostrerà il bottone “Done”/”Redo” a seconda se il task sia stato completato o meno. Queste due necessità suggeriscono l’uso delle direttive *ngFor e *ngIf

    <div class="row">
      <table class="table">
        <tr *ngFor="#todo of todos">
          <td [class.done-true]="todo.done">{{todo.text}}</td>
          <td>
            <button class="pull-right btn btn-default" (click)="markDone(todo)" *ngIf="!todo.done">
              Done
            </button>
            <button class="pull-right btn btn-default" (click)="markUndone(todo)" *ngIf="todo.done">
              Redo
            </button>
          </td>
        </tr>
     </table>
    </div>
    

    Occhio ad un paio di cose: l’asterisco * a fianco della direttiva è parte integrante della sintassi e serve per evidenziare che abbiamo a che fare con direttive che modificano il DOM (e che nasconde una sintassi più complessa). Il cancelletto # davanti all’i-esimo elemento del vettore todos, ovvero #todo, permette di creare una variabile di template locale, da usare all’interno del template che la direttiva *ngFor sta “ripetendo”.
    UPDATE: dalla versione RC1 la sintassi #todo è stata deprecata in favore di una dichiarazione esplicita come let todo: l’uso della direttiva *ngFor diventa quindi:

    <tr *ngFor="let todo of todos">
    

  • Attributi: sono tutte quelle direttive che alterano la visualizzazione o il comportamento di un elemento esistente, come ngModel o ngClass

Servizi e Dependency Injection

Fino adesso ci siamo concentrati sul rapporto tra controller e vista, ed è ciò che un componente deve fare. Ogni applicativo che si rispetti però ha una parte di logica incapsulata in altri “artefatti”, ovvero i servizi, come già ci aveva insegnato la versione precedente di Angular. I servizi sono quindi quelli che gestiscono l’accesso al backend, scrivono log o validano gli input, non sono i componenti a doverlo fare!

Ammettiamo di voler aggiungere alla nostra applicazione un servizio che salva i todo nello storage del browser. In TypeScript, questa classe avrà una firma del tipo:

@Injectable()
export class TodoListService {
    store(todo: Todo) {
       ...
    }
    storeAll(todos: Todo[]) {
       ...
    }
    getAll() {
       ...
    }
    update(todo: Todo) {
       ...
    }
}

Si tratta quindi di una normale classe TypeScript, ma grazie al decoratore @Injectable() (ricordatevi le parentesi tonde!!), Angular sa che si tratta di una classe candidata ad essere iniettata in quei componenti o in quei servizi che la richiedono come dipendenza. Se quindi volessimo iniettare TodoListService in AppComponent:

@Component({
    selector: 'todo-list-app',
    providers: [
        TodoListService
    ]
})
export class AppComponent {

    constructor(private _todoListService: TodoListService) {}
    addNewElement(element: string) {
        let todo = {id: this.todoList.length + 1, text: element, done: false};
        this._todoListService.store(todo);
    }
}

Tramite la proprietà providers dell’oggetto passato al decoratore @Component() si specificano i tipi che vogliamo iniettare al componente, il quale, tramite il costruttore, associa il tipo ad una variabile privata, che può essere usata all’interno della classe.

C’è una grande differenza però rispetto alla versione precedente di Angular: mentre prima i servizi erano sempre singleton, adesso lo scope dipende da dove sono dichiarati come provider nella gerarchia dei componenti. Spieghiamo meglio questo passaggio. Il sistema di Dependency Injection di Angular 2 è gerarchico, nel senso che segue la gerarchia dei componenti. E’ come se ogni componente avesse a disposizione un “injector” al quale si chiede se esiste un’istanza del servizio o un provider (dichiarato come metadato di @Component) per istanziarlo: se non esiste, l’injector chiede la dipendenza all’injector componente padre e così via.

Posso quindi dichiarare un servizio a livello di bootstrap dell’applicazione oppure a livello di componente: nel primo caso, tutti i componenti avranno la stessa istanza dei servizi (così come in AngularJS), nel secondo caso invece l’istanza sarà condivisa solo ed esclusivamente dal componente in cui è stato dichiarato il provider e dai suoi componenti figli.

Nella nostra app, i componenti sono così strutturati:

<todo-list-app>
     <todo-form>
     <todo-list>

Dichiarare quindi il provider a livello di todo-list-app significa condividere l’istanza con todo-form e todo-list, perché chi crea il servizio è il provider del componente padre. Nella nostra app, solo todo-list ha bisogno del servizio, per cui dichiarerà solo il costruttore, senza il provider:

@Component({
    selector: 'todo-list',
    ...
})
export class TodoListComponent {
    constructor(private _todoListService: TodoListService) {}
...
}

Conclusioni

In questo primo post introduttivo abbiamo affrontato tutti i concetti fondamentali che ci permettono di capire come è organizzata la nuova struttura di Angular 2 basata sui componenti, ma non siamo ancora in grado di eseguire una applicazione. Manca da affrontare il bootstrap e altri argomenti salienti come la comunicazione con il server e il routing, che tratteremo prossimamente nella seconda parte. Se siete curiosi date intanto un’occhiata al progetto su GitHub.

Andrea Como

Sono un software engineer focalizzato nella progettazione e sviluppo di applicazioni web in Java. Presso OmniaGroup ricopro il ruolo di Tech Leader sulle tecnologie legate alla piattaforma Java EE 5 (come WebSphere 7.0, EJB3, JPA 1 (EclipseLink), JSF 1.2 (Mojarra) e RichFaces 3) e Java EE 6 con JBoss AS 7, in particolare di CDI, JAX-RS, nonché di EJB 3.1, JPA2, JSF2 e RichFaces 4. Al momento mi occupo di ECM, in particolar modo sulla customizzazione di Alfresco 4 e sulla sua installazione con tecnologie da devops come Vagrant e Chef. In passato ho lavorato con la piattaforma alternativa alla enterprise per lo sviluppo web: Java SE 6, Tomcat 6, Hibernate 3 e Spring 2.5. Nei ritagli di tempo sviluppo siti web in PHP e ASP. Per maggiori informazioni consulta il mio curriculum pubblico. Follow me on Twitter - LinkedIn profile - Google+