Az Angular DOM manipulációs technikák felfedezése a ViewContainerRef használatával

Amikor a DOM-mal való munkáról olvasok az Angularban, mindig látok egy vagy néhány ilyen osztályt említve: ElementRef, TemplateRef, ViewContainerRef és mások. Sajnos, bár néhányukkal foglalkoznak az Angular dokukban vagy a kapcsolódó cikkekben, még nem találtam az általános mentális modell leírását és példákat arra, hogy ezek hogyan működnek együtt. Ez a cikk egy ilyen modell leírását tűzte ki célul.

Ha mélyebb információkat keresel a DOM manipulációról Angularban a Renderer és View Containers használatával kapcsolatban, nézd meg az előadásomat az NgVikings-en. Vagy olvass el egy mélyreható cikket a dinamikus DOM-manipulációról DOM-mal való munka Angularban: váratlan következmények és optimalizálási technikák

Ha a angular.js világból származol, akkor tudod, hogy elég könnyű volt manipulálni a DOM-ot. Az Angular element befecskendezte a DOM link függvénybe, és lekérdezhette bármelyik csomópontot a komponens sablonján belül, hozzáadhatott vagy eltávolíthatott gyermek csomópontokat, módosíthatta a stílusokat stb. Ennek a megközelítésnek azonban volt egy nagy hiányossága – szorosan egy böngészőplatformhoz volt kötve.

Az új Angular verzió különböző platformokon fut – böngészőben, mobilplatformon vagy egy webmunkáson belül. Szükség van tehát egy absztrakciós szintre, amely a platformspecifikus API és a keretrendszer interfészei között áll. Az angularban ezek az absztrakciók a következő hivatkozási típusok formájában jelennek meg: ElementRef, TemplateRef, ViewRef, ComponentRef és ViewContainerRef. Ebben a cikkben részletesen megnézzük az egyes referenciatípusokat, és megmutatjuk, hogyan használhatók a DOM manipulálására.

@ViewChildLink erre a szakaszra

Mielőtt felfedeznénk a DOM absztrakciókat, értsük meg, hogyan érjük el ezeket az absztrakciókat egy komponens/irányító osztályon belül. Az Angular biztosít egy DOM-lekérdezések nevű mechanizmust. Ez @ViewChild és @ViewChildren dekorátorok formájában jelenik meg. Ugyanúgy viselkednek, csak az előbbi egy hivatkozást ad vissza, míg az utóbbi több hivatkozást ad vissza QueryList objektumként. A cikk példáiban többnyire a ViewChild dekorátort fogom használni, és nem fogom használni az előtte lévő @ szimbólumot.

Ezeket a dekorátorokat általában sablonhivatkozási változókkal párosítjuk. A sablonreferencia-változó egyszerűen egy sablonon belüli DOM-elemre való névre szóló hivatkozás. Tekinthetjük úgy, mint valami hasonlót egy html elem id attribútumához. Egy DOM-elemet sablonhivatkozással jelölünk, majd egy osztályon belül lekérdezzük azt a ViewChild dekorátor segítségével. Íme az alapvető példa:

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

A ViewChild dekorátor alapvető szintaxisa a következő:

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

A példában látható, hogy a html-ben tref mint sablonhivatkozás nevét adtam meg, és megkapom az ehhez az elemhez tartozó ElementRef-et. A második paraméter read nem mindig szükséges, mivel az Angular a DOM-elem típusából következtetni tud a referencia típusára. Ha például egy egyszerű html-elemről van szó, például span, az Angular ElementRef.-t ad vissza, ha egy template elemről van szó, akkor TemplateRef-t ad vissza. Néhány hivatkozást, mint például a ViewContainerRef nem lehet kikövetkeztetni, azokat külön kell kérni a read paraméterben. Másokat, mint például a ViewRef nem lehet visszaadni a DOM-ból, és kézzel kell konstruálni.

Oké, most, hogy már tudjuk, hogyan kell lekérdezni a hivatkozásokat, kezdjük el felfedezni őket.

ElementRefLink erre a szakaszra

Ez a legalapvetőbb absztrakció. Ha megfigyeljük az osztályszerkezetét, láthatjuk, hogy csak azt a natív elemet tartalmazza, amelyhez kapcsolódik. Hasznos a natív DOM elem eléréséhez, ahogy itt láthatjuk:

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

Az ilyen használatról azonban az Angular csapat lebeszél. Nemcsak biztonsági kockázatot jelent, hanem szorosan összekapcsolja az alkalmazás és a renderelési rétegeket, ami megnehezíti az alkalmazás több platformon való futtatását. Úgy vélem, hogy nem a nativeElement-hoz való hozzáférés az, ami megtöri az absztrakciót, hanem inkább egy specifikus DOM API használata, mint a textContent. De ahogy később látni fogod, az Angularban megvalósított DOM manipulációs mentális modell aligha igényel ilyen alacsonyabb szintű hozzáférést.

ElementRef bármely DOM elemhez visszaadható a ViewChild dekorátor segítségével. Mivel azonban minden komponens egy saját DOM-elemen belül van elhelyezve, és minden direktívát DOM-elemekre alkalmazunk, a komponens- és direktívaosztályok a függőségi injektálás (Dependency Injection, DI) segítségével megkaphatják a ElementRef egy példányát, amely a gazdaelemhez kapcsolódik:

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

Míg tehát egy komponens DI-n keresztül hozzáférhet a gazdaeleméhez, a ViewChild dekorátort leggyakrabban a nézetben (sablonban) lévő DOM-elemre való hivatkozás megszerzésére használják. A direktívák esetében azonban ez fordítva van – nekik nincsenek nézeteik, és általában közvetlenül azzal az elemmel dolgoznak, amelyhez csatolják őket.

TemplateRefLink erre a szakaszra

A sablon fogalma a legtöbb webfejlesztő számára ismerős lehet. Olyan DOM-elemek csoportjáról van szó, amelyek az alkalmazás nézeteiben újrafelhasználásra kerülnek. Mielőtt a HTML5-szabvány bevezette volna a sabloncímkét, a legtöbb sablon egy script címkébe csomagolva érkezett a böngészőhöz, a type attribútum valamilyen variációjával:

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

Ez a megközelítés minden bizonnyal számos hátránnyal járt, például a szemantika és a DOM-modellek kézi létrehozásának szükségessége miatt. A template taggel a böngésző elemzi a html-t és létrehoz egy DOM fát, de nem rendereli azt. Ezután a content tulajdonságon keresztül érhető el:

<>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 felkarolja ezt a megközelítést és megvalósítja a TemplateRef osztályt, hogy egy sablonnal dolgozzon. Az alábbi módon használható:

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

A keretrendszer eltávolítja a template elemet a DOM-ból, és egy megjegyzést illeszt a helyére. Így néz ki renderelve:

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

Az TemplateRef osztály önmagában egy egyszerű osztály. A elementRef tulajdonságban tart egy hivatkozást a gazdaelemére, és egy metódusa van: createEmbeddedView. Ez a metódus azonban nagyon hasznos, mivel lehetővé teszi számunkra, hogy létrehozzunk egy nézetet, és ViewRefként visszaadjunk egy hivatkozást rá.

ViewRefLink erre a szakaszra

Ez az absztrakciótípus egy Angular nézetet reprezentál. Az Angular világában a View az alkalmazás felhasználói felületének alapvető építőköve. Az elemek legkisebb csoportosítása, amelyek együtt jönnek létre és semmisülnek meg. Az Angular filozófiája arra ösztönzi a fejlesztőket, hogy a felhasználói felületet nézetek kompozíciójaként tekintsék, ne pedig önálló HTML-tagek fájaként.

Az Angular kétféle nézetet támogat:

  • Beágyazott nézetek, amelyek egy sablonhoz kapcsolódnak
  • Gazdanézetek, amelyek egy komponenshez kapcsolódnak

Beágyazott nézet létrehozásaLink erre a szakaszra

A sablon egyszerűen egy nézet tervrajzát tartalmazza. A nézetet a sablonból a fent említett createEmbeddedView módszerrel lehet instanciázni a következőképpen:

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

Host nézet létrehozásaLink erre a szakaszra

A host nézetek egy komponens dinamikus instanciázásakor jönnek létre. Egy komponens dinamikusan létrehozható a ComponentFactoryResolver:

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

Az Angularban minden komponens egy adott injektor példányhoz van kötve, ezért a komponens létrehozásakor átadjuk az aktuális injektor példányt. Ne felejtsük el azt sem, hogy a dinamikusan instanciált komponenseket hozzá kell adni a modul vagy a befogadó komponens EntryComponents-éhez.

Azt láttuk tehát, hogyan hozható létre mind a beágyazott, mind a befogadó nézet. Miután egy nézetet létrehoztunk, a ViewContainer segítségével beilleszthető a DOM-ba. A következő szakasz ennek működését tárja fel.

ViewContainerRefLink erre a szakaszra

AViewContainerRef egy olyan konténert képvisel, amelyhez egy vagy több nézet csatolható.

Az első dolog, amit itt meg kell említeni, hogy bármely DOM elem használható nézetkonténerként. Ami érdekes, hogy az Angular nem az elem belsejébe illeszti be a nézeteket, hanem a ViewContainer-hez kötött elem után csatolja őket. Ez hasonló ahhoz, ahogyan a router-outlet beilleszti a komponenseket.

Általában a ng-container elem jó jelölt arra, hogy kijelölje azt a helyet, ahol egy ViewContainer-nek létre kell jönnie. Ez megjegyzésként jelenik meg, és így nem vezet be felesleges HTML-elemeket a DOM-ba. Íme a példa egy ViewContainer létrehozására egy adott helyen egy komponenssablonban:

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

A többi DOM-absztrakcióhoz hasonlóan a ViewContainer is egy adott DOM-elemhez van kötve, amelyet a element tulajdonságon keresztül érhetünk el. A fenti példában a megjegyzésként megjelenített ng-container elemhez van kötve, így a kimenet template bindings={}.

Nézetek manipulálásaLink erre a szakaszra

ViewContainer egy kényelmes API-t biztosít a nézetek manipulálásához:

<>Másolás
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}

Már korábban láttuk, hogyan lehet kétféle nézetet manuálisan létrehozni egy sablonból és egy komponensből. Ha már van egy nézetünk, akkor azt a insert módszerrel beilleszthetjük a DOM-ba. Íme egy példa arra, hogyan hozhatunk létre egy beágyazott nézetet egy sablonból, és hogyan illeszthetjük be egy ng-container elemmel jelölt meghatározott helyre:

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

Ezzel a megvalósítással a kapott html így néz ki:

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

A nézet eltávolításához a DOM-ból a detach módszert használhatjuk. Az összes többi metódus önmagyarázó, és arra használható, hogy az index alapján hivatkozást kapjunk egy nézetre, áthelyezzük a nézetet egy másik helyre, vagy eltávolítsuk az összes nézetet a konténerből.

Nézet létrehozásaLink erre a szakaszra

ViewContainer A nézet automatikus létrehozásához is biztosít egy API-t:

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

Ezek egyszerűen kényelmes csomagolások ahhoz, amit fentebb kézzel végeztünk. Létrehoznak egy nézetet egy sablonból vagy komponensből, és beillesztik a megadott helyre.

ngTemplateOutlet és ngComponentOutletLink erre a szakaszra

Míg mindig jó tudni, hogyan működnek a mögöttes mechanizmusok, általában kívánatos, hogy legyen valamilyen rövidítés. Ez a rövidítés két direktíva formájában jelenik meg: ngTemplateOutlet és ngComponentOutlet. E sorok írásakor mindkettő kísérleti jellegű, a ngComponentOutlet a 4. verziótól lesz elérhető. De ha mindent elolvastál fentebb, akkor nagyon könnyű lesz megérteni, hogy mit csinálnak.

ngTemplateOutletLink erre a szakaszra

Ez egy DOM elemet jelöl ViewContainer-nek, és beilleszt egy sablon által létrehozott beágyazott nézetet anélkül, hogy ezt kifejezetten meg kellene tenned egy komponensosztályban. Ez azt jelenti, hogy a fenti példa, ahol létrehoztunk egy nézetet és beillesztettük egy #vc DOM elembe, átírható így:

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

Amint látható, nem használunk semmilyen nézetet példányosító kódot a komponensosztályban. Nagyon praktikus!

ngComponentOutletLink erre a szakaszra

Ez a direktíva analóg a ngTemplateOutlet-zal, azzal a különbséggel, hogy ez egy gazdanézetet hoz létre (instanciál egy komponenst), és nem egy beágyazott nézetet. Így használhatja:

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

Wrapping upLink to this section

Tudom, hogy mindezt az információt sok lehet megemészteni. De valójában elég koherens, és egy világos mentális modellt határoz meg a DOM nézeteken keresztül történő manipulálásához. Az Angular DOM absztrakcióira való hivatkozásokat egy ViewChild lekérdezéssel kapja meg a sablonváltozói hivatkozásokkal együtt. A legegyszerűbb burkolat egy DOM elem körül a ElementRef. A sablonokhoz a TemplateRef áll rendelkezésre, amely lehetővé teszi egy beágyazott nézet létrehozását. A ComponentFactoryResolver segítségével létrehozott componentRef-nézeteket a ComponentFactoryResolver segítségével lehet elérni. A nézetek a ViewContainerRef segítségével manipulálhatók. Két direktíva van, amely a manuális folyamatot automatikussá teszi: ngTemplateOutlet – a beágyazott nézetekhez és ngComponentOutlet a gazdanézetekhez (dinamikus komponensek).

Diszkusszió a közösséggel

Leave a Reply