Exploring Angular DOM manipulation techniques using ViewContainerRef

Kiedykolwiek czytam o pracy z DOM w Angular, zawsze widzę jedną lub kilka z tych klas wymienionych: ElementRef, TemplateRef, ViewContainerRef i inne. Niestety, mimo że niektóre z nich są uwzględnione w Angularowych docsach lub powiązanych artykułach, nie znalazłem jeszcze opisu ogólnego modelu mentalnego i przykładów jak te klasy ze sobą współpracują. Ten artykuł ma na celu opisanie takiego modelu.

Jeśli szukasz bardziej dogłębnych informacji na temat manipulacji DOM w Angularze przy użyciu Renderera i View Containerów sprawdź mój wykład na NgVikings. Lub przeczytaj dogłębny artykuł na temat dynamicznej manipulacji DOM Praca z DOM w Angular: nieoczekiwane konsekwencje i techniki optymalizacji

Jeśli pochodzisz ze świata angular.js, wiesz, że manipulowanie DOM było dość łatwe. Angular wstrzykiwał DOM element do link funkcji i można było odpytywać dowolny węzeł w obrębie szablonu komponentu, dodawać lub usuwać węzły dzieci, modyfikować style itp. Jednak to podejście miało jedną poważną wadę – było ściśle związane z platformą przeglądarkową.

Nowa wersja Angulara działa na różnych platformach – w przeglądarce, na platformie mobilnej lub wewnątrz web worker. Tak więc wymagany jest poziom abstrakcji, aby stanąć pomiędzy API specyficznym dla danej platformy a interfejsami frameworka. W angular te abstrakcje występują w postaci następujących typów referencyjnych: ElementRef, TemplateRef, ViewRef, ComponentRef i ViewContainerRef. W tym artykule przyjrzymy się szczegółowo każdemu typowi referencyjnemu i pokażemy, jak można je wykorzystać do manipulowania DOM.

@ViewChildLink do tej sekcji

Zanim zbadamy abstrakcje DOM, zrozumiemy, w jaki sposób uzyskujemy dostęp do tych abstrakcji wewnątrz klasy komponentu/dyrektywy. Angular udostępnia mechanizm zwany zapytaniami DOM. Występuje on w postaci dekoratorów @ViewChild oraz @ViewChildren. Zachowują się one tak samo, tylko pierwszy z nich zwraca jedną referencję, natomiast drugi zwraca wiele referencji jako obiekt QueryList. W przykładach w tym artykule będę używał głównie dekoratora ViewChild i nie będę używał symbolu @ przed nim.

Zazwyczaj dekoratory te są sparowane z szablonowymi zmiennymi referencyjnymi. Zmienna referencyjna szablonu jest po prostu nazwanym odwołaniem do elementu DOM wewnątrz szablonu. Można ją postrzegać jako coś podobnego do atrybutu id elementu html. Oznaczasz element DOM referencją do szablonu, a następnie odpytywałeś go wewnątrz klasy używając dekoratora ViewChild. Oto podstawowy przykład:

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

Podstawowa składnia dekoratora ViewChild to:

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

W tym przykładzie widać, że w parametrze html podałem tref jako nazwę referencyjną szablonu i otrzymuję ElementRef związany z tym elementem. Drugi parametr read nie zawsze jest wymagany, ponieważ Angular może wywnioskować typ referencji na podstawie typu elementu DOM. Na przykład, jeśli jest to prosty element html, taki jak span, Angular zwraca ElementRef. Jeśli jest to element template, zwraca TemplateRef. Niektóre referencje, takie jak ViewContainerRef nie mogą być wywnioskowane i muszą być zapytane konkretnie w parametrze read. Inne, jak ViewRef, nie mogą być zwrócone z DOM i muszą być skonstruowane ręcznie.

Dobrze, teraz, gdy wiemy, jak pytać o referencje, zacznijmy je badać.

ElementRefLink do tej sekcji

Jest to najbardziej podstawowa abstrakcja. Jeśli obserwujesz strukturę jej klasy, zobaczysz, że przechowuje ona tylko rodzimy element, z którym jest powiązana. Jest to przydatne przy dostępie do natywnego elementu DOM, jak możemy zobaczyć tutaj:

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

Jednakże takie użycie jest odradzane przez zespół Angulara. Nie tylko stanowi to zagrożenie bezpieczeństwa, ale także ściśle łączy twoją aplikację i warstwy renderowania, co utrudnia uruchamianie aplikacji na wielu platformach. Uważam, że to nie dostęp do nativeElement łamie abstrakcję, ale raczej użycie specyficznego API DOM jak textContent. Ale jak zobaczysz później, model mentalny manipulacji DOM zaimplementowany w Angular prawie nigdy nie wymaga takiego dostępu niższego poziomu.

ElementRef może być zwrócony dla dowolnego elementu DOM przy użyciu dekoratora ViewChild. Ale ponieważ wszystkie komponenty są hostowane wewnątrz niestandardowego elementu DOM, a wszystkie dyrektywy są stosowane do elementów DOM, klasy komponentów i dyrektyw mogą uzyskać instancję ElementRef związaną z ich elementem hosta poprzez Dependency Injection (DI):

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

Więc podczas gdy komponent może uzyskać dostęp do swojego elementu hosta poprzez DI, dekorator ViewChild jest najczęściej używany do uzyskania referencji do elementu DOM w swoim widoku (szablonie). Jednak w przypadku dyrektyw jest odwrotnie – nie mają one widoków i zazwyczaj działają bezpośrednio z elementem, do którego są dołączone.

TemplateRefLink do tej sekcji

Pojęcie szablonu powinno być znane większości programistów stron internetowych. Jest to grupa elementów DOM, które są ponownie wykorzystywane w widokach w całej aplikacji. Zanim standard HTML5 wprowadził znacznik szablonu, większość szablonów docierała do przeglądarki zawinięta w znacznik script z jakąś odmianą atrybutu type:

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

Takie podejście z pewnością miało wiele wad, takich jak semantyka i konieczność ręcznego tworzenia modeli DOM. Dzięki znacznikowi template przeglądarka parsuje html i tworzy drzewo DOM, ale nie renderuje go. Następnie można uzyskać do niego dostęp poprzez właściwość content:

<>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 przyjmuje to podejście i implementuje klasę TemplateRef do pracy z szablonem. Oto jak można jej użyć:

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

Framework usuwa element template z DOM i wstawia w jego miejsce komentarz. Tak to wygląda po wyrenderowaniu:

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

Sama w sobie klasa TemplateRef jest prostą klasą. Przechowuje ona referencję do swojego elementu nadrzędnego we właściwości elementRef i posiada jedną metodę: createEmbeddedView. Metoda ta jest jednak bardzo użyteczna, ponieważ pozwala nam stworzyć widok i zwrócić do niego referencję jako ViewRef.

ViewRefLink do tej sekcji

Ten typ abstrakcji reprezentuje widok Angular View. W świecie Angulara widok jest podstawowym elementem składowym UI aplikacji. Jest to najmniejsze zgrupowanie elementów, które są tworzone i niszczone razem. Filozofia Angular zachęca programistów do postrzegania UI jako kompozycji Widoków, a nie jako drzewa samodzielnych znaczników HTML.

Angular obsługuje dwa typy widoków:

  • Widoki osadzone, które są powiązane z Szablonem
  • Widoki hosta, które są powiązane z Komponentem

Tworzenie widoku osadzonegoLink do tej sekcji

Szablon po prostu przechowuje projekt widoku. Widok może zostać utworzony z szablonu za pomocą wspomnianej metody createEmbeddedView w następujący sposób:

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

Tworzenie widoku hostaLink do tej sekcji

Widoki hosta są tworzone, gdy komponent jest dynamicznie tworzony. Komponent może być utworzony dynamicznie za pomocą ComponentFactoryResolver:

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

W Angular, każdy komponent jest związany z konkretną instancją injectora, więc przekazujemy bieżącą instancję injectora podczas tworzenia komponentu. Nie zapominajmy również, że komponenty, które są tworzone dynamicznie muszą być dodane do EntryComponents modułu lub komponentu hostującego.

Więc, widzieliśmy jak można tworzyć zarówno widoki osadzone jak i hostujące. Po utworzeniu widoku można go wstawić do DOM za pomocą ViewContainer. Następna sekcja bada jego funkcjonalność.

ViewContainerRefLink do tej sekcji

ViewContainerRef reprezentuje kontener, do którego można dołączyć jeden lub więcej widoków.

Pierwszą rzeczą, o której należy tutaj wspomnieć jest to, że każdy element DOM może być użyty jako kontener widoku. Interesujące jest to, że Angular nie wstawia widoków wewnątrz elementu, ale dołącza je po elemencie związanym z ViewContainer. Jest to podobne do tego, w jaki sposób router-outlet wstawia komponenty.

Zwykle dobrym kandydatem do oznaczenia miejsca, w którym powinien zostać utworzony ViewContainer jest element ng-container. Jest on renderowany jako komentarz, a więc nie wprowadza do DOM zbędnych elementów HTML. Oto przykład tworzenia ViewContainer w określonym miejscu w szablonie komponentu:

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

Tak jak w przypadku innych abstrakcji DOM, ViewContainer jest związany z określonym elementem DOM dostępnym poprzez właściwość element. W powyższym przykładzie jest on związany z elementem ng-container wyrenderowanym jako komentarz, a więc wyjściem jest template bindings={}.

Manipulowanie widokamiLink do tej sekcji

ViewContainer zapewnia wygodne API do manipulowania widokami:

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

Wcześniej widzieliśmy, jak dwa typy widoków mogą być tworzone ręcznie z szablonu i komponentu. Gdy mamy już widok, możemy go wstawić do DOM za pomocą metody insert. Poniżej znajduje się przykład tworzenia widoku osadzonego z szablonu i wstawiania go w określone miejsce oznaczone elementem 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); }}

Przy takiej implementacji wynikowy html wygląda następująco:

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

Aby usunąć widok z DOM, możemy skorzystać z metody detach. Wszystkie inne metody nie wymagają wyjaśnień i mogą być użyte do uzyskania odniesienia do widoku przez indeks, przeniesienia widoku w inne miejsce lub usunięcia wszystkich widoków z kontenera.

Tworzenie widokówLink do tej sekcji

ViewContainer dostarcza również API do automatycznego tworzenia widoków:

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

Są to po prostu wygodne zawijasy do tego, co zrobiliśmy ręcznie powyżej. Tworzą one widok z szablonu lub komponentu i wstawiają go w określonym miejscu.

ngTemplateOutlet i ngComponentOutletLink do tej sekcji

Choć zawsze dobrze jest wiedzieć, jak działają podstawowe mechanizmy, zwykle pożądane jest posiadanie jakiegoś skrótu. Ten skrót ma postać dwóch dyrektyw: ngTemplateOutlet i ngComponentOutlet. W chwili pisania tego tekstu obie są eksperymentalne, a ngComponentOutlet będzie dostępna od wersji 4. Ale jeśli przeczytałeś wszystko powyżej, bardzo łatwo będzie zrozumieć, co one robią.

ngTemplateOutletLink do tej sekcji

Ta dyrektywa oznacza element DOM jako ViewContainer i wstawia osadzony widok utworzony przez szablon bez potrzeby robienia tego jawnie w klasie komponentu. Oznacza to, że powyższy przykład, w którym utworzyliśmy widok i wstawiliśmy go do elementu DOM #vc, można przepisać w następujący sposób:

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

Jak widać, nie używamy żadnego kodu inicjującego widok w klasie komponentu. Bardzo przydatne!

ngComponentOutletLink do tej sekcji

Ta dyrektywa jest analogiczna do ngTemplateOutlet z tą różnicą, że tworzy widok hosta (instantiates a component), a nie widok osadzony. Możesz jej użyć w następujący sposób:

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

ZawijanieLink do tej sekcji

Zdaję sobie sprawę, że wszystkie te informacje mogą być trudne do strawienia. Ale tak naprawdę jest to całkiem spójne i kładzie jasny model mentalny dla manipulowania DOM poprzez widoki. Odwołania do Angularowych abstrakcji DOM uzyskujemy poprzez użycie zapytania ViewChild wraz z odwołaniami do zmiennych szablonowych. Najprostszym opakowaniem wokół elementu DOM jest ElementRef. Dla szablonów masz TemplateRef, który pozwala na stworzenie osadzonego widoku. Widoki osadzone mogą być dostępne na componentRef utworzonych przy użyciu ComponentFactoryResolver. Widoki mogą być manipulowane za pomocą ViewContainerRef. Istnieją dwie dyrektywy, które sprawiają, że proces ręczny jest automatyczny: ngTemplateOutlet – dla widoków osadzonych i ngComponentOutlet dla widoków hosta (komponentów dynamicznych).

Dyskutuj ze społecznością

Leave a Reply