Stefano Marchisio - sviluppatore freelance: angular | asp.net core mvc c#
Quest'articolo spiega come manipolare il DOM in Angular attraverso le “Directive attributo” e le “Directive strutturali” piuttosto che lavorare a basso livello usando “Renderer2” e “ViewContainer” (ElementRef, TemplateRef, ViewContainerRef)
Introduzione
In un ottica web un developer è abituato a manipolare il DOM in un certo modo. Per esempio, creando lato client del codice HTML per poi inserirlo nel DOM tramite delle API JQuery, piuttosto che fare una chiamata AJAX che ci restituisca del codice HTML. Esiste poi l’opzione di far ritornare dati in formato JSON che verranno poi renderizzati tramite una delle tante librerie di template. Con Angular la manipolazione del DOM non avviene in questo modo, questo può essere causa di disagio soprattutto all’inizio in cui uno inizia a lavorare con Angular. In quest’articolo vediamo il perché, e come possiamo fare a manipolare il DOM utilizzando le API fornite da Angular.
Iniziamo col dire che Angular è un framework a componenti e di solito troviamo 3 file per ogni componente: un file .ts , un file .css e un file .html che rappresenta il template del componente. All’interno del template oltre all’HTML classico troviamo anche degli attributi/direttive di binding che servono a legare il template HTML al file TypeScript corrispondente, in questo modo la UI verrà automaticamente aggiornata nel momento in cui si cambia il valore di una delle proprietà del file TypeScript.
Per questo motivo altre alla compilazione TypeScript esiste anche una seconda compilazione Angular. Infatti in Angular è presente anche un compilatore, e questa può avvenire in modalità JIT piuttosto che AOT. Lo scopo di tale compilazione è tradurre l’HTML e gli attributi/direttive di binding presenti nel template in qualche cosa comprensibile dal framework. Senza entrare troppo nel dettaglio per ogni componente viene anche creata una classe di “ChangeDetection” che rileva eventuali modifiche (confrontando i vecchi valori con i nuovi), al verificarsi ci certi eventi.
Da questo si evince che il DOM è generato da queste classi (che non sono direttamente utilizzate dallo sviluppatore). Inoltre il collegamento tra questa famiglia di classi ed il DOM è unidirezionale, infatti se manipolo il DOM alla vecchia maniera (per esempio eliminando un nodo), la classe di “ChangeDetection” corrispondente non se ne accorge e continua a fare il suo lavoro. Ciò alla lunga può causare dei problemi di vario genere (in particolar modo prestazionali).
Tutto questo per spiegare il perché con Angular si deve procedere in modo diverso nella manipolazione del DOM da come siamo abituati a fare di solito. Nello stesso tempo Angular fornisce diversi strumenti/API per lavorare con il DOM in modo sicuro.
Come facciamo
In Angular abbiamo 2 strade per fa ciò
1) Usare le “Attribute directive” e “Structural directive” buld-in all’interno del framework che rendono tale cosa estremamente semplice
2) Lavorare ad un livello più basso usando: “Renderer2” e “ViewContainer
Direttive “Attribute directive” e “Structural directive”
In Angular esistono 2 famiglie di direttive buld-in che servono per manipolare il DOM in modo semplice e sicuro, ognuna di queste 2 famiglie è poi specializza per lavorare con gli attributi di un elemento piuttosto che aggiungere o rimuovere elementi.
Le “Attribute directive” appartengono alla prima famiglia e servono per lavorare con gli attributi di un elemento. In Angular ne esistono 2: “ngClass” e “ngStyle”. Con la prima è possibile aggiungere o rimuovere classi css da un elemento, con la seconda e possibile creare o rimuovere proprietà css da un elemento. Ogni direttiva può poi essere messa in binding con una proprietà all’interno del file TypeScript, in modo che i valori vengano settati in modo dinamico (sotto un esempio di ngClass).
Le “Structural directive” appartengono alla seconda famiglia e servono per aggiungere o rimuovere elementi dal DOM. In Angular ne esistono 3: “ngIf”, “ngFor”, “ngSwitch”. Come si evince dal nome, con la prima è possibile creare degli elementi in modo condizionale, con la seconda è possibile renderizzare una lista di elementi, con la terza è possibile creare degli elementi in modo condizionale tramite un’istruzione di switch. Tutte le direttive strutturali possono essere messe in binding con proprietà contenute all’interno del file TypeScript (sotto un paio di esempi).
Come si può notare le direttive strutturali sono precedute con un “*” davanti al nome, infatti vengono scritte come: *ngIf, *ngFor, *ngSwitchCase (questa è una “syntax sugar”). Come detto all’inizio dell’articolo, oltre alla compilazione TypeScript esiste anche una seconda compilazione Angular che lavora principalmente sui template HTML.
Una direttiva “ngIf” come quella sottostante
Viene tradotta in questo modo dal compilatore
Questo per rendere più agevole la scrittura di codice, ma se scrivessimo un “ngIf” nel secondo formato il tutto funzionerebbe senza problemi. Per maggiori informazioni in merito alle direttive build-in si rimanda alla documentazione del framework.
“Renderer2” e “ViewContainer”
Fino ad ora abbiamo visto come possiamo manipolare il DOM usando le direttive build-in presenti nel framework, ma nel caso queste non bastassero dobbiamo iniziare a lavorare ad un livello più basso ed è in questo caso che entrano in gioco “Rendere2” e “ViewContainer”.
“Rendere2” viene usato quando vogliamo lavorare con gli attributi di un elemento.
“ViewContainer” viene usato quando vogliamo aggiungere o rimuovere degli elementi.
Prima di continuare dobbiamo fare una premessa per capire meglio quello che verrà dopo. Abbiamo detto che Angular è un “framework per lo sviluppo di applicazioni web” più comunemente dette SPA. Gli sviluppatori di Angular non si sono solo prefissati che un applicazione possa essere eseguita all’interno del browser, ma che possa anche essere eseguita in un web-workers o server-side. Detto ciò, in alcuni momenti un browser come lo intendiamo noi potrebbe non essere presente, per cui hanno introdotto una serie di astrazioni e “Render2” è appunto una di queste.
“Renderer2”
ElementRef: fornisce una referenza ad un elemento del DOM, può essere iniettato nel costruttore di un componet / directive oppure letto tramite: @ViewChild(), @ViewChildren. Nel caso la iniettiamo nel costruttore di una direttiva fornisce una referenza all’elemento sul quale la direttiva è applicata.
Rendere2: è un astrazione al DOM per non incorrere agli effetti server-side o web-workers, di norma tale classe è iniettata nel costruttore di un componet / directive.
Queste classi lavorano insieme (Rendere2 lavora sul DOM mentre ElementRef è una referenza ad un elemento del DOM). Sotto possiamo vedere il codice di una semplice direttiva.
Come si può vedere manipolare il DOM con “Rendere2” non è più complicato che manipolare il DOM direttamente, inoltre “Rendere2” contiene una serie metodi con i quali è possibile effettuare le principali operazioni sugli attributi: setAttribute(), removeAttribute(), addClass(), removeClass(), setStyle(), removeStyle().
“ViewContainer”
La prima cosa da dire è che ogni elemento del DOM può essere usato come ViewContainer, inoltre Angular non inserisce il contenuto all’interno di un ViewContainer ma lo appende dopo. Un candidato ideale come ViewContainer può essere l’elemento “ng-container”, questo perché viene renderizzato come commento evitando di inserire nel DOM elementi ritondanti. Un ViewContainer possiede vari metodi tra cui: createEmbeddedView() e createComponent(). Con questi metodi è possibile creare EmbeddedView e HostView, le prime sono create usando come modello un “Template” le seconde sono create usando come modello un “Component”.
ViewContainerRef: rappresenta un contenitore o placeholder dove una o più viste possono essere attaccate. Può essere iniettato nel costruttore oppure letto tramite: @ViewChild(), @ViewChildren(). Se iniettato nel costruttore di un componente, rappresenta il componente stesso.
TemplateRef: rappresenta il template con cui è possibile creare un EmbeddedView. Può essere iniettato nel costruttore (nel caso si tratti di una direttiva custum) oppure letto tramite: @ViewChild(), @ViewChildren().
Sotto si può vedere un esempio di codice di un EmbeddedView (ovvero view che usano come modello un template).
Come si può vedere abbiamo un div / #container1 ed un template #tpl1. All’interno del componente tramite @ViewChild() vengono lette le reference di questi 2 elementi e all’interno del metodo ngAfterViewInit() viene creata l’EmbeddedView tramite il metodo this.vcr1.createEmbeddedView(this.tpl1). Da notare che l’ EmbeddedView è stata creata all’interno del metodo ngAfterViewInit() perché le reference recuperate tramite @ViewChild() sono disponibili dopo questo evento, prima avrebbero valore undfined se stiamo usando componenti dinamici. Con Angular 8 le cose cambiano leggermente, maggiori informazioni quì.
Ciò vale tutte le volte che si utilizza @ViewChild(), @ViewChildren (), etc. In questo modo il codice HTML contenuto nel template viene inserito in modo sicuro all’interno del DOM. La classe ViewContainer fornisce inoltre una serie di altri metodi tra cui clear(), con cui è possibile rimuovere il codice HTML del template dal DOM.
Sotto si può vedere un esempio di codice di un EmbeddedView (ovvero view che usano come modello una direttiva strutturale custom).
Come si può vedere, all’interno del nostro componente abbiamo 2 bottoni (uno per creare e l’altro per rimuovere) ed una direttiva strutturale custom contenuta in un div.
Come menzionato sopra, la direttiva viene tradotta dal compilatore in questo modo
All’interno del codice TypeScript della direttiva, vengono iniettati nel costruttore il ViewContainerRef ed il TemplateRef. In questo modo quando premiamo i bottoni di creazione e rimozione vengono chiamati rispettivamente i metodi del ViewContainer: this.vcr.createEmbeddedView(this.tpl) e this.vcr.clear(). Come si può vedere il codice di una direttiva strutturale è abbastanza simile al codice utilizzato per creare un EmbeddedView all’interno di un componente. Il vantaggio è che in questo modo ho un maggior riutilizzo del codice oltre al fatto di separare le responsabilità Component / Directive.
Sotto si può vedere un esempio di codice di un HostView (ovvero view che usano come modello un Component).
In questo esempio abbiamo scelto di utilizzare come modello un componente (ovvero vogliamo creare un HostView). Non essendo possibile istanziare il componente direttamente con una new dobbiamo crearlo utilizzando la factory relativa al componete. Per far ciò, all’interno del costruttore iniettiamo il servizio “ComponentFactoryResolver” e poi tramite questo servizio creiamo la factory del componente che vogliamo creare. Infine tramite il metodo createComponent(factory) del viewcontainer creiamo il nostro componente passando come parametro la factory appena creata. In questo modo il codice HTML contenuto nel componente viene inserito in modo sicuro all’interno del DOM. La classe ViewContainer fornisce inoltre una serie di altri metodi tra cui clear(), con cui è possibile rimuovere il codice dal DOM.
ngTemplateOutlet e NgComponentOutlet
Fino ad ora abbiamo utilizzato le api fornite da ViewContainerRef: createEmbeddedView() e createComponent(); per creare EmbeddedView e HostView utilizzando un approccio imperativo da codice, ma Angular ci mette anche a disposizione delle direttive strutturali per fare la stessa cosa in modo dichiarativo. Queste direttive sono usate frequentemente con ng-container, ma possono anche essere applicate ad un elemento html.
In questo esempio è stata utilizzata la direttiva “ngTemplateOutlet” all’interno di un ng-container, passando come parametro la template reference variable che identifica il template da renderizzare. Come si può vedere non è stato necessario utilizzare nessuna delle api degli esempi precedenti, rendendo la cosa molto più veloce! Anche se non mostrato nell’esempio, è anche possibile definire e passare al template un context contenente delle variabili.
In questo esempio è stata utilizzata la direttiva “ngComponentOutlet” all’interno di un ng-container, passando come parametro la variabile compfirst tipizzata come CompFirstComponent. Come si può vedere non è stato necessario utilizzare nessuna delle api degli esempi precedenti, rendendo la cosa molto più veloce! Anche se non mostrato nell’esempio, è anche possibile definire e passare al componente che verrà creato un injector sul quale sono stati registrati dei servizi.
Come si può vedere l’utilizzo delle direttive strutturali “ngTemplateOutlet” e “ngComponentOutlet” permettono la creazione di EmbeddedView e HostView in modo dichiarativo, senza la necessità di richiamare le api degli esempi precedenti.
Conclusioni
Fino ad ora sono state utilizzate le API presenti nel framework. Cose analoghe possono anche essere fatte utilizzando gli “Overlay” presenti in “Angular Material”, ma non è lo scopo di quest’articolo.
1) Introduzione ad Angular / Sommario
2) Angular Component Communication (Data Binding) – parte 1
3) Angular Component Communication (decoratori @Input, @Output) – parte 2
5) Cosa sono le “projection” in Angular (ng-content ContentChild ContentChildren)
6) Come manipolare il DOM da un applicazione Angular
Se volete contattarmi il mio profilo Linkedin è il seguente:
Stefano Marchisio - sviluppatore freelance: angular | asp.net core mvc c#
Top comments (0)