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 ViewRef
ké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:
<>CopyngAfterViewInit() { 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
:
<>Copyconstructor(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ásclass 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:
<>Copyclass 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