Udforskning af Angular DOM-manipulationsteknikker ved hjælp af ViewContainerRef

Når jeg læser om at arbejde med DOM i Angular, ser jeg altid en eller få af disse klasser nævnt: ElementRef, TemplateRef, ViewContainerRef og andre. Desværre, selv om nogle af dem er dækket i Angular-dokumentationen eller relaterede artikler, har jeg endnu ikke fundet beskrivelsen af den overordnede mentale model og eksempler på, hvordan disse arbejder sammen. Denne artikel har til formål at beskrive en sådan model.

Hvis du er på udkig efter mere dybdegående oplysninger om DOM-manipulation i Angular ved hjælp af Renderer og View Containers, så tjek min talk på NgVikings. Eller læs en dybdegående artikel om dynamisk DOM-manipulation Arbejde med DOM i Angular: uventede konsekvenser og optimeringsteknikker

Hvis du kommer fra angular.js verden, ved du, at det var ret nemt at manipulere DOM. Angular injicerede DOM element i link-funktionen, og du kunne forespørge enhver node inden for komponentens skabelon, tilføje eller fjerne underordnede noder, ændre stilarter osv. Denne tilgang havde dog én stor mangel – den var tæt bundet til en browserplatform.

Den nye Angular-version kører på forskellige platforme – i en browser, på en mobilplatform eller inde i en web worker. Så der er behov for et abstraktionsniveau for at stå mellem platformsspecifikke API’er og rammegrænsefladerne. I angular kommer disse abstraktioner i en form af følgende referencetyper: ElementRef, TemplateRef, ViewRef, ComponentRef og ViewContainerRef. I denne artikel tager vi et kig på hver referencetype i detaljer og viser, hvordan de kan bruges til at manipulere DOM.

@ViewChildLink til dette afsnit

Hvor vi udforsker DOM-abstraktionerne, skal vi forstå, hvordan vi får adgang til disse abstraktioner inde i en komponent/directive-klasse. Angular tilbyder en mekanisme kaldet DOM-forespørgsler. Den kommer i form af @ViewChild og @ViewChildren-dekoratorer. De opfører sig på samme måde, blot returnerer førstnævnte én reference, mens sidstnævnte returnerer flere referencer som et QueryList-objekt. I eksemplerne i denne artikel vil jeg hovedsagelig bruge ViewChild-dekoratoren og vil ikke bruge @-symbolet før den.

Sædvanligvis er disse dekoratorer parret med skabelonreferencevariabler. En skabelonreferencevariabel er simpelthen en navngiven reference til et DOM-element i en skabelon. Du kan se det som noget, der svarer til id-attributten på et html-element. Du markerer et DOM-element med en skabelonreference og forespørger det derefter inde i en klasse ved hjælp af ViewChild-dekoratoren. Her er det grundlæggende eksempel:

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

Den grundlæggende syntaks for ViewChild-dekoratoren er følgende:

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

I dette eksempel kan du se, at jeg har angivet tref som et skabelonreferencenavn i html og modtager den ElementRef, der er tilknyttet dette element. Den anden parameter read er ikke altid påkrævet, da Angular kan udlede referencetypen ud fra DOM-elementets type. Hvis det f.eks. er et simpelt html-element som span, returnerer Angular ElementRef. Hvis det er et template-element, returnerer den TemplateRef. Nogle referencer, som ViewContainerRef, kan ikke udledes og skal spørges specifikt efter i parameteren read. Andre, som ViewRef, kan ikke returneres fra DOM og skal konstrueres manuelt.

Okay, nu hvor vi ved, hvordan vi spørger efter referencerne, kan vi begynde at udforske dem.

ElementRefLink til dette afsnit

Dette er den mest grundlæggende abstraktion. Hvis du observerer dens klassestruktur, vil du se, at den kun indeholder det oprindelige element, som den er tilknyttet. Den er nyttig til at få adgang til native DOM-elementet, som vi kan se her:

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

En sådan brug frarådes dog af Angular-teamet. Ikke alene udgør det en sikkerhedsrisiko, men det kobler også dit program- og renderingslag tæt sammen, hvilket gør det vanskeligt at køre en app på flere platforme. Jeg mener ikke, at det er adgangen til nativeElement, der bryder abstraktionen, men derimod brugen af et specifikt DOM API som textContent. Men som du vil se senere, kræver den mentale DOM-manipulationsmodel, der er implementeret i Angular, næsten aldrig en sådan adgang på lavere niveau.

ElementRef kan returneres for ethvert DOM-element ved hjælp af ViewChild-dekoratoren. Men da alle komponenter er hostet inde i et brugerdefineret DOM-element, og alle direktiver anvendes på DOM-elementer, kan komponent- og direktivklasser få en instans af ElementRef, der er tilknyttet deres værtselement, via Dependency Injection (DI):

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

Så mens en komponent kan få adgang til sit værtselement via DI, bruges ViewChild-dekoratoren oftest til at få en reference til et DOM-element i sin visning (skabelon). Men det er omvendt for direktiver – de har ingen visninger, og de arbejder normalt direkte med det element, de er knyttet til.

TemplateRefLink til dette afsnit

Begrebet skabelon burde være velkendt for de fleste webudviklere. Det er en gruppe af DOM-elementer, der genbruges i visninger i hele programmet. Før HTML5-standarden indførte skabelonmærket, ankom de fleste skabeloner til browseren indpakket i et script-tag med en eller anden variation af type-attributten:

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

Denne fremgangsmåde havde bestemt mange ulemper som f.eks. semantikken og nødvendigheden af manuelt at oprette DOM-modeller. Med template-taggen analyserer en browser html og opretter et DOM-træ, men gengiver det ikke. Det kan derefter tilgås via content-egenskaben:

<>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 omfavner denne tilgang og implementerer TemplateRef-klassen til at arbejde med en skabelon. Her er, hvordan den kan bruges:

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

Frameworket fjerner template-elementet fra DOM og indsætter en kommentar i stedet. Sådan ser det ud, når det er gengivet:

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

I sig selv er TemplateRef-klassen en simpel klasse. Den indeholder en reference til sit værtselement i elementRef-egenskaben og har én metode: createEmbeddedView. Denne metode er dog meget nyttig, da den giver os mulighed for at oprette en visning og returnere en reference til den som ViewRef.

ViewRefLink til dette afsnit

Denne type abstraktion repræsenterer en Angular View. I Angular-verdenen er en View en grundlæggende byggesten i applikationens brugergrænseflade. Det er den mindste gruppering af elementer, som oprettes og ødelægges sammen. Angular-filosofien opfordrer udviklere til at se brugergrænsefladen som en sammensætning af Views og ikke som et træ af enkeltstående HTML-tags.

Angular understøtter to typer views:

  • Indlejrede views, som er knyttet til en skabelon
  • Værtsviews, som er knyttet til en komponent

Skabelse af en indlejret viewLink til dette afsnit

En skabelon indeholder simpelthen et blueprint for en view. En visning kan instantieres fra skabelonen ved hjælp af den førnævnte createEmbeddedView-metode på denne måde:

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

Skabelse af en værtsvisningLink til dette afsnit

Værtsvisninger oprettes, når en komponent instantieres dynamisk. En komponent kan oprettes dynamisk ved hjælp af ComponentFactoryResolver:

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

I Angular er hver komponent bundet til en bestemt instans af en injektor, så vi videregiver den aktuelle injektorinstans, når vi opretter komponenten. Glem heller ikke, at komponenter, der instantieres dynamisk, skal tilføjes til EntryComponents for et modul eller en værtskomponent.

Så har vi set, hvordan både indlejrede og værtsvisninger kan oprettes. Når en visning er oprettet, kan den indsættes i DOM’en ved hjælp af ViewContainer. I næste afsnit udforskes dens funktionalitet.

ViewContainerRefLink til dette afsnit

ViewContainerRef repræsenterer en container, hvor en eller flere visninger kan vedhæftes.

Det første, der skal nævnes her, er, at ethvert DOM-element kan bruges som en visningscontainer. Det interessante er, at Angular ikke indsætter visninger inde i elementet, men tilføjer dem efter det element, der er bundet til ViewContainer. Dette svarer til, hvordan router-outlet indsætter komponenter.

Usuelt set er en god kandidat til at markere et sted, hvor der skal oprettes en ViewContainer, ng-container-elementet. Det gengives som en kommentar, og det indfører således ikke overflødige HTML-elementer i DOM’et. Her er et eksempel på oprettelse af en ViewContainer på et bestemt sted i en komponentskabelon:

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

Som med andre DOM-abstraktioner er ViewContainer bundet til et bestemt DOM-element, som der er adgang til via element-egenskaben. I eksemplet ovenfor er det bundet til ng-container-elementet, der gengives som en kommentar, og output er derfor template bindings={}.

Manipulering af visningerLink til dette afsnit

ViewContainer giver et praktisk API til at manipulere visninger:

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

Vi har tidligere set, hvordan to typer visninger kan oprettes manuelt fra en skabelon og en komponent. Når vi har en visning, kan vi indsætte den i en DOM ved hjælp af insert-metoden. Her er et eksempel på at oprette en indlejret visning fra en skabelon og indsætte den på et bestemt sted, der er markeret med et ng-container-element:

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

Med denne implementering ser det resulterende html således ud:

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

For at fjerne en visning fra DOM kan vi bruge detach-metoden. Alle andre metoder er selvforklarende og kan bruges til at få en reference til en visning ved hjælp af indekset, flytte visningen til en anden placering eller fjerne alle visninger fra containeren.

Skabelse af visningerLink til dette afsnit

ViewContainer indeholder også en API til automatisk at oprette en visning:

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

Disse er blot praktiske indpakninger til det, vi har gjort manuelt ovenfor. De opretter en visning fra en skabelon eller komponent og indsætter den på den angivne placering.

ngTemplateOutlet og ngComponentOutletLink til dette afsnit

Selv om det altid er godt at vide, hvordan de underliggende mekanismer fungerer, er det normalt ønskeligt at have en eller anden form for genvej. Denne genvej kommer i form af to direktiver: ngTemplateOutlet og ngComponentOutlet. I skrivende stund er begge eksperimentelle, og ngComponentOutlet vil være tilgængelig fra og med version 4. Men hvis du har læst alt det ovenstående, vil det være meget let at forstå, hvad de gør.

ngTemplateOutletLink til dette afsnit

Dette markerer et DOM-element som et ViewContainer og indsætter en indlejret visning, der er oprettet af en skabelon, uden at det er nødvendigt at gøre det eksplicit i en komponentklasse. Det betyder, at eksemplet ovenfor, hvor vi oprettede en visning og indsatte den i et #vc DOM-element, kan omskrives på denne måde:

<>Kopier
@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 {}

Som du kan se, bruger vi ikke nogen visningsinstantieringskode i komponentklassen. Meget praktisk!

ngComponentOutletLink til dette afsnit

Dette direktiv er analogt med ngTemplateOutlet med den forskel, at det opretter en værtsvisning (instantierer en komponent) og ikke en indlejret visning. Du kan bruge det på denne måde:

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

OpkoblingLink til dette afsnit

Jeg er klar over, at alle disse oplysninger måske er meget at fordøje. Men faktisk er det ret sammenhængende og udstikker en klar mental model til at manipulere DOM via visninger. Du får referencer til Angular DOM-abstraktioner ved hjælp af en ViewChild-forespørgsel sammen med skabelonvariabelreferencer. Den enkleste indpakning omkring et DOM-element er ElementRef. For skabeloner har du TemplateRef, som giver dig mulighed for at oprette en indlejret visning. Værtsvisninger kan tilgås på componentRef oprettet ved hjælp af ComponentFactoryResolver. Visningerne kan manipuleres med ViewContainerRef. Der er to direktiver, der gør den manuelle proces automatisk: ngTemplateOutlet – for indlejrede visninger og ngComponentOutlet for værtsvisninger (dynamiske komponenter).

Diskuter med fællesskabet

Leave a Reply