Il framework Angular mette a disposizione alcune tecniche per la gestione dei dati nei template, si tratta di tecniche per la gestione della visualizzazione dei dati ma anche tecniche per la gestione dei dati inseriti dall’utente senza l’uso dei form. In questo articolo saranno comprese le tecniche molto usate che non prevedono l’uso dei form e delle variabili, tecniche già esposte in precedenti articoli.
La tecnica più semplice è l’uso del carattere |
detto anche Pipe (in inglese traduzione della parola tubo), questa tecnica viene descritta nel sito ufficiale e prevede la definizione di semplici espressioni che accettano un valore in input e ritornano il valore trasformato, sono molto usate perché possono essere usate nei template per trasformare e formattare i dati senza dover modificare il valore originale, un esempio semplice di utilizzo è la formattazione delle date che spesso nei servizi/webService
sono nel formato standard json/javascript
ma nei template è necessario visualizzarlo nel formato dd/mm/yyyy
o simili.
{{valueDate | date: 'dd/MM/yyyy'}}
Le più comuni espressioni pipe previste dal framework sono: DatePipe, UpperCasePipe, LowerCasePipe, CurrencyPipe, DecimalPipe, PercentPipe.
E’ possibile scrivere espressioni personalizzate, per esempio per definire una espressione per eseguire l’esponenziale bisogna definire una classe che implementi il metodo di trasformazione:
import { Pipe, PipeTransform } from '@angular/core'; @Pipe({name: 'exponentialStrength'}) export class ExponentialStrengthPipe implements PipeTransform { transform(value: number, exponent = 1): number { return Math.pow(value, exponent); } }
E poi è possibile usare l’espressione nei template
{{2 | exponentialStrength: 10}}
La documentazione ufficiale mette a disposizione diversi esempi semplici e spiegazioni complete riguardo a questo argomento. L’esempio completo dell’esponenziale infatti è completato nella documentazione ufficiale con l’uso delle variabili è l’uso del ngModel:
@Component({ selector: 'app-power-boost-calculator', template: ` <h2>Power Boost Calculator</h2> <label for="power-input">Normal power: </label> <input id="power-input" type="text" [(ngModel)]="power"> <label for="boost-input">Boost factor: </label> <input id="boost-input" type="text" [(ngModel)]="factor"> <p> Super Hero Power: {{power | exponentialStrength: factor}} </p> `, styles: ['input {margin: .5rem 0;}'] }) export class PowerBoostCalculatorComponent { power = 5; factor = 1; }
Un’altra applicazione delle pipe è realizzare filtri sugli elenchi nei cicli ngFor, per esempio per filtrare i primi 50 elementi di una lista si può usare la espressione standard slice:
<li *ngFor="let elemento of listaCompleta | slice:0:50 ; let i = index;"> ... </li>
Ma è possibile usare anche slice personalizzare definire da classi specifiche, per esempio per eseguire un filter su una lista si può definre una espressione personalizzata:
@Pipe({ name: 'flyingHeroes' }) export class FlyingHeroesPipe implements PipeTransform { transform(allHeroes: Hero[]) { return allHeroes.filter(hero => hero.canFly); } }
E nel template si può richiamare la espressione
<li *ngFor="let hero of (heroes | flyingHeroes)"> {{hero.name}} </li>
Questa tecnica è usata moltissimo dai programmatori anche per gestire gli oggetti json: in fase di sviluppo è necessario trasformare oggetti json in stringa per visualizzare l’intero oggetto in un template, nativamente Angular non visualizza gli oggetti complessi mentre l’uso del pipe json
permette di avere a video la forma completa di un oggetto, la documentazione ufficiale spiega che è usato per convertire un valore nel suo formato json rappresentabile in pagina. L’esempio semplice di questo componente:
@Component({ selector: 'json-pipe', template: `<div> <p>Without JSON pipe:</p><pre>{{object}}</pre> <p>With JSON pipe:</p><pre>{{object | json}}</pre> </div>` }) export class JsonPipeComponent { object: Object = {foo: 'bar', baz: 'qux', nested: {xyz: 3, numbers: [1, 2, 3, 4, 5]}}; }
Il framework mette a disposizione anche il pipe di tipo AsyncPipe che verrà introdotto nell’articolo specifico che palerà della gestione dei servizi e delle funzioni asincrone.
La gestione di form e inserimento dati è sempre un argomento molto delicato e complesso visto che ogni framework e ogni linguaggio gestire l’inserimento di dati in maniera differente. Su Angular esistono diversi modi più o meno ufficiali e più o meno efficienti, qui saranno esposti diversi modi di gestione degli input usabili a seconda delle diverse esigenze. Il metodo più semplice è gestire il valore con il metodo change, così da avere un metodo per la gestione del valore, l’esempio classico è:
lista: Person[]=[{id:'1',nome:'Alberto'},{id:'2',nome:'Andrea'},...] selezionato: Person=this.lista[0]; cambia($event: any): void { let input = $event.target; this.selezionato=this.lista.filter ( e => e.id===input.value)[0]; } ... <select (change)="cambia($event)"> <option *ngFor="let el of lista;" value="{{el.id}}">{{el.nome}} ({{el.id}})</option> </select> <span>Elemento selezionato: {{selezionato.nome}}</span>
questo metodo potrebbe sembrare arcaico e da evitare, in realtà è molto utile quando un input deve scatenare un evento senza l’uso di bottoni submit o simili, l’esempio più classico è un form per la selezione e il caricamento di un file il cui metodo di gestione può essere:
fileUploaded: File; uploadListener($event: any): void { //console.log("uploadListener"); this.fileUploaded = $event.target.files[0]; let readFile = new FileReader(); readFile.onload = (e) => { //load data & next step } readFile.readAsArrayBuffer(this.fileUploaded); }
Il metodo più semplice per creare forms composti è usare la tecnica del template-driven ben documentata nel sito ufficiale, prevede l’uso delle parole chiave NgModel e NgForm. Prima di tutto bisogna definire una classe che rappresenta il modello dei dati inseriti, per esempio:
class Person { constructor( public id: string, public nome: string, public age?: number ) { } }
Nella classe è possibile definire una proprietà modello con dei valori predefiniti e un metodo per la gestione del submit, nell’esempio viene solo gestita la visibilità del form:
ngOnInit() { this.newHero(); } formModel : Person ; newHero() { this.formModel = new Person('4','Daniele',42 ); } submitted = false; onSubmit() { this.submitted = true; }
Nel template bisogna usare ngForm per creare un riferimento e ngModel per ogni input per il recupero e la gestione dei valori, la validità dei form viene gestita grazie alle regole HTML: nell’esempio il nome è obbligatorio e il bottone si disabilità automaticamente quando il form diventa invalido a seconda delle regole indicate nel form:
<div [hidden]="submitted"> <form #personForm="ngForm" (ngSubmit)="onSubmit()"> <input type="text" class="form-control" id="name" required [(ngModel)]="formModel.nome" name="nome" /> <div>Nome inserito {{formModel.nome}}</div> <button type="button" (click)="newHero();">New Hero</button> <button type="submit" [disabled]="!personForm.form.valid">Submit</button> </form> </div>
Questa tecnica è molto usata quando le regole di validazione sono semplici, gli input sono numericamente poche e quando il comportamento di un form è semplice senza particolarità.
La tecnica più evoluta e usata nel mondo è la creazione dei form reattivi (traduzione dell’inglese reactive-forms), con questa tecnica è possibile creare una sincronia attiva tra oggetto nella classe e valore nel template, nella documentazione è definita come il metodo ufficiale per creare form in Angular. Per poterla attivare bisogna includere nel modulo specifico:
import { ReactiveFormsModule } from '@angular/forms'; ... @NgModule({ imports: [ // other imports ... ,ReactiveFormsModule ], });
Questa libreria/modulo standard di Angular prevedo l’uso della classe FormControl nel package standard angular/forms
che permette di collegare una proprietà nella classe ad un oggetto nel template proprio con la proprietà formControl. Nella classe per esempio è possibile definire una proprietà e un metodo per modificarne il valore
import { FormControl } from '@angular/forms'; ... nomeReattivo = new FormControl(''); updateNameReattivo() { this.nomeReattivo.setValue('Alberto'); }
Nel template è possibile creare il collegamento tra oggetto nella classe e tag di tipo input con una proprietà del framework Angular, inoltre sempre possibile creare recuperare il valore in maniera standard e creare eventi
Nome reattivo:<input id="name" type="text" [formControl]="nomeReattivo"> <div>Nome reattivo inserito: {{ nomeReattivo.value }}</div> <button type="button" (click)="updateNameReattivo()">Update nome reattivo</button>
Questa tecnica è molto veloce da implementare se il numero di campi è limitato, nel caso di form con diversi campi potrebbe essere noioso e sconveniente usare questa tecnica infatti esiste la possibilità di creare un “gruppo” di elementi di tipo FormControl: è necessario creare un oggetto di tipo FormGroup contenente la lista degli input definiti come nell’esempio precedente:
import { FormGroup, FormControl } from '@angular/forms'; import { Validators } from '@angular/forms'; ... personFormGroup = new FormGroup({ firstName: new FormControl('', [ Validators.required, Validators.minLength(3) ]), repeatName: new FormControl('', [this.comparisonValidator ]), mail: new FormControl('', Validators.pattern("[a-z0-9._%+-]+@[a-z0-9.-]+.[a-z]{2,3}$") ) }); setValueFirstName(first){ this.personFormGroup.controls['firstName'].setValue(first); } onSubmitpPersonGroup() { // TODO: Use EventEmitter with form value console.log(this.personFormGroup.value); }
Nel template non si deve inserire una proprietà per ogni input ma si deve agganciare l’intero form al gruppo definito nella classe.
<form [formGroup]="personFormGroup" (ngSubmit)="onSubmitpPersonGroup()"> <label for="first-name">First Name: </label> <input id="first-name" type="text" formControlName="firstName" required /> <label for="last-name">Repeat Name: </label> <input id="last-name" type="text" formControlName="repeatName" /> <label for="last-name">E-Mail: </label> <input id="last-name" type="text" formControlName="mail" /> <button type="submit" [disabled]="!personFormGroup.valid">Submit personFormGroup</button> </form>
In questo semplice esempio è possibile notare che sono state introdotte delle regole di validazione nella definizione del FormGroup, la tecnica di form-reattivi permette di definire per ogni campo una regola di validazione che viene validato in maniera reattiva. La documentazione ufficiale descrive tutte le regole previste dal pacakge standard di angular ma è possibile definire regole di validazione personalizzate, per esempio per validare che l’utente abbia inserito correttamente due volte lo stesso valore nei due campi:
comparisonValidator(control: AbstractControl) : ValidationErrors{ if (!control.parent) return; const name=control.parent.value.firstName; const isValid=control.value===name; return isValid ? null : { 'myCustomError': 'This value is invalid' }; }
Nel template è possibile gestire la visualizzazione di messaggi a sconda se un errore di validazione è presente o meno, usando l’esempio di validazione dei nomi inseriti due volte è possibile visualizzare un messaggio se i nomi inseriti non coincidono:
<span [hidden]="!personFormGroup.get('repeatName').errors?.myCustomError"> I nomi devono coincidere</span>
Un esempio funzionante è disponibile al solito repository:
https://github.com/alnao/AngularReactNodeExamples/tree/master/AngularDatasetsFilms
Esiste una ulteriore tecnica per gestire l’inserimento dei dati e il passaggio dei dati dal template alla classe corrispettiva: questa tecnica prevede la definizione di un riferimento in un oggetto nel DOM che può essere usata in altri punti del template, la documentazione ufficiale è molto chiara a riguardo e prevede l’uso del carattere #
per la definizione di questi riferimenti, un semplice esempio di template che usa questa tecnica è:
<input #phone placeholder="phone number" /> <button type="button" (click)="callPhone(phone.value)">Call</button>
Questa tecnica è considerata deprecata da molti anche se, essendo stata molto usata in passato, si trova molto spesso nei progetti ed è ancora usata dai programmatori per la sua semplicità d’uso.
Il framework Angular è studiato per essere modulato formato da tanti componenti che condividono informazioni e dati tra di loro, nel caso di dati dinamici sono previsti alcuni decorator studiati per semplificare la vita dei programmatori. Prima di tutto bisogna pensare di essere in strutture del tipo annidato: un componente contiene uno o più component. Se il padre volesse inviare un dato dinamico non può usare la tecnica del “Single-slot content projection” o simili in quanto funzionano solo con contenuti statici.
La direttiva di input permette di inviare informazioni dal padre al figlio, per esempio avendo due componenti e volendo passare un valore numerico da un padre ad un figlio:
@Component({ selector: 'app-padre', template: `<app-figlio [valoreF]="valoreP"></app-figlio> <p (click)="addNelPadre()">Aggiungi uno</p>`, styles: [ ] }) export class PadreComponent { valoreP : number =0; ngOnInit(): void { this.valoreP=42; } addNelPadre(){ this.valoreP++;} } //////////////////////////////////////////////////// Component({ selector: 'app-figlio', template: `<p>Il valore è {{valoreF}}</p>`, styles: [ ] }) export class FiglioComponent { @Input() valoreF : number | undefined; }
nell’esempio è stato inserito anche un metodo nel padre per poter modificare il valore (nel padre) e constatare che il valore viene propagato dinamicamente anche nel figlio.
La tecnica per inviare dati ed eventi da un componente ad un genitore prevede l’uso della direttiva @Output e l’uso degli EventEmitter nel caso ci sia la necessità di eseguire un evento nel componente superiore scatenato da un evento del figlio padre, per esempio un bottone nel figlio per eseguire un metodo nel componente padre con la possibilità di far transitare anche un valore (stringa):
@Component({ selector: 'app-padre', template: `<app-figlio [valoreF]="valoreP" (figlioEmitter)="eventoNelFiglio($event)"></app-figlio> <p (click)="addNelPadre()">Aggiungi uno</p>`, styles: [ ] }) export class PadreComponent { valoreP : number =0; ngOnInit(): void { this.valoreP=42; } addNelPadre(){ this.valoreP++;} eventoNelFiglio(value : string){this.valoreP+=Number(value);} } //////////////////////////////////////////////////// @Component({ selector: 'app-figlio', template: `<p>Il valore è {{valoreF}}</p> <label>Add <input #inputF></label> <button (click)="addValue(inputF.value)"> Add to parent's value</button>`, styles: [ ] }) export class FiglioComponent { @Input() valoreF : number | undefined; @Output() figlioEmitter = new EventEmitter<string>(); addValue(value: string){ this.figlioEmitter.emit(value); } }
Questa tecnica prevede anche l’uso di @Input e @Output assieme, nell’esempio appena introdotto il figlio ha due proprietà diverse (una per input e una per output), è possibile inoltre evitare di dover aggiungere le annotation nel codice ma è possibile dichiarare l’elenco delle proprietà coinvolte nella dichiarazione dei meta-dati del componente aggiungendo anche un alias in modo che proprietà del padre e del figlio abbiano nomi diverse:
@Component({ selector: 'app-figlio', template: `<p>Il valore è {{valore}}</p> <label>Add <input #inputF></label> <button (click)="addValue(inputF.value)"> Add to parent's value</button>`, styles: [ ], inputs: ['valore: valoreF'], // propertyName:aliasInParent outputs: ['evento: figlioEmitter'] }) export class FiglioComponent { valore : number | undefined; figlioEmitter = new EventEmitter<string>(); addValue(value: string){ this.evento.emit(value); } }
La documentazione ufficiale è sempre il punto di riferimento di questa tecnica che è da usare ogni volta che è necessario inviare o scambiare dati dinamici tra componenti in un progetto Angular.
In Angular, un template è un frammento di un’interfaccia grafica utente (UI), spesso in italiano viene confuso con il nome “modulo” ma conviene abituarsi ad usare il termine originale in inglese. I template sono scritti in HTML e all’interno è possibile utilizzare una sintassi speciale per sfruttare molte delle funzionalità di Angular che estende la sintassi HTML, questa tecnica permette di creare contenuti dinamici modificando le proprietà del documento (DOM). La sintassi HTML deve essere valida e il fratello non deve includere tag base come
<html>
e <body>
visto che sono gestiti dal framework.
- La prima tecnica, chiamata Interpolation, prevede l’uso dei limitatori
{{
e}}
per visualizzare variabili o richiamare metodi della classe corrispondente al template, per esempio se in un template è presente una proprietà è possibile visualizzare la variabile nel template. Per esempio<div >Benvenuto {{name}}</div>
- La seconda tecnica, chiamata Template statements, permette di gestire eventi nei template richiamando metodi nella classe corrispondenti, a qualsiasi oggetto DOM è possibile associare un evento con le parentesi tonde e indicare il nome del metodo richiamato. Per esempio
<button type="button" (click)="changeName()"
- La terza tecnica, chiamata Property binding, permette di gestire proprietà e oggetti DOM da valori o metodi della classe richiamando il valore con le parentesi quadre. Per esempio
<button type="button"[disabled]="isDisabled"
- La quarta tecnica, chiamata Event binding, permette di gestire eventi specifici come l’inserimento dei tasti tramite sottoeventi con metodi specifici. Per esempio
e(keydown.shift.t)="onKeydown($event)"
- Altre tecniche come le direttive input-output e gli eventEmitter saranno introdotti in specifici articoli.
Angular poi mette a disposizione alcune proprietà chiave per la gestione di alcuni casi specifici molto frequenti:
- ndClass: utlizzata per la gestione delle classi in maniera dinamica usando le operazioni ternarie o metodi della classe corrispondente. Per esempio
<p [ngClass]="isDisabled ? 'textRed' : 'textGreen' ">
- *ngFor: utile per gestire cicli in maniera dinamica usando come base un elenco presente nella classe del componente. Per esempio
<span *ngFor="let figlio of lista">{{figlio}} </span>
- *ngIf: utile per gestire la visibilita di oggetti DOM con una condizione, spesso viene usata una proprietà della classe del componente. Per esempio
<div *ngIf="!isDisabled">...</div>
Tutte queste tecniche sono molto usate nei progetti e devono essere ben conosciute in quanto permettono di separare le logiche presenti nelle classi e la rappresentazione grafica del template. Un semplice esempio completo con queste tecniche in un template in-line:
import { Component, OnInit } from '@angular/core'; @Component({ selector: 'app-nome-componente-due', template: `<div > Benvenuto {{name}} <button type="button" (click)="changeName()" [disabled]="isDisabled"> Cambia nome</button> <div *ngIf="!isDisabled">Clicca il bottone per scoprire</div> <input (keydown.shift.t)="onKeydown($event)" /> <div [ngSwitch]="name" [ngClass]="isDisabled ? 'textRed' : 'textGreen' "> <div *ngSwitchCase="'Alberto Nao'" >Fratello minore</div> <div *ngSwitchCase="'Andrea Nao'" >Fratello maggiore <br />figli: <span *ngFor="let figlio of lista">{{figlio}} </span> </div> </div> <div><ng-content></ng-content></div> </div>`, styleUrls: ['./nome-componente-due.component.css'] }) export class NomeComponenteDueComponent implements OnInit { constructor() { } name = 'Alberto Nao'; isDisabled=false; lista=['Alice','Elena','Achille','Ettore']; ngOnInit(): void { } changeName(){ this.name="Andrea Nao"; this.isDisabled=true; } onKeydown(event:any){// console.log(event) console.log( "E' stato premuto la lettera "+event.key+" " ); } }
La documentazione ufficiale è ricca di esempi e descrive molto bene tutte le tecniche utilizzabili nei template Angular.