Esplorare le tecniche di manipolazione DOM in Angular usando ViewContainerRef

Ogni volta che leggo di lavorare con DOM in Angular vedo sempre una o poche di queste classi menzionate: ElementRef, TemplateRef, ViewContainerRef e altre. Sfortunatamente, anche se alcune di esse sono coperte nei documenti di Angular o in articoli correlati, non ho ancora trovato la descrizione del modello mentale generale e degli esempi di come funzionano insieme. Questo articolo mira a descrivere tale modello.

Se state cercando informazioni più approfondite sulla manipolazione del DOM in Angular usando Renderer e View Containers guardate il mio intervento a NgVikings. Oppure leggete un articolo approfondito sulla manipolazione dinamica del DOM Lavorare con DOM in Angular: conseguenze inaspettate e tecniche di ottimizzazione

Se venite dal mondo angular.js, sapete che era abbastanza facile manipolare il DOM. Angular iniettava il DOM element nella funzione link e si poteva interrogare qualsiasi nodo all’interno del template del componente, aggiungere o rimuovere nodi figli, modificare gli stili ecc. Tuttavia, questo approccio aveva una grande lacuna – era strettamente legato alla piattaforma del browser.

La nuova versione di Angular funziona su diverse piattaforme – in un browser, su una piattaforma mobile o all’interno di un web worker. Quindi è necessario un livello di astrazione per stare tra le API specifiche della piattaforma e le interfacce del framework. In Angular queste astrazioni si presentano sotto forma dei seguenti tipi di riferimento: ElementRef, TemplateRef, ViewRef, ComponentRef e ViewContainerRef. In questo articolo daremo un’occhiata a ciascun tipo di riferimento in dettaglio e mostreremo come possono essere usati per manipolare DOM.

@ViewChildLink a questa sezione

Prima di esplorare le astrazioni DOM, cerchiamo di capire come accedere a queste astrazioni all’interno di un componente/classe direttiva. Angular fornisce un meccanismo chiamato DOM queries. Si presenta sotto forma di @ViewChild e @ViewChildren decoratori. Si comportano allo stesso modo, solo che il primo restituisce un riferimento, mentre il secondo restituisce più riferimenti come un oggetto QueryList. Negli esempi di questo articolo userò principalmente il decoratore ViewChild e non userò il simbolo @ prima di esso.

Di solito, questi decoratori sono accoppiati a variabili di riferimento template. Una variabile di riferimento al template è semplicemente un riferimento nominativo ad un elemento DOM all’interno di un template. Puoi vederla come qualcosa di simile all’attributo id di un elemento html. Si contrassegna un elemento DOM con un riferimento al template e poi lo si interroga all’interno di una classe usando il decoratore ViewChild. Ecco l’esempio di base:

<>Copy
@Component({ selector: 'sample', template: ` <span #tref>I am span</span> `})export class SampleComponent implements AfterViewInit { @ViewChild("tref", {read: ElementRef}) tref: ElementRef; ngAfterViewInit(): void { // outputs `I am span` console.log(this.tref.nativeElement.textContent); }}

La sintassi di base del decoratore ViewChild è:

<>Copy
@ViewChild(, {read: });

In questo esempio potete vedere che ho specificato tref come nome di riferimento del template nel html e ricevere il ElementRef associato a questo elemento. Il secondo parametro read non è sempre richiesto, poiché Angular può dedurre il tipo di riferimento dal tipo dell’elemento DOM. Per esempio, se è un semplice elemento html come span, Angular restituisce ElementRef. Se è un elemento template, restituisce TemplateRef. Alcuni riferimenti, come ViewContainerRef non possono essere dedotti e devono essere richiesti specificamente nel parametro read. Altri, come ViewRef non possono essere restituiti dal DOM e devono essere costruiti manualmente.

Ok, ora che sappiamo come interrogare i riferimenti, cominciamo ad esplorarli.

ElementRefLink a questa sezione

Questa è l’astrazione più basilare. Se osservate la sua struttura di classe, vedrete che tiene solo l’elemento nativo a cui è associato. È utile per accedere all’elemento DOM nativo, come possiamo vedere qui:

<>Copy
// outputs `I am span`console.log(this.tref.nativeElement.textContent);

Tuttavia, tale uso è scoraggiato dal team Angular. Non solo rappresenta un rischio per la sicurezza, ma accoppia strettamente l’applicazione e i livelli di rendering, il che rende difficile eseguire un’applicazione su più piattaforme. Credo che non sia l’accesso a nativeElement a rompere l’astrazione, ma piuttosto l’uso di una specifica API DOM come textContent. Ma come vedrete più avanti, il modello mentale di manipolazione DOM implementato in Angular non richiede quasi mai un tale accesso di livello inferiore.

ElementRef può essere restituito per qualsiasi elemento DOM utilizzando il decoratore ViewChild. Ma poiché tutti i componenti sono ospitati all’interno di un elemento DOM personalizzato e tutte le direttive sono applicate agli elementi DOM, le classi dei componenti e delle direttive possono ottenere un’istanza di ElementRef associata al loro elemento ospite tramite Dependency Injection (DI):

<>Copy
@Component({ selector: 'sample', ...export class SampleComponent{ constructor(private hostElement: ElementRef) { //outputs <sample>...</sample> console.log(this.hostElement.nativeElement.outerHTML); }

Quindi, mentre un componente può ottenere l’accesso al suo elemento ospite attraverso la DI, il decoratore ViewChild è usato più spesso per ottenere un riferimento a un elemento DOM nella sua vista (template). Ma è il contrario per le direttive – non hanno viste e di solito lavorano direttamente con l’elemento a cui sono collegate.

TemplateRefLink a questa sezione

La nozione di template dovrebbe essere familiare alla maggior parte degli sviluppatori web. È un gruppo di elementi DOM che vengono riutilizzati nelle viste in tutta l’applicazione. Prima che lo standard HTML5 introducesse il tag template, la maggior parte dei template arrivavano al browser avvolti in un tag script con qualche variazione dell’attributo type:

<>Copy
<script type="text/template"> <span>I am span in template</span></script>

Questo approccio aveva certamente molti svantaggi come la semantica e la necessità di creare manualmente modelli DOM. Con il tag template un browser analizza html e crea un albero DOM ma non lo rende. Si può quindi accedervi attraverso la proprietà content:

<>Copy
<script> let tpl = document.querySelector('#tpl'); let container = document.querySelector('.insert-after-me'); insertAfter(container, tpl.content);</script><div class="insert-after-me"></div><ng-template> <span>I am span in template</span></ng-template>

Angular abbraccia questo approccio e implementa la classe TemplateRef per lavorare con un modello. Ecco come può essere usata:

<>Copy
@Component({ selector: 'sample', template: ` <ng-template #tpl> <span>I am span in template</span> </ng-template> `})export class SampleComponent implements AfterViewInit { @ViewChild("tpl") tpl: TemplateRef<any>; ngAfterViewInit() { let elementRef = this.tpl.elementRef; // outputs `template bindings={}` console.log(elementRef.nativeElement.textContent); }}

Il framework rimuove l’elemento template dal DOM e inserisce un commento al suo posto. Questo è come appare quando viene reso:

<>Copy
<sample> <!--template bindings={}--></sample>

Di per sé la classe TemplateRef è una classe semplice. Tiene un riferimento al suo elemento ospite nella proprietà elementRef e ha un metodo: createEmbeddedView. Tuttavia, questo metodo è molto utile poiché ci permette di creare una vista e restituire un riferimento ad essa come ViewRef.

ViewRefLink a questa sezione

Questo tipo di astrazione rappresenta una vista Angular. Nel mondo Angular una View è un elemento fondamentale dell’interfaccia utente dell’applicazione. È il più piccolo raggruppamento di elementi che vengono creati e distrutti insieme. La filosofia di Angular incoraggia gli sviluppatori a vedere l’interfaccia utente come una composizione di viste, non come un albero di tag HTML indipendenti.

Angular supporta due tipi di viste:

  • Viste embedded che sono collegate a un template
  • Viste host che sono collegate a un componente

Creazione di una vista embeddedLink a questa sezione

Un template contiene semplicemente un blueprint per una vista. Una vista può essere istanziata dal template usando il suddetto metodo createEmbeddedView come questo:

<>Copy
ngAfterViewInit() { let view = this.tpl.createEmbeddedView(null);}

Creazione di una vista hostLink a questa sezione

Le viste host sono create quando un componente viene istanziato dinamicamente. Un componente può essere creato dinamicamente usando ComponentFactoryResolver:

<>Copy
constructor(private injector: Injector, private r: ComponentFactoryResolver) { let factory = this.r.resolveComponentFactory(ColorComponent); let componentRef = factory.create(injector); let view = componentRef.hostView;}

In Angular, ogni componente è legato ad una particolare istanza di un iniettore, quindi stiamo passando l’istanza corrente dell’iniettore quando si crea il componente. Inoltre, non dimenticate che i componenti che sono istanziati dinamicamente devono essere aggiunti agli EntryComponents di un modulo o di un componente ospitante.

Quindi, abbiamo visto come possono essere create sia le viste embedded che quelle host. Una volta creata una vista può essere inserita nel DOM usando ViewContainer. La prossima sezione esplora le sue funzionalità.

ViewContainerRefLink a questa sezione

ViewContainerRef rappresenta un contenitore dove una o più viste possono essere attaccate.

La prima cosa da menzionare qui è che qualsiasi elemento DOM può essere usato come contenitore della vista. Ciò che è interessante è che Angular non inserisce le viste all’interno dell’elemento, ma le aggiunge dopo l’elemento legato a ViewContainer. Questo è simile a come il router-outlet inserisce i componenti.

Di solito, un buon candidato per segnare un posto dove un ViewContainer dovrebbe essere creato è l’elemento ng-container. È reso come un commento e quindi non introduce elementi HTML ridondanti nel DOM. Ecco l’esempio della creazione di un ViewContainer in un posto specifico in un template di componente:

<>Copy
@Component({ selector: 'sample', template: ` <span>I am first span</span> <ng-container #vc></ng-container> <span>I am last span</span> `})export class SampleComponent implements AfterViewInit { @ViewChild("vc", {read: ViewContainerRef}) vc: ViewContainerRef; ngAfterViewInit(): void { // outputs `template bindings={}` console.log(this.vc.element.nativeElement.textContent); }}

Proprio come con altre astrazioni DOM, ViewContainer è legato ad un particolare elemento DOM a cui si accede attraverso la proprietà element. Nell’esempio sopra è legato all’elemento ng-container reso come commento, e quindi l’output è template bindings={}.

Manipolazione delle visteLink a questa sezione

ViewContainer fornisce una comoda API per manipolare le viste:

<>Copy
class ViewContainerRef { ... clear() : void insert(viewRef: ViewRef, index?: number) : ViewRef get(index: number) : ViewRef indexOf(viewRef: ViewRef) : number detach(index?: number) : ViewRef move(viewRef: ViewRef, currentIndex: number) : ViewRef}

Abbiamo visto prima come due tipi di viste possono essere create manualmente da un template e un componente. Una volta che abbiamo una vista, possiamo inserirla in un DOM usando il metodo insert. Ecco l’esempio della creazione di una vista incorporata da un template e del suo inserimento in un posto specifico segnato da un elemento ng-container:

<>Copy
@Component({ selector: 'sample', template: ` <span>I am first span</span> <ng-container #vc></ng-container> <span>I am last span</span> <ng-template #tpl> <span>I am span in template</span> </ng-template> `})export class SampleComponent implements AfterViewInit { @ViewChild("vc", {read: ViewContainerRef}) vc: ViewContainerRef; @ViewChild("tpl") tpl: TemplateRef<any>; ngAfterViewInit() { let view = this.tpl.createEmbeddedView(null); this.vc.insert(view); }}

Con questa implementazione, il html risultante appare così:

<>Copy
<sample> <span>I am first span</span> <!--template bindings={}--> <span>I am span in template</span> <span>I am last span</span> <!--template bindings={}--></sample>

Per rimuovere una vista dal DOM, possiamo usare il metodo detach. Tutti gli altri metodi sono autoesplicativi e possono essere usati per ottenere un riferimento a una vista tramite l’indice, spostare la vista in un’altra posizione o rimuovere tutte le viste dal contenitore.

Creazione di visteLink a questa sezione

ViewContainer fornisce anche un’API per creare automaticamente una vista:

<>Copy
class ViewContainerRef { element: ElementRef length: number createComponent(componentFactory...): ComponentRef<C> createEmbeddedView(templateRef...): EmbeddedViewRef<C> ...}

Sono semplicemente comodi wrapper a ciò che abbiamo fatto manualmente sopra. Creano una vista da un template o da un componente e la inseriscono nella posizione specificata.

ngTemplateOutlet e ngComponentOutletLink a questa sezione

Anche se è sempre bene sapere come funzionano i meccanismi sottostanti, è solitamente desiderabile avere una sorta di scorciatoia. Questa scorciatoia si presenta sotto forma di due direttive: ngTemplateOutlet e ngComponentOutlet. Al momento di scrivere entrambe sono sperimentali e ngComponentOutlet sarà disponibile a partire dalla versione 4. Ma se avete letto tutto quanto sopra, sarà molto facile capire cosa fanno.

ngTemplateOutletLink a questa sezione

Questa contrassegna un elemento DOM come un ViewContainer e inserisce una vista incorporata creata da un template senza la necessità di farlo esplicitamente in una classe componente. Ciò significa che l’esempio precedente in cui abbiamo creato una vista e l’abbiamo inserita in un elemento DOM #vc può essere riscritto in questo modo:

<>Copy
@Component({ selector: 'sample', template: ` <span>I am first span</span> <ng-container ="tpl"></ng-container> <span>I am last span</span> <ng-template #tpl> <span>I am span in template</span> </ng-template> `})export class SampleComponent {}

Come potete vedere non usiamo alcun codice per istanziare la vista nella classe componente. Molto utile!

ngComponentOutletLink a questa sezione

Questa direttiva è analoga a ngTemplateOutlet con la differenza che crea una vista host (istanzia un componente), e non una vista incorporata. Potete usarla in questo modo:

<>Copy
<ng-container *ngComponentOutlet="ColorComponent"></ng-container>

Wrapping upLink a questa sezione

Mi rendo conto che tutte queste informazioni possono essere molto da digerire. Ma in realtà è abbastanza coerente e delinea un chiaro modello mentale per manipolare il DOM tramite le viste. Si ottengono riferimenti alle astrazioni DOM di Angular usando una query ViewChild insieme a riferimenti a variabili template. Il wrapper più semplice intorno ad un elemento DOM è ElementRef. Per i template avete TemplateRef che vi permette di creare una vista incorporata. Si può accedere alle viste host su componentRef create usando ComponentFactoryResolver. Le viste possono essere manipolate con ViewContainerRef. Ci sono due direttive che rendono automatico il processo manuale: ngTemplateOutlet – per le viste incorporate e ngComponentOutlet per le viste host (componenti dinamici).

Discutere con la comunità

Leave a Reply