Zkoumání technik manipulace s DOM v Angularu pomocí ViewContainerRef

Kdykoli čtu o práci s DOM v Angularu, vždy narazím na zmínku o jedné nebo několika z těchto tříd: ElementRef, TemplateRef, ViewContainerRef a další. Bohužel, i když se některé z nich v dokumentech Angularu nebo v souvisejících článcích vyskytují, zatím jsem nenašel popis celkového mentálního modelu a příklady jejich vzájemné spolupráce. Tento článek si klade za cíl takový model popsat.

Pokud hledáte podrobnější informace o manipulaci s DOM v Angularu pomocí Rendereru a View Containers, podívejte se na mou přednášku na NgVikings. Nebo si přečtěte podrobný článek o dynamické manipulaci s DOM Práce s DOM v Angularu: nečekané důsledky a optimalizační techniky

Pokud pocházíte ze světa angular.js, víte, že manipulace s DOM byla poměrně snadná. Angular injektoval DOM element do funkce link a vy jste se mohli dotazovat na libovolný uzel v rámci šablony komponenty, přidávat nebo odebírat podřízené uzly, upravovat styly atd. Tento přístup měl však jeden zásadní nedostatek – byl úzce vázán na platformu prohlížeče.

Nová verze Angularu běží na různých platformách – v prohlížeči, na mobilní platformě nebo uvnitř webového workeru. Je tedy nutná úroveň abstrakce, která stojí mezi rozhraním API specifickým pro danou platformu a rozhraním frameworku. V Angularu mají tyto abstrakce podobu následujících referenčních typů: ElementRef, TemplateRef, ViewRef, ComponentRef a ViewContainerRef. V tomto článku se na jednotlivé referenční typy podíváme podrobně a ukážeme si, jak je lze použít k manipulaci s DOM.

@ViewChildOdkaz na tuto sekci

Předtím, než se budeme zabývat abstrakcemi DOM, pochopíme, jak k těmto abstrakcím přistupujeme uvnitř třídy komponenty/směrovky. Angular poskytuje mechanismus, který se nazývá dotazy DOM. Je k dispozici ve formě dekorátorů @ViewChild a @ViewChildren. Chovají se stejně, pouze první z nich vrací jeden odkaz, zatímco druhý vrací více odkazů jako objekt QueryList. V příkladech v tomto článku budu používat převážně dekorátor ViewChild a nebudu používat symbol @ před ním.

Obvykle se tyto dekorátory párují s referenčními proměnnými šablony. Referenční proměnná šablony je jednoduše pojmenovaný odkaz na prvek DOM v rámci šablony. Můžete se na ni dívat jako na něco podobného jako na atribut id elementu html. Element DOM označíte odkazem na šablonu a pak se na něj uvnitř třídy dotazujete pomocí dekorátoru ViewChild. Zde je základní příklad:

<>Kopírovat
@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); }}

Základní syntaxe dekorátoru ViewChild je následující:

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

V tomto příkladu vidíte, že jsem zadal tref jako referenční jméno šablony v html a obdržel ElementRef spojený s tímto prvkem. Druhý parametr read není vždy nutný, protože Angular může odvodit typ reference podle typu elementu DOM. Pokud se například jedná o jednoduchý html prvek jako span, vrátí Angular ElementRef. Pokud se jedná o prvek template, vrátí TemplateRef. Některé odkazy, jako například ViewContainerRef, nelze odvodit a je třeba se na ně výslovně zeptat v parametru read. Jiné, jako ViewRef, nelze vrátit z DOM a je třeba je zkonstruovat ručně.

Tak, když už víme, jak se na reference ptát, začněme je zkoumat.

ElementRefOdkaz na tuto sekci

Jedná se o nejzákladnější abstrakci. Pokud budete sledovat strukturu její třídy, zjistíte, že obsahuje pouze nativní prvek, se kterým je spojena. Je užitečná pro přístup k nativnímu prvku DOM, jak můžeme vidět zde:

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

Tým Angularu však takové použití nedoporučuje. Nejenže představuje bezpečnostní riziko, ale také těsně spojuje vrstvy aplikace a vykreslování, což ztěžuje provoz aplikace na více platformách. Domnívám se, že abstrakci nenarušuje přístup k nativeElement, ale spíše použití specifického rozhraní DOM API, jako je textContent. Jak ale uvidíte později, mentální model manipulace s DOM implementovaný v systému Angular takový přístup nižší úrovně téměř nikdy nevyžaduje.

ElementRef lze vrátit pro libovolný prvek DOM pomocí dekorátoru ViewChild. Protože jsou však všechny komponenty umístěny uvnitř vlastního prvku DOM a všechny direktivy jsou aplikovány na prvky DOM, mohou třídy komponent a direktiv získat instanci ElementRef spojenou s jejich hostitelským prvkem prostřednictvím Dependency Injection (DI):

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

Takže zatímco komponenta může získat přístup ke svému hostitelskému prvku prostřednictvím DI, dekorátor ViewChild se nejčastěji používá k získání odkazu na prvek DOM ve svém zobrazení (šabloně). U direktiv je to ale obráceně – nemají žádné pohledy a obvykle pracují přímo s prvkem, ke kterému jsou připojeny.

TemplateRefOdkaz na tuto sekci

Pojmem šablona by se měla seznámit většina webových vývojářů. Jedná se o skupinu prvků DOM, které se opakovaně používají v zobrazeních napříč celou aplikací. Než standard HTML5 zavedl značku šablony, většina šablon přicházela do prohlížeče zabalená ve značce script s nějakou variantou atributu type:

<>Kopírovat
<script type="text/template"> <span>I am span in template</span></script>

Tento přístup měl jistě mnoho nevýhod, například sémantiku a nutnost ručně vytvářet modely DOM. Pomocí značky template prohlížeč analyzuje html a vytvoří strom DOM, ale nezobrazí jej. K němu pak lze přistupovat prostřednictvím vlastnosti content:

<>Kopírovat
<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 tento přístup přijímá a implementuje třídu TemplateRef pro práci se šablonou. Zde je uvedeno, jak ji lze použít:

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

Rámec odstraní prvek template z DOM a na jeho místo vloží komentář. Takto to vypadá po vykreslení:

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

Sama o sobě je třída TemplateRef jednoduchou třídou. Uchovává odkaz na svůj hostitelský prvek ve vlastnosti elementRef a má jednu metodu: createEmbeddedView. Tato metoda je však velmi užitečná, protože nám umožňuje vytvořit pohled a vrátit na něj odkaz jako ViewRef.

ViewRefOdkaz na tuto sekci

Tento typ abstrakce představuje pohled Angular View. Ve světě Angular je View základním stavebním prvkem uživatelského rozhraní aplikace. Je to nejmenší seskupení prvků, které se vytvářejí a ničí společně. Filozofie jazyka Angular nabádá vývojáře, aby uživatelské rozhraní vnímali jako kompozici pohledů, nikoli jako strom samostatných značek HTML.

Angular podporuje dva typy pohledů:

  • Vložené pohledy, které jsou propojeny se šablonou
  • Hostitelské pohledy, které jsou propojeny s komponentou

Vytvoření vloženého pohleduOdkaz na tuto část

Šablona jednoduše obsahuje plán pohledu. Pohled lze ze šablony instancovat pomocí výše zmíněné metody createEmbeddedView takto:

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

Vytvoření hostitelského pohleduOdkaz na tuto sekci

Hostitelské pohledy se vytvářejí při dynamické instanci komponenty. Komponentu lze dynamicky vytvořit pomocí ComponentFactoryResolver:

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

V systému Angular je každá komponenta vázána na konkrétní instanci injektoru, takže při vytváření komponenty předáváme aktuální instanci injektoru. Nezapomeňte také, že dynamicky instancované komponenty musí být přidány do EntryComponents modulu nebo hostitelské komponenty.

Viděli jsme tedy, jak lze vytvářet vložené i hostitelské pohledy. Jakmile je pohled vytvořen, lze jej vložit do DOM pomocí ViewContainer. V další části prozkoumáme jeho funkčnost.

ViewContainerRefOdkaz na tuto část

ViewContainerRef představuje kontejner, do kterého lze připojit jeden nebo více pohledů.

V první řadě je třeba zmínit, že jako kontejner pohledu lze použít jakýkoli prvek DOM. Zajímavé je, že Angular nevkládá pohledy dovnitř elementu, ale připojuje je za element svázaný s ViewContainer. Je to podobné, jako když router-outlet vkládá komponenty.

Obvykle je dobrým kandidátem na označení místa, kde má být vytvořen ViewContainer, element ng-container. Vykresluje se jako komentář, a tak nezavádí do DOM nadbytečné prvky HTML. Zde je příklad vytvoření ViewContainer na konkrétním místě v šabloně komponenty:

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

Stejně jako u ostatních abstrakcí DOM je ViewContainer vázán na konkrétní prvek DOM, ke kterému se přistupuje prostřednictvím vlastnosti element. Ve výše uvedeném příkladu je vázána na prvek ng-container vykreslený jako komentář, takže výstupem je template bindings={}.

Manipulace s pohledyOdkaz na tuto část

ViewContainer poskytuje pohodlné rozhraní API pro manipulaci s pohledy:

<>Kopírovat
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}

Dříve jsme viděli, jak lze ručně vytvořit dva typy pohledů ze šablony a komponenty. Jakmile máme pohled vytvořen, můžeme jej vložit do DOM pomocí metody insert. Zde je příklad vytvoření vloženého pohledu ze šablony a jeho vložení na konkrétní místo označené prvkem 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); }}

Při této implementaci vypadá výsledný html takto:

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

Pro odstranění pohledu z DOM můžeme použít metodu detach. Všechny ostatní metody jsou samovysvětlující a lze je použít k získání odkazu na pohled podle indexu, k přesunutí pohledu na jiné místo nebo k odstranění všech pohledů z kontejneru.

Vytvoření pohledůOdkaz na tuto sekci

ViewContainer také poskytuje API pro automatické vytvoření pohledu:

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

Jsou to prostě pohodlné obaly k tomu, co jsme výše provedli ručně. Vytvoří pohled ze šablony nebo komponenty a vloží jej na určené místo.

ngTemplateOutlet a ngComponentOutletOdkaz na tuto část

Ačkoli je vždy dobré vědět, jak fungují základní mechanismy, obvykle je žádoucí mít nějakou zkratku. Tato zkratka má podobu dvou direktiv: ngTemplateOutlet a ngComponentOutlet. V době psaní tohoto článku jsou obě experimentální a ngComponentOutlet bude k dispozici od verze 4. Pokud jste si ale přečetli vše výše, bude velmi snadné pochopit, co dělají.

ngTemplateOutletOdkaz na tuto sekci

Tato označuje prvek DOM jako ViewContainer a vkládá vložený pohled vytvořený šablonou, aniž by to bylo nutné explicitně provádět ve třídě komponenty. To znamená, že výše uvedený příklad, kdy jsme vytvořili pohled a vložili jej do prvku DOM #vc, lze přepsat takto:

<>Kopírovat
@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 {}

Jak vidíte, nepoužíváme žádný kód pro instanci pohledu ve třídě komponenty. Velmi praktické!

ngComponentOutletOdkaz na tuto sekci

Tato direktiva je analogická direktivě ngTemplateOutlet s tím rozdílem, že vytváří hostitelský pohled (instancuje komponentu), a nikoli vložený pohled. Můžete ji použít takto:

<>Kopírování
<ng-container *ngComponentOutlet="ColorComponent"></ng-container>

ZabaleníOdkaz do této sekce

Uvědomuji si, že všechny tyto informace mohou být příliš těžko stravitelné. Ale ve skutečnosti je to docela ucelené a stanovuje to jasný mentální model pro manipulaci s DOM prostřednictvím pohledů. Odkazy na abstrakce DOM systému Angular získáte pomocí dotazu ViewChild spolu s odkazy na proměnné šablony. Nejjednodušší obal kolem prvku DOM je ElementRef. Pro šablony máte k dispozici TemplateRef, který umožňuje vytvořit vložený pohled. K hostitelským pohledům lze přistupovat na componentRef vytvořeném pomocí ComponentFactoryResolver. S pohledy lze manipulovat pomocí ViewContainerRef. Existují dvě směrnice, díky nimž je ruční proces automatický: ngTemplateOutlet – pro vložené pohledy a ngComponentOutlet pro hostitelské pohledy (dynamické komponenty).

Diskuse s komunitou

Leave a Reply