Utforska tekniker för hantering av DOM i Angular med ViewContainerRef

När jag läser om hur man jobbar med DOM i Angular ser jag alltid att en eller några av dessa klasser nämns: ElementRef, TemplateRef, ViewContainerRef och andra. Även om några av dem behandlas i Angular-dokumentationen eller relaterade artiklar har jag tyvärr ännu inte hittat beskrivningen av den övergripande mentala modellen och exempel på hur dessa fungerar tillsammans. Den här artikeln syftar till att beskriva en sådan modell.

Om du letar efter mer djupgående information om DOM-manipulation i Angular med hjälp av Renderer och View Containers kolla in mitt föredrag på NgVikings. Eller läs en djupgående artikel om dynamisk DOM-manipulering Arbeta med DOM i Angular: oväntade konsekvenser och optimeringstekniker

Om du kommer från angular.js-världen vet du att det var ganska enkelt att manipulera DOM. Angular injicerade DOM element i link-funktionen och du kunde fråga vilken nod som helst inom komponentens mall, lägga till eller ta bort underordnade noder, ändra stilar osv. Det här tillvägagångssättet hade dock en stor brist – det var starkt bundet till en webbläsarplattform.

Den nya Angular-versionen körs på olika plattformar – i en webbläsare, på en mobilplattform eller inuti en webworker. Det krävs alltså en abstraktionsnivå för att stå mellan plattformsspecifika API:er och ramgränssnitten. I Angular kommer dessa abstraktioner i form av följande referenstyper: ElementRef, TemplateRef, ViewRef, ComponentRef och ViewContainerRef. I den här artikeln tar vi en titt på varje referenstyp i detalj och visar hur de kan användas för att manipulera DOM.

@ViewChildLink till det här avsnittet

För att utforska DOM-abstraktionerna ska vi förstå hur vi får tillgång till dessa abstraktioner inuti en komponent/direktivklass. Angular tillhandahåller en mekanism som kallas DOM queries. Den kommer i form av @ViewChild och @ViewChildren dekoratorer. De beter sig på samma sätt, bara att den förstnämnda returnerar en referens, medan den sistnämnda returnerar flera referenser som ett QueryList-objekt. I exemplen i den här artikeln kommer jag främst att använda ViewChild-dekoratorn och kommer inte att använda @-symbolen före den.

I vanliga fall paras dessa dekoratorer ihop med mallreferensvariabler. En mallreferensvariabel är helt enkelt en namngiven referens till ett DOM-element i en mall. Du kan se det som något som liknar id-attributet i ett html-element. Du markerar ett DOM-element med en mallreferens och frågar sedan efter det inuti en klass med hjälp av dekoratorn ViewChild. Här är det grundläggande exemplet:

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

Den grundläggande syntaxen för ViewChild-dekoratorn är:

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

I det här exemplet kan du se att jag har angett tref som mallreferensnamn i html och tar emot ElementRef som är associerad med detta element. Den andra parametern read behövs inte alltid, eftersom Angular kan härleda referenstypen från typen av DOM-elementet. Om det till exempel är ett enkelt html-element som span returnerar Angular ElementRef. Om det är ett template-element returnerar Angular TemplateRef. Vissa referenser, som ViewContainerRef, kan inte härledas och måste efterfrågas specifikt i parametern read. Andra, som ViewRef kan inte returneras från DOM och måste konstrueras manuellt.

Okej, nu när vi vet hur vi frågar efter referenserna kan vi börja utforska dem.

ElementRefLänk till det här avsnittet

Detta är den mest grundläggande abstraktionen. Om du observerar dess klasstruktur ser du att den bara innehåller det ursprungliga elementet som den är associerad med. Den är användbar för att få tillgång till ett inhemskt DOM-element som vi kan se här:

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

En sådan användning avråds dock av Angular-teamet. Det innebär inte bara en säkerhetsrisk, utan det kopplar också ihop program- och renderingslagren, vilket gör det svårt att köra en app på flera olika plattformar. Jag tror inte att det är tillgången till nativeElement som bryter abstraktionen, utan snarare användningen av ett specifikt DOM API som textContent. Men som du kommer att se senare kräver den mentala modellen för DOM-manipulering som implementeras i Angular nästan aldrig sådan åtkomst på lägre nivå.

ElementRef kan returneras för alla DOM-element med hjälp av dekoratorn ViewChild. Men eftersom alla komponenter finns i ett anpassat DOM-element och alla direktiv tillämpas på DOM-element, kan komponent- och direktivklasser få en instans av ElementRef som är associerad med deras värdelement genom Dependency Injection (DI):

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

Så även om en komponent kan få tillgång till sitt värdelement genom DI används dekoratorn ViewChild oftast för att få en referens till ett DOM-element i sin vy (mall). Men det är omvänt för direktiv – de har inga vyer och arbetar vanligtvis direkt med elementet de är kopplade till.

TemplateRefLink till det här avsnittet

Begreppet mall borde vara bekant för de flesta webbutvecklare. Det är en grupp DOM-element som återanvänds i vyer i hela programmet. Innan HTML5-standarden införde malltaggen anlände de flesta mallar till webbläsaren inlindade i en script-tagg med någon variant av type-attributet:

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

Detta tillvägagångssätt hade förvisso många nackdelar, som semantiken och nödvändigheten av att manuellt skapa DOM-modeller. Med template taggen analyserar en webbläsare html och skapar ett DOM träd men renderar det inte. Det kan sedan nås via egenskapen 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 omfamnar detta tillvägagångssätt och implementerar TemplateRef-klassen för att arbeta med en mall. Så här kan den användas:

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

Ramenverket tar bort template-elementet från DOM och infogar en kommentar i dess ställe. Så här ser det ut när det renderas:

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

Som sådan är TemplateRef en enkel klass. Den har en referens till sitt värdelement i egenskapen elementRef och har en metod: createEmbeddedView. Denna metod är dock mycket användbar eftersom den tillåter oss att skapa en vy och returnera en referens till den som ViewRef.

ViewRefLänk till det här avsnittet

Den här typen av abstraktion representerar en Angular View. I Angular-världen är en vy en grundläggande byggsten i programmets användargränssnitt. Det är den minsta grupperingen av element som skapas och förstörs tillsammans. Angular-filosofin uppmuntrar utvecklare att se användargränssnittet som en sammansättning av vyer, inte som ett träd av fristående HTML-taggar.

Angular har stöd för två typer av vyer:

  • Inbäddade vyer som är kopplade till en mall
  • Värdvyer som är kopplade till en komponent

Skapa en inbäddad vyLänk till det här avsnittet

En mall innehåller helt enkelt en plan för en vy. En vy kan instansieras från mallen med hjälp av den tidigare nämnda createEmbeddedView-metoden så här:

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

Skapa en värdvynLänk till det här avsnittet

Värdvyer skapas när en komponent instansieras dynamiskt. En komponent kan skapas dynamiskt med hjälp av ComponentFactoryResolver:

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

I Angular är varje komponent bunden till en viss instans av en injektor, så vi lämnar över den aktuella injektorinstansen när vi skapar komponenten. Glöm inte heller att komponenter som instansieras dynamiskt måste läggas till i EntryComponents för en modul eller värdkomponent.

Så, vi har sett hur både inbäddade och värdvyer kan skapas. När en vy har skapats kan den infogas i DOM med hjälp av ViewContainer. I nästa avsnitt utforskas dess funktionalitet.

ViewContainerRefLänk till det här avsnittet

ViewContainerRef representerar en behållare där en eller flera vyer kan fästas.

Det första som bör nämnas här är att vilket DOM-element som helst kan användas som en vybehållare. Det som är intressant är att Angular inte infogar vyer inuti elementet, utan lägger till dem efter elementet som är bundet till ViewContainer. Detta liknar hur router-outlet infogar komponenter.

En bra kandidat för att markera en plats där en ViewContainer ska skapas är vanligtvis ng-container-elementet. Det återges som en kommentar och introducerar därför inte överflödiga HTML-element i DOM. Här är exemplet på att skapa en ViewContainer på en specifik plats i en komponentmall:

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

Som med andra DOM-abstraktioner är ViewContainer bunden till ett visst DOM-element som nås via egenskapen element. I exemplet ovan är det bundet till elementet ng-container som renderas som en kommentar, så utgången är template bindings={}.

Manipulering av vyerLänk till det här avsnittet

ViewContainer ger ett bekvämt API för att manipulera vyer:

<>Kopiera
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 tidigare sett hur två typer av vyer kan skapas manuellt från en mall och en komponent. När vi har en vy kan vi infoga den i en DOM med hjälp av metoden insert. Här är ett exempel på hur man skapar en inbäddad vy från en mall och infogar den på en specifik plats som markeras av ett 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 denna implementering ser den resulterande html ut så här:

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

För att ta bort en vy från DOM kan vi använda metoden detach. Alla andra metoder är självförklarande och kan användas för att få en referens till en vy genom indexet, flytta vyn till en annan plats eller ta bort alla vyer från behållaren.

Skapa vyerLänk till det här avsnittet

ViewContainer ger också ett API för att skapa en vy automatiskt:

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

Dessa är helt enkelt bekväma inpackningar till det vi har gjort manuellt ovan. De skapar en vy från en mall eller komponent och infogar den på den angivna platsen.

ngTemplateOutlet och ngComponentOutletLänk till det här avsnittet

Samtidigt som det alltid är bra att veta hur de underliggande mekanismerna fungerar, är det oftast önskvärt att ha någon form av genväg. Denna genväg kommer i form av två direktiv: ngTemplateOutlet och ngComponentOutlet. När detta skrivs är båda experimentella och ngComponentOutlet kommer att vara tillgängligt från och med version 4. Men om du har läst allt ovan kommer det att vara mycket lätt att förstå vad de gör.

ngTemplateOutletLänk till det här avsnittet

Detta markerar ett DOM-element som en ViewContainer och infogar en inbäddad vy som skapats av en mall utan att det är nödvändigt att uttryckligen göra detta i en komponentklass. Detta innebär att exemplet ovan där vi skapade en vy och infogade den i ett #vc DOM-element kan skrivas om så här:

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

Som du kan se använder vi ingen kod för att instansiera vyer i komponentklassen. Mycket praktiskt!

ngComponentOutletLänk till det här avsnittet

Detta direktiv är analogt med ngTemplateOutlet med skillnaden att det skapar en värdvy (instansierar en komponent) och inte en inbäddad vy. Du kan använda det så här:

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

Wrapping upLink till det här avsnittet

Jag inser att all denna information kan vara mycket att smälta. Men det är faktiskt ganska sammanhängande och lägger ut en tydlig mental modell för att manipulera DOM via vyer. Du får referenser till Angular DOM-abstraktioner genom att använda en ViewChild-fråga tillsammans med mallvariabelreferenser. Den enklaste omslaget runt ett DOM-element är ElementRef. För mallar har du TemplateRef som låter dig skapa en inbäddad vy. Värdvyer kan nås på componentRef som skapats med hjälp av ComponentFactoryResolver. Vyerna kan manipuleras med ViewContainerRef. Det finns två direktiv som gör den manuella processen automatisk: ngTemplateOutlet – för inbäddade vyer och ngComponentOutlet för värdvyer (dynamiska komponenter).

Diskutera med samhället

Leave a Reply