Exploring Angularin DOM-käsittelytekniikoiden tutkiminen ViewContainerRefin avulla

Kun luen Angularin DOM:n kanssa työskentelystä Angularissa, näen aina, että yksi tai muutama näistä luokista mainitaan: ElementRef, TemplateRef, ViewContainerRef ja muita. Valitettavasti, vaikka joitakin niistä käsitellään Angularin dokumenteissa tai niihin liittyvissä artikkeleissa, en ole vielä löytänyt kuvausta yleisestä mentaalimallista ja esimerkkejä siitä, miten nämä toimivat yhdessä. Tämän artikkelin tarkoituksena on kuvata tällainen malli.

Jos etsit syvällisempää tietoa DOM:n manipuloinnista Angularissa käyttäen Rendereriä ja View Containersia, tutustu puheeseeni NgVikingsissä. Tai lue syventävä artikkeli dynaamisesta DOM-manipuloinnista Työskentely DOM:n kanssa Angularissa: odottamattomia seurauksia ja optimointitekniikoita

Jos tulet angular.js maailmasta, tiedät, että DOM:n manipulointi oli melko helppoa. Angular injektoi DOM element link-funktioon ja voit kysyä mitä tahansa solmua komponentin mallin sisällä, lisätä tai poistaa lapsisolmuja, muokata tyylejä jne. Tällä lähestymistavalla oli kuitenkin yksi suuri puute – se oli tiukasti sidottu selainalustaan.

Uusi Angular-versio toimii eri alustoilla – selaimessa, mobiilialustalla tai web workerin sisällä. Tarvitaan siis abstraktiotaso alustakohtaisen API:n ja kehyksen rajapintojen väliin. Angularissa nämä abstraktiot tulevat seuraavien viitetyyppien muodossa: ElementRef, TemplateRef, ViewRef, ComponentRef ja ViewContainerRef. Tässä artikkelissa tarkastelemme kutakin viittaustyyppiä yksityiskohtaisesti ja näytämme, miten niitä voidaan käyttää DOM:n manipulointiin.

@ViewChildLinkki tähän osioon

Ennen kuin tutkimme DOM-abstraktioita, ymmärretään, miten pääsemme käsiksi näihin abstraktioihin komponentti-/ohjausluokan sisällä. Angular tarjoaa mekanismin nimeltä DOM-kyselyt. Se tulee @ViewChild– ja @ViewChildren-dekoraattoreiden muodossa. Ne käyttäytyvät samalla tavalla, mutta ensimmäinen palauttaa vain yhden viittauksen, kun taas jälkimmäinen palauttaa useita viittauksia QueryList-objektina. Tämän artikkelin esimerkeissä käytän enimmäkseen ViewChild-dekoraattoria enkä käytä sitä edeltävää @-symbolia.

Yleensä nämä dekoraattorit on yhdistetty malliviitemuuttujiin. Malliviitemuuttuja on yksinkertaisesti nimetty viittaus DOM-elementtiin mallin sisällä. Sitä voi pitää samanlaisena kuin html-elementin id-attribuuttia. DOM-elementti merkitään malliviittauksella ja sitten sitä kysytään luokan sisällä ViewChild-koristeen avulla. Tässä on perusesimerkki:

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

Dekoraattorin ViewChild perussyntaksi on seuraava:

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

Tässä esimerkissä näet, että määrittelin tref mallin viitteen nimeksi html ja saan tähän elementtiin liittyvän ElementRef. Toista parametria read ei aina tarvita, sillä Angular voi päätellä viitetyypin DOM-elementin tyypin perusteella. Jos kyseessä on esimerkiksi yksinkertainen html-elementti kuten span, Angular palauttaa ElementRef. Jos kyseessä on template-elementti, se palauttaa TemplateRef. Joitakin viittauksia, kuten ViewContainerRef, ei voida päätellä, vaan niitä on kysyttävä erikseen read-parametrilla. Toisia, kuten ViewRef, ei voida palauttaa DOM:sta, vaan ne on muodostettava manuaalisesti.

Okei, nyt kun tiedämme, miten viitteitä kysytään, aletaan tutkia niitä.

ElementtiViittausLinkki tähän osioon

Tämä on yksinkertaisin abstraktio. Jos tarkkailet sen luokkarakennetta, huomaat, että se pitää sisällään vain sen natiivielementin, johon se liittyy. Se on hyödyllinen natiivin DOM-elementin käyttämiseen, kuten näemme tässä:

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

Angular-tiimi kuitenkin varoittaa tällaisesta käytöstä. Se ei ainoastaan aiheuta tietoturvariskiä, vaan myös kytkee sovelluksen ja renderointikerroksen tiukasti yhteen, mikä vaikeuttaa sovelluksen käyttämistä useilla alustoilla. Uskon, että nativeElement:n käyttö ei ole se, joka rikkoo abstraktion, vaan pikemminkin tietyn DOM API:n, kuten textContent:n, käyttö. Mutta kuten myöhemmin nähdään, Angularissa toteutettu DOM-manipulaation mentaalimalli tuskin koskaan tarvitsee tällaista alemman tason pääsyä.

ElementRef voidaan palauttaa mille tahansa DOM-elementille käyttämällä ViewChild-dekoraattoria. Mutta koska kaikki komponentit sijaitsevat mukautetun DOM-elementin sisällä ja kaikkia direktiivejä sovelletaan DOM-elementteihin, komponentti- ja direktiiviluokat voivat saada isäntäelementtiinsä liittyvän ElementRef:n instanssin DI:n (Dependency Injection) avulla:

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

Niinpä vaikka komponentti voi saada pääsyn isäntäelementtiinsä DI:n kautta, ViewChild-dekoraattoria käytetään useimmiten saamaan viittaus DOM-elementtiin näkymässään (template). Mutta direktiivien kohdalla asia on päinvastoin – niillä ei ole näkymiä ja ne toimivat yleensä suoraan sen elementin kanssa, johon ne on liitetty.

TemplateRefLinkki tähän jaksoon

Mallin käsitteen pitäisi olla tuttu useimmille web-kehittäjille. Se on ryhmä DOM-elementtejä, joita käytetään uudelleen näkymissä eri puolilla sovellusta. Ennen kuin HTML5-standardi otti käyttöön template-tunnisteen, useimmat mallit saapuivat selaimeen käärittynä script-tunnisteeseen, jossa oli jokin muunnelma type-attribuutista:

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

Tällä lähestymistavalla oli toki monia haittapuolia, kuten semantiikka ja DOM-mallien manuaalisen luomisen pakollisuus. template-tagilla selain jäsentää html ja luo DOM-puun, mutta ei renderöi sitä. Sitä voidaan sitten käyttää content-ominaisuuden kautta:

<>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 omaksuu tämän lähestymistavan ja toteuttaa TemplateRef-luokan toimimaan mallin kanssa. Näin sitä voidaan käyttää:

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

Kehys poistaa template-elementin DOM:sta ja lisää sen tilalle kommentin. Näin se näyttää renderöitynä:

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

Senänsä TemplateRef-luokka on yksinkertainen luokka. Se pitää elementRef-ominaisuudessa viittausta isäntäelementtiinsä ja sillä on yksi metodi: createEmbeddedView. Tämä metodi on kuitenkin erittäin hyödyllinen, sillä sen avulla voimme luoda näkymän ja palauttaa siihen viittauksen ViewRef.

ViewRefLinkki tähän osioon

Tämä abstraktiotyyppi edustaa Angular-näkymää. Angular-maailmassa View on sovelluksen käyttöliittymän perustavanlaatuinen rakennuspalikka. Se on pienin elementtien ryhmittymä, joka luodaan ja tuhotaan yhdessä. Angularin filosofia kannustaa kehittäjiä näkemään käyttöliittymän näkymien koostumuksena, ei itsenäisten HTML-tagien puuna.

Angular tukee kahdenlaisia näkymiä:

  • Sulautetut näkymät, jotka on linkitetty malliin
  • Isännänäkymät, jotka on linkitetty komponenttiin

Sisäänrakennetun näkymän luominenLinkki tähän osioon

Malli pitää sisällään yksinkertaisesti näkymän pohjapiirustuksen. Näkymä voidaan instansioida mallista käyttämällä edellä mainittua createEmbeddedView-metodia näin:

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

Isäntänäkymän luominenLinkki tähän osioon

Isäntänäkymät luodaan, kun komponentti instanioidaan dynaamisesti. Komponentti voidaan luoda dynaamisesti käyttämällä ComponentFactoryResolver:

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

Angularissa jokainen komponentti on sidottu tiettyyn injektorin instanssiin, joten välitämme nykyisen injektorin instanssin komponenttia luotaessa. Älä myöskään unohda, että dynaamisesti instansoitavat komponentit on lisättävä moduulin tai isäntäkomponentin EntryComponents-komponenttiin.

Olemme siis nähneet, miten sekä upotettuja että isäntäkomponentteja voidaan luoda. Kun näkymä on luotu, se voidaan lisätä DOM:iin käyttämällä ViewContainer. Seuraavassa osiossa tutkitaan sen toiminnallisuutta.

ViewContainerRefLinkki tähän osioon

ViewContainerRef edustaa säiliötä, johon voidaan liittää yksi tai useampi näkymä.

Ensin on mainittava, että mitä tahansa DOM-elementtiä voidaan käyttää näkymän säiliönä. Mielenkiintoista on, että Angular ei aseta näkymiä elementin sisään, vaan liittää ne ViewContainer sidotun elementin jälkeen. Tämä on samanlaista kuin se, miten router-outlet lisää komponentteja.

Yleensä hyvä ehdokas merkitsemään paikka, johon ViewContainer tulisi luoda, on ng-container-elementti. Se renderöidään kommenttina, joten se ei tuo turhia HTML-elementtejä DOM:iin. Tässä on esimerkki ViewContainer luomisesta tiettyyn paikkaan komponenttimallissa:

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

Aivan kuten muutkin DOM-abstraktiot, ViewContainer on sidottu tiettyyn DOM-elementtiin, johon pääsee käsiksi element-ominaisuuden kautta. Yllä olevassa esimerkissä se on sidottu kommenttina renderöityyn ng-container-elementtiin, joten tuloste on template bindings={}.

Näkymien manipulointiLinkki tähän osioon

ViewContainer tarjoaa kätevän API:n näkymien manipulointiin:

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

Edellisessä kappaleessa näimme aiemmin, kuinka mallineesta ja komponenteista voidaan manuaalisesti luoda kahta erilaista näkymää. Kun meillä on näkymä, voimme lisätä sen DOM:iin käyttämällä insert-metodia. Tässä on esimerkki upotetun näkymän luomisesta mallista ja sen lisäämisestä tiettyyn paikkaan, joka on merkitty ng-container-elementillä:

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

Tällä toteutuksella tuloksena oleva html näyttää tältä:

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

Poistaaksemme näkymän DOM:sta voimme käyttää detach-metodia. Kaikki muut metodit ovat itsestään selviä, ja niitä voidaan käyttää hakemaan viittaus näkymään indeksin perusteella, siirtämään näkymä toiseen paikkaan tai poistamaan kaikki näkymät säiliöstä.

Näkymien luominenLinkki tähän osioon

ViewContainer tarjoaa myös API:n näkymien automaattiseen luomiseen:

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

Näissä on kyse vain kätevistä kääreistä sille, mitä teimme manuaalisesti edellä. Ne luovat näkymän mallista tai komponentista ja lisäävät sen määritettyyn paikkaan.

ngTemplateOutlet ja ngComponentOutletLinkki tähän osioon

Vaikka on aina hyvä tietää, miten taustalla olevat mekanismit toimivat, on yleensä toivottavaa, että on jonkinlainen oikotie. Tämä oikotie tulee kahden direktiivin muodossa: ngTemplateOutlet ja ngComponentOutlet. Tätä kirjoitettaessa molemmat ovat kokeellisia, ja ngComponentOutlet on käytettävissä versiosta 4 alkaen. Mutta jos olet lukenut kaiken yllä olevan, on hyvin helppo ymmärtää, mitä ne tekevät.

ngTemplateOutletLinkki tähän osioon

Tällä merkitään DOM-elementti ViewContainer:ksi ja lisätään mallin luoma sulautettu näkymä ilman, että tätä tarvitsee tehdä eksplisiittisesti komponenttiluokassa. Tämä tarkoittaa, että yllä oleva esimerkki, jossa loimme näkymän ja lisäsimme sen #vc DOM-elementtiin, voidaan kirjoittaa uudelleen näin:

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

Kuten huomaat, emme käytä komponenttiluokassa mitään näkymän instantiointikoodia. Erittäin kätevää!

ngComponentOutletLinkki tähän osioon

Tämä direktiivi on analoginen ngTemplateOutlet:n kanssa sillä erotuksella, että se luo isäntänäkymän (instantioi komponentin) eikä upotettua näkymää. Voit käyttää sitä näin:

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

PakkausLinkki tähän osioon

Tiedän, että kaikki tämä tieto saattaa olla paljon sulateltavaa. Mutta itse asiassa se on melko johdonmukainen ja esittää selkeän henkisen mallin DOM:n manipuloimiseksi näkymien avulla. Viittaukset Angularin DOM-abstraktioihin saadaan käyttämällä ViewChild-kyselyä yhdessä mallimuuttujien viittausten kanssa. Yksinkertaisin DOM-elementin ympärillä oleva kääre on ElementRef. Malleja varten sinulla on TemplateRef, jonka avulla voit luoda upotetun näkymän. Isäntänäkymiä voidaan käyttää componentRef:lla, joka on luotu ComponentFactoryResolver:n avulla. Näkymiä voidaan käsitellä ViewContainerRef:llä. On kaksi direktiiviä, jotka tekevät manuaalisesta prosessista automaattisen: ngTemplateOutlet – sulautetuille näkymille ja ngComponentOutlet isäntänäkymille (dynaamiset komponentit).

Keskustele yhteisön kanssa.

Leave a Reply