Explorarea tehnicilor de manipulare a DOM în Angular folosind ViewContainerRef

Când citesc despre lucrul cu DOM în Angular, văd întotdeauna una sau câteva dintre aceste clase menționate: ElementRef, TemplateRef, ViewContainerRef și altele. Din păcate, deși unele dintre ele sunt abordate în documentele Angular sau în articole conexe, încă nu am găsit descrierea modelului mental general și exemple despre cum funcționează acestea împreună. Acest articol își propune să descrie un astfel de model.

Dacă sunteți în căutarea unor informații mai aprofundate despre manipularea DOM în Angular folosind Renderer și View Containers, verificați conferința mea de la NgVikings. Sau citiți un articol aprofundat despre manipularea dinamică a DOM Lucrul cu DOM în Angular: consecințe neașteptate și tehnici de optimizare

Dacă veniți din lumea angular.js, știți că era destul de ușor să manipulați DOM-ul. Angular injecta DOM element în funcția link și puteai să interoghezi orice nod din șablonul componentei, să adaugi sau să elimini noduri copil, să modifici stiluri etc. Totuși, această abordare avea un neajuns major – era strâns legată de o platformă de browser.

Noua versiune Angular rulează pe diferite platforme – într-un browser, pe o platformă mobilă sau în interiorul unui web worker. Astfel, este necesar un nivel de abstractizare care să stea între API-ul specific platformei și interfețele cadrului. În angular, aceste abstracțiuni vin sub forma următoarelor tipuri de referință: ElementRef, TemplateRef, ViewRef, ComponentRef și ViewContainerRef. În acest articol vom analiza în detaliu fiecare tip de referință și vom arăta cum pot fi utilizate pentru a manipula DOM.

@ViewChildLink la această secțiune

Până să explorăm abstracțiunile DOM, să înțelegem cum accesăm aceste abstracțiuni în interiorul unei clase de componente/directive. Angular oferă un mecanism numit interogări DOM. Acesta se prezintă sub forma unor decoratori @ViewChild și @ViewChildren. Acestea se comportă la fel, doar că primul returnează o referință, în timp ce al doilea returnează mai multe referințe sub forma unui obiect QueryList. În exemplele din acest articol voi folosi mai ales decoratorul ViewChild și nu voi folosi simbolul @ înaintea acestuia.

De obicei, acești decoratori sunt asociați cu variabile de referință de șablon. O variabilă de referință a șablonului este pur și simplu o referință numită la un element DOM în cadrul unui șablon. Puteți să o vedeți ca pe ceva similar cu atributul id al unui element html. Marcați un element DOM cu o referință de șablon și apoi îl interogați în interiorul unei clase folosind decoratorul ViewChild. Iată exemplul de bază:

<>Copie
@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); }}

Sintaxa de bază a decoratorului ViewChild este: ViewChild:

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

În acest exemplu puteți vedea că am specificat tref ca nume de referință al șablonului în html și primesc ElementRef asociat cu acest element. Al doilea parametru read nu este întotdeauna necesar, deoarece Angular poate deduce tipul de referință prin tipul elementului DOM. De exemplu, dacă este un element html simplu, cum ar fi span, Angular returnează ElementRef. Dacă este un element template, returnează TemplateRef. Unele referințe, cum ar fi ViewContainerRef nu pot fi deduse și trebuie să fie cerute în mod specific în parametrul read. Altele, cum ar fi ViewRef nu pot fi returnate din DOM și trebuie construite manual.

Bine, acum că știm cum să interogăm referințele, să începem să le explorăm.

ElementRefLink către această secțiune

Aceasta este cea mai elementară abstractizare. Dacă observați structura clasei sale, veți vedea că deține doar elementul nativ cu care este asociat. Este utilă pentru accesarea elementului DOM nativ, așa cum putem vedea aici:

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

Cu toate acestea, o astfel de utilizare este descurajată de către echipa Angular. Nu numai că prezintă un risc de securitate, dar, de asemenea, cuplează strâns aplicația și straturile de redare, ceea ce face dificilă rularea unei aplicații pe mai multe platforme. Cred că nu accesul la nativeElement rupe abstractizarea, ci mai degrabă utilizarea unui API DOM specific, cum ar fi textContent. Dar, după cum veți vedea mai târziu, modelul mental de manipulare DOM implementat în Angular nu necesită aproape niciodată un astfel de acces de nivel inferior.

ElementRef poate fi returnat pentru orice element DOM folosind decoratorul ViewChild. Dar, deoarece toate componentele sunt găzduite în interiorul unui element DOM personalizat și toate directivele sunt aplicate elementelor DOM, clasele de componente și directive pot obține o instanță de ElementRef asociată cu elementul lor gazdă prin intermediul injecției de dependență (DI):

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

Atunci, în timp ce o componentă poate obține acces la elementul său gazdă prin DI, decoratorul ViewChild este utilizat cel mai adesea pentru a obține o referință la un element DOM în vizualizarea sa (șablon). Dar, este invers în cazul directivelor – acestea nu au vizualizări și, de obicei, lucrează direct cu elementul la care sunt atașate.

TemplateRefLink to this section

Noțiunea de șablon ar trebui să fie familiară pentru majoritatea dezvoltatorilor web. Este un grup de elemente DOM care sunt refolosite în vizualizări în întreaga aplicație. Înainte ca standardul HTML5 să introducă tag-ul șablon, majoritatea șabloanelor ajungeau în browser înfășurate într-un tag script cu o variație a atributului type:

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

Această abordare avea cu siguranță multe dezavantaje, cum ar fi semantica și necesitatea de a crea manual modele DOM. Cu tag-ul template, un browser analizează html și creează un arbore DOM, dar nu îl redă. Acesta poate fi apoi accesat prin intermediul proprietății content:

<>Copie
<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 îmbrățișează această abordare și implementează clasa TemplateRef pentru a lucra cu un model. Iată cum poate fi utilizată:

<>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); }}

Cadrul elimină elementul template din DOM și inserează un comentariu în locul acestuia. Acesta este modul în care arată atunci când este redat:

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

Pe cont propriu, clasa TemplateRef este o clasă simplă. Ea deține o referință la elementul său gazdă în proprietatea elementRef și are o singură metodă: createEmbeddedView. Cu toate acestea, această metodă este foarte utilă, deoarece ne permite să creăm o vizualizare și să returnăm o referință la aceasta ca ViewRef.

ViewRefLink către această secțiune

Acest tip de abstracție reprezintă o vizualizare Angular View. În lumea Angular, un View este o componentă fundamentală a interfeței de utilizare a aplicației. Este cea mai mică grupare de elemente care sunt create și distruse împreună. Filozofia Angular încurajează dezvoltatorii să vadă interfața de utilizator ca pe o compoziție de vizualizări, nu ca pe un arbore de etichete HTML de sine stătătoare.

Angular acceptă două tipuri de vizualizări:

  • Vizualizări încorporate care sunt legate de un șablon
  • Vizualizări gazdă care sunt legate de o componentă

Crearea unei vizualizări încorporateLegătura către această secțiune

Un șablon conține pur și simplu o schiță pentru o vizualizare. O vizualizare poate fi instanțiată din șablon folosind metoda createEmbeddedView menționată mai sus, astfel:

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

Crearea unei vizualizări gazdăLink către această secțiune

Visualizările gazdă sunt create atunci când o componentă este instanțiată dinamic. O componentă poate fi creată în mod dinamic folosind ComponentFactoryResolver:

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

În Angular, fiecare componentă este legată de o anumită instanță a unui injector, așa că trecem instanța curentă a injectorului atunci când creăm componenta. De asemenea, nu uitați că componentele care sunt instanțiate dinamic trebuie să fie adăugate la EntryComponents ale unui modul sau ale unei componente gazdă.

Acum am văzut cum pot fi create atât vederile încorporate, cât și cele gazdă. Odată ce o vizualizare este creată, aceasta poate fi inserată în DOM folosind ViewContainer. Următoarea secțiune explorează funcționalitatea acesteia.

ViewContainerRefLink către această secțiune

ViewContainerRef reprezintă un container în care pot fi atașate una sau mai multe vizualizări.

Primul lucru care trebuie menționat aici este că orice element DOM poate fi folosit ca un container de vizualizare. Ceea ce este interesant este că Angular nu inserează vizualizările în interiorul elementului, ci le atașează după elementul legat la ViewContainer. Acest lucru este similar cu modul în care router-outlet inserează componentele.

De obicei, un bun candidat pentru a marca un loc în care ar trebui creat un ViewContainer este elementul ng-container. Acesta este redat ca un comentariu și astfel nu introduce elemente HTML redundante în DOM. Iată exemplul de creare a unui ViewContainer într-un anumit loc dintr-un șablon de componentă:

<>Copie
@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); }}

La fel ca și în cazul altor abstracțiuni DOM, ViewContainer este legat de un anumit element DOM accesat prin intermediul proprietății element. În exemplul de mai sus este legată de elementul ng-container redat ca un comentariu, și astfel ieșirea este template bindings={}.

Manipularea vizualizărilorLegăturați această secțiune

ViewContainer oferă o API convenabilă pentru manipularea vizualizărilor:

<>Copie
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}

Am văzut mai devreme cum două tipuri de vizualizări pot fi create manual dintr-un șablon și o componentă. Odată ce avem o vizualizare, o putem insera într-un DOM folosind metoda insert. Iată exemplul de creare a unei vizualizări încorporate dintr-un șablon și de inserare a acesteia într-un anumit loc marcat de un element 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); }}

Cu această implementare, html rezultată arată astfel:

<>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>

Pentru a elimina o vizualizare din DOM, putem folosi metoda detach. Toate celelalte metode se explică de la sine și pot fi folosite pentru a obține o referință la o vizualizare după index, pentru a muta vizualizarea într-o altă locație sau pentru a elimina toate vizualizările din container.

Crearea de vizualizăriLink către această secțiune

ViewContainer oferă, de asemenea, un API pentru a crea o vizualizare în mod automat:

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

Acestea sunt pur și simplu niște învelișuri convenabile pentru ceea ce am făcut manual mai sus. Ele creează o vizualizare dintr-un șablon sau o componentă și o inserează în locația specificată.

ngTemplateOutlet și ngComponentOutletLink la această secțiune

În timp ce este întotdeauna bine să știm cum funcționează mecanismele de bază, de obicei este de dorit să avem un fel de scurtătură. Această scurtătură vine sub forma a două directive: ngTemplateOutlet și ngComponentOutlet. La momentul scrierii acestui articol, ambele sunt experimentale, iar ngComponentOutlet va fi disponibilă începând cu versiunea 4. Dar dacă ați citit tot ce s-a scris mai sus, va fi foarte ușor să înțelegeți ce fac ele.

ngTemplateOutletLink la această secțiune

Aceasta marchează un element DOM ca fiind un ViewContainer și inserează o vedere încorporată creată de un șablon fără a fi nevoie să faceți acest lucru în mod explicit într-o clasă de componentă. Aceasta înseamnă că exemplul de mai sus, în care am creat o vizualizare și am inserat-o într-un element DOM #vc, poate fi rescris astfel:

<>Copiază
@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 {}

După cum puteți vedea, nu folosim niciun cod de instanțiere a vizualizării în clasa de componente. Foarte la îndemână!

ngComponentOutletLink către această secțiune

Această directivă este analogă cu ngTemplateOutlet cu diferența că ea creează o vedere gazdă (instanțiază o componentă), și nu o vedere încorporată. O puteți utiliza astfel:

<>Copiază
<ng-container *ngComponentOutlet="ColorComponent"></ng-container>

ÎncheiereLink la această secțiune

Îmi dau seama că toate aceste informații pot fi foarte greu de digerat. Dar, de fapt, este destul de coerentă și stabilește un model mental clar pentru manipularea DOM prin intermediul vizualizărilor. Obțineți referințe la abstracțiile DOM din Angular prin utilizarea unei interogări ViewChild împreună cu referințe la variabilele șablon. Cel mai simplu înveliș în jurul unui element DOM este ElementRef. Pentru șabloane aveți TemplateRef care vă permite să creați o vizualizare încorporată. Vizualizările gazdă pot fi accesate pe componentRef create folosind ComponentFactoryResolver. Vizualizările pot fi manipulate cu ViewContainerRef. Există două directive care fac ca procesul manual să fie automat: ngTemplateOutlet – pentru vederile încorporate și ngComponentOutlet pentru vederile gazdă (componente dinamice).

Discută cu comunitatea

Leave a Reply