Exploring Angular DOM manipulatie technieken met behulp van ViewContainerRef

Wanneer ik ook maar iets lees over het werken met DOM in Angular zie ik altijd wel een of enkele van deze classes genoemd worden: ElementRef, TemplateRef, ViewContainerRef en anderen. Helaas, hoewel sommige van hen worden behandeld in Angular docs of aanverwante artikelen, heb ik nog niet gevonden de beschrijving van het algemene mentale model en voorbeelden van hoe deze samenwerken. Dit artikel is bedoeld om een dergelijk model te beschrijven.

Als je op zoek bent naar meer diepgaande informatie over DOM manipulatie in Angular met behulp van Renderer en View Containers bekijk dan mijn talk op NgVikings. Of lees een diepgaand artikel over dynamische DOM manipulatie Werken met DOM in Angular: onverwachte gevolgen en optimalisatietechnieken

Als je uit de angular.js wereld komt, weet je dat het vrij eenvoudig was om de DOM te manipuleren. Angular injecteerde DOM element in link-functie en je kon elke node binnen de template van een component opvragen, child nodes toevoegen of verwijderen, stijlen wijzigen, enzovoort. Deze aanpak had echter één grote tekortkoming – het was strak gebonden aan een browserplatform.

De nieuwe Angular-versie draait op verschillende platforms – in een browser, op een mobiel platform of binnen een webwerker. Dus een niveau van abstractie is nodig om tussen platform-specifieke API en het kader interfaces staan. In Angular komen deze abstracties in de vorm van de volgende referentie types: ElementRef, TemplateRef, ViewRef, ComponentRef en ViewContainerRef. In dit artikel bekijken we elk referentietype in detail en laten we zien hoe ze kunnen worden gebruikt om DOM te manipuleren.

@ViewChildLink naar deze sectie

Voordat we de DOM-abstracties verkennen, laten we eerst begrijpen hoe we toegang krijgen tot deze abstracties binnen een component/directive-klasse. Angular biedt een mechanisme genaamd DOM queries. Het komt in de vorm van @ViewChild en @ViewChildren decorators. Ze gedragen zich hetzelfde, alleen de eerste retourneert één referentie, terwijl de laatste meerdere referenties retourneert als een QueryList object. In de voorbeelden in dit artikel zal ik vooral de ViewChild decorator gebruiken en niet het @ symbool ervoor.

Over het algemeen worden deze decorators gekoppeld aan template referentie variabelen. Een sjabloonreferentievariabele is eenvoudigweg een benoemde verwijzing naar een DOM-element binnen een sjabloon. U kunt het zien als iets dat vergelijkbaar is met het id-attribuut van een html-element. U markeert een DOM-element met een sjabloonverwijzing en query’t het vervolgens binnen een klasse met behulp van de ViewChild-decorator. Hier is het basisvoorbeeld:

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

De basissyntaxis van de ViewChild-decorator is:

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

In dit voorbeeld kunt u zien dat ik tref heb opgegeven als sjabloonreferentienaam in de html en de ElementRef ontvang die bij dit element hoort. De tweede parameter read is niet altijd nodig, omdat Angular het verwijzingstype kan afleiden uit het type van het DOM-element. Als het bijvoorbeeld een eenvoudig html-element is zoals span, geeft Angular ElementRef. terug Als het een template-element is, geeft het TemplateRef terug. Sommige referenties, zoals ViewContainerRef kunnen niet worden afgeleid en moeten specifiek worden opgevraagd in de read parameter. Andere, zoals ViewRef kunnen niet uit het DOM worden geretourneerd en moeten handmatig worden geconstrueerd.

Okee, nu we weten hoe we de referenties kunnen opvragen, laten we ze gaan onderzoeken.

ElementRefLink naar deze sectie

Dit is de meest elementaire abstractie. Als je naar de klassenstructuur kijkt, zie je dat het alleen het native element bevat waarmee het geassocieerd is. Het is nuttig voor toegang tot native DOM-elementen, zoals we hier kunnen zien:

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

Het Angular-team ontmoedigt echter een dergelijk gebruik. Niet alleen levert het een veiligheidsrisico op, maar het koppelt ook je applicatie en rendering lagen aan elkaar, wat het moeilijk maakt om een app op meerdere platformen te draaien. Ik geloof dat het niet de toegang tot nativeElement is die de abstractie breekt, maar eerder het gebruik van een specifieke DOM API zoals textContent. Maar zoals u later zult zien, vereist het DOM-manipulatie-mentale model dat in Angular is geïmplementeerd vrijwel nooit dergelijke toegang op een lager niveau.

ElementRef kan voor elk DOM-element worden geretourneerd met behulp van de ViewChild-decorator. Maar aangezien alle componenten worden gehost binnen een aangepast DOM-element en alle richtlijnen worden toegepast op DOM-elementen, kunnen component- en directieklassen een instantie van ElementRef verkrijgen die is gekoppeld aan hun host-element via Dependency Injection (DI):

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

Dus terwijl een component via DI toegang kan krijgen tot zijn host-element, wordt de ViewChild-decorator het vaakst gebruikt om een verwijzing naar een DOM-element in zijn view (sjabloon) te krijgen. Maar, het is omgekeerd voor directives – ze hebben geen views en ze werken meestal direct met het element waaraan ze zijn gekoppeld.

TemplateRefLink naar deze sectie

Het begrip template zou voor de meeste webontwikkelaars bekend moeten zijn. Het is een groep DOM-elementen die worden hergebruikt in weergaven in de applicatie. Voordat de HTML5-standaard de sjabloontag introduceerde, kwamen de meeste sjablonen bij de browser aan in een script-tag met een variatie van het type-attribuut:

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

Deze aanpak had zeker veel nadelen, zoals de semantiek en de noodzaak om handmatig DOM-modellen te maken. Met de template tag parseert een browser html en maakt een DOM boom aan, maar rendert deze niet. Deze kan vervolgens worden geopend via de content eigenschap:

<>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 omarmt deze aanpak en implementeert de TemplateRef klasse om met een sjabloon te werken. Dit is hoe het kan worden gebruikt:

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

Het framework verwijdert het template-element uit het DOM en voegt er een commentaar voor in de plaats in. Dit is hoe het eruit ziet wanneer het wordt weergegeven:

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

Zelf is de TemplateRef-klasse een eenvoudige klasse. Zij houdt een verwijzing naar haar host-element in de elementRef eigenschap en heeft één methode: createEmbeddedView. Deze methode is echter zeer nuttig, omdat het ons in staat stelt een view te maken en een verwijzing ernaar terug te geven als ViewRef.

ViewRefLink naar deze sectie

Dit type abstractie vertegenwoordigt een Angular View. In de Angular wereld is een View een fundamentele bouwsteen van de applicatie UI. Het is de kleinste groepering van elementen die samen worden gecreëerd en vernietigd. De Angular-filosofie moedigt ontwikkelaars aan om de UI te zien als een samenstelling van Views, niet als een boom van losstaande HTML-tags.

Angular ondersteunt twee soorten views:

  • Embedded Views die zijn gekoppeld aan een Template
  • Host Views die zijn gekoppeld aan een Component

Een embedded view makenLink naar deze sectie

Een template bevat eenvoudigweg een blauwdruk voor een view. Een view kan worden geïnstantieerd vanuit het sjabloon met behulp van de eerder genoemde createEmbeddedView-methode, zoals deze:

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

Een host view makenLink naar deze sectie

Host views worden gemaakt wanneer een component dynamisch wordt geïnstantieerd. Een component kan dynamisch worden gemaakt met 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 is elke component gebonden aan een bepaalde instantie van een injector, dus geven we de huidige instantie van de injector door bij het maken van de component. Vergeet ook niet dat componenten die dynamisch worden geïnstantieerd, moeten worden toegevoegd aan de EntryComponents van een module of hostingcomponent.

Zo, we hebben gezien hoe zowel embedded als hostweergaven kunnen worden gemaakt. Zodra een view is gemaakt, kan deze in de DOM worden ingevoegd met ViewContainer. De volgende sectie onderzoekt de functionaliteit ervan.

ViewContainerRefLink naar deze sectie

ViewContainerRef vertegenwoordigt een container waaraan een of meer views kunnen worden gekoppeld.

Het eerste wat hier moet worden vermeld is dat elk DOM-element kan worden gebruikt als een view container. Wat interessant is, is dat Angular geen views in het element plaatst, maar ze toevoegt na het element dat is gebonden aan ViewContainer. Dit is vergelijkbaar met hoe de router-outlet componenten invoegt.

Een goede kandidaat om een plaats te markeren waar een ViewContainer moet worden gemaakt, is het ng-container element. Het wordt weergegeven als een commentaar en introduceert dus geen overbodige HTML elementen in het DOM. Hier volgt een voorbeeld van het maken van een ViewContainer op een specifieke plaats in een componentsjabloon:

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

Net als bij andere DOM-abstracties is ViewContainer gebonden aan een bepaald DOM-element dat wordt benaderd via de element-eigenschap. In het voorbeeld hierboven is het gebonden aan het ng-container element dat als commentaar wordt weergegeven, en dus is de uitvoer template bindings={}.

Beeldingen manipulerenLink naar deze sectie

ViewContainer biedt een handige API voor het manipuleren van weergaven:

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

We hebben eerder gezien hoe twee soorten weergaven handmatig kunnen worden gemaakt van een sjabloon en een component. Zodra we een view hebben, kunnen we deze invoegen in een DOM met behulp van de insert methode. Hier is het voorbeeld van het maken van een embedded view vanuit een template en het invoegen op een specifieke plaats, gemarkeerd door een 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); }}

Met deze implementatie ziet de resulterende html er als volgt uit:

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

Om een view uit het DOM te verwijderen, kunnen we de detach methode gebruiken. Alle andere methoden spreken voor zich en kunnen worden gebruikt om een verwijzing naar een view via de index op te vragen, de view naar een andere locatie te verplaatsen, of alle views uit de container te verwijderen.

Creating ViewsLink naar deze sectie

ViewContainer biedt ook een API om automatisch een view te creëren:

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

Dit zijn gewoon handige wrappers voor wat we hierboven handmatig hebben gedaan. Ze maken een view van een template of component en voegen deze in op de gespecificeerde locatie.

ngTemplateOutlet en ngComponentOutletLink naar deze sectie

Hoewel het altijd goed is om te weten hoe de onderliggende mechanismen werken, is het meestal wenselijk om een soort van snelkoppeling te hebben. Deze snelkoppeling komt in de vorm van twee directieven: ngTemplateOutlet en ngComponentOutlet. Op het moment van schrijven zijn beide nog experimenteel en ngComponentOutlet zal beschikbaar zijn vanaf versie 4. Maar als je alles hierboven hebt gelezen, is het heel gemakkelijk te begrijpen wat ze doen.

ngTemplateOutletLink naar deze sectie

Deze markeert een DOM element als een ViewContainer en voegt een embedded view gemaakt door een template in zonder de noodzaak om dit expliciet te doen in een component class. Dit betekent dat het bovenstaande voorbeeld, waarbij we een view hebben gemaakt en deze in een #vc DOM element hebben ingevoegd, als volgt kan worden herschreven:

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

Zoals u kunt zien gebruiken we geen view instantiating code in de component class. Erg handig!

ngComponentOutletLink naar deze sectie

Deze directive is analoog aan ngTemplateOutlet met het verschil dat het een host view maakt (een component instantieert), en geen embedded view. U kunt het als volgt gebruiken:

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

Wrapping upLink to this section

Ik realiseer me dat al deze informatie misschien veel is om te verteren. Maar eigenlijk is het vrij samenhangend en geeft het een duidelijk mentaal model voor het manipuleren van het DOM via views. Je krijgt verwijzingen naar Angular DOM abstracties door gebruik te maken van een ViewChild query samen met template variabele verwijzingen. De eenvoudigste wrapper rond een DOM element is ElementRef. Voor templates heb je TemplateRef waarmee je een embedded view kunt maken. Host views kunnen worden benaderd op componentRef gemaakt met ComponentFactoryResolver. De views kunnen worden gemanipuleerd met ViewContainerRef. Er zijn twee directives die het handmatige proces automatisch maken: ngTemplateOutlet – voor embedded views en ngComponentOutlet voor host views (dynamische componenten).

Discussieer met de gemeenschap

Leave a Reply