Exploring Angular DOM manipulation techniques using ViewContainerRef

Wenn ich über die Arbeit mit DOM in Angular lese, sehe ich immer eine oder mehrere dieser Klassen erwähnt: ElementRef, TemplateRef, ViewContainerRef und andere. Leider, obwohl einige von ihnen in Angular docs oder verwandte Artikel abgedeckt sind, habe ich noch nicht die Beschreibung des gesamten mentalen Modells und Beispiele gefunden, wie diese zusammenarbeiten. Dieser Artikel zielt darauf ab, ein solches Modell zu beschreiben.

Wenn Sie nach tiefer gehenden Informationen über DOM-Manipulation in Angular mit Renderer und View Containern suchen, lesen Sie meinen Vortrag bei NgVikings. Oder lesen Sie einen ausführlichen Artikel über dynamische DOM-Manipulation Arbeiten mit DOM in Angular: unerwartete Konsequenzen und Optimierungstechniken

Wenn Sie aus der angular.js Welt kommen, wissen Sie, dass es ziemlich einfach war, das DOM zu manipulieren. Angular hat das DOM element in die link Funktion injiziert und man konnte jeden Knoten im Template der Komponente abfragen, Kindknoten hinzufügen oder entfernen, Stile ändern usw. Dieser Ansatz hatte jedoch ein großes Manko – er war eng an eine Browser-Plattform gebunden.

Die neue Angular-Version läuft auf verschiedenen Plattformen – in einem Browser, auf einer mobilen Plattform oder innerhalb eines Web Workers. Daher ist eine Abstraktionsebene erforderlich, die zwischen der plattformspezifischen API und den Schnittstellen des Frameworks steht. In Angular kommen diese Abstraktionen in Form der folgenden Referenztypen vor: ElementRef, TemplateRef, ViewRef, ComponentRef und ViewContainerRef. In diesem Artikel werden wir einen Blick auf jeden Referenztyp im Detail werfen und zeigen, wie sie verwendet werden können, um das DOM zu manipulieren.

@ViewChildLink zu diesem Abschnitt

Bevor wir die DOM-Abstraktionen erforschen, wollen wir verstehen, wie wir auf diese Abstraktionen innerhalb einer Komponente/Richtungsklasse zugreifen. Angular bietet einen Mechanismus namens DOM-Queries. Es gibt ihn in Form von @ViewChild und @ViewChildren Dekoratoren. Sie verhalten sich gleich, nur gibt ersterer einen Verweis zurück, während letzterer mehrere Verweise als QueryList-Objekt zurückgibt. In den Beispielen in diesem Artikel werde ich hauptsächlich den ViewChild-Dekorator verwenden und nicht das @-Symbol davor.

In der Regel werden diese Dekoratoren mit Template-Referenzvariablen gepaart. Eine Template-Referenzvariable ist einfach ein benannter Verweis auf ein DOM-Element innerhalb eines Templates. Man kann sie als etwas Ähnliches wie das id-Attribut eines html-Elements betrachten. Sie markieren ein DOM-Element mit einer Template-Referenz und fragen es dann innerhalb einer Klasse mit dem ViewChild-Dekorator ab. Hier ist das grundlegende Beispiel:

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

Die grundlegende Syntax des ViewChild-Dekorators ist:

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

In diesem Beispiel können Sie sehen, dass ich tref als Template-Referenzname im html angegeben habe und das mit diesem Element verbundene ElementRef erhalte. Der zweite Parameter read ist nicht immer erforderlich, da Angular den Referenztyp anhand des Typs des DOM-Elements ableiten kann. Wenn es sich zum Beispiel um ein einfaches HTML-Element wie span handelt, gibt Angular ElementRef. zurück. Wenn es sich um ein template-Element handelt, gibt es TemplateRef zurück. Einige Referenzen, wie ViewContainerRef, können nicht abgeleitet werden und müssen speziell im read Parameter abgefragt werden. Andere, wie ViewRef, können nicht aus dem DOM zurückgegeben werden und müssen manuell konstruiert werden.

Okay, jetzt, wo wir wissen, wie man die Referenzen abfragt, können wir anfangen, sie zu erforschen.

ElementRefLink zu diesem Abschnitt

Dies ist die einfachste Abstraktion. Wenn Sie die Klassenstruktur beobachten, werden Sie sehen, dass sie nur das native Element enthält, mit dem sie verknüpft ist. Es ist nützlich für den Zugriff auf native DOM-Elemente, wie wir hier sehen können:

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

Doch von einer solchen Verwendung wird vom Angular-Team abgeraten. Sie stellt nicht nur ein Sicherheitsrisiko dar, sondern koppelt auch die Anwendungs- und Rendering-Schichten eng aneinander, was es schwierig macht, eine Anwendung auf mehreren Plattformen zu betreiben. Ich glaube, dass es nicht der Zugriff auf nativeElement ist, der die Abstraktion bricht, sondern eher die Verwendung einer spezifischen DOM-API wie textContent. Aber wie Sie später sehen werden, erfordert das in Angular implementierte mentale Modell der DOM-Manipulation fast nie einen solchen Zugriff auf niedrigerer Ebene.

ElementRef kann für jedes DOM-Element mit dem ViewChild-Dekorator zurückgegeben werden. Da jedoch alle Komponenten innerhalb eines benutzerdefinierten DOM-Elements gehostet werden und alle Direktiven auf DOM-Elemente angewendet werden, können Komponenten- und Direktivenklassen eine Instanz von ElementRef erhalten, die mit ihrem Host-Element durch Dependency Injection (DI) verbunden ist:

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

Während also eine Komponente durch DI Zugriff auf ihr Host-Element erhalten kann, wird der ViewChild-Dekorator am häufigsten verwendet, um einen Verweis auf ein DOM-Element in seinem View (Template) zu erhalten. Aber bei Direktiven ist es umgekehrt – sie haben keine Views und arbeiten normalerweise direkt mit dem Element, an das sie angehängt sind.

TemplateRefLink zu diesem Abschnitt

Der Begriff Template sollte den meisten Webentwicklern bekannt sein. Es handelt sich dabei um eine Gruppe von DOM-Elementen, die in Ansichten in der gesamten Anwendung wiederverwendet werden. Bevor der HTML5-Standard das Template-Tag einführte, wurden die meisten Templates in einem script-Tag mit einer Variation des type-Attributs an den Browser übergeben:

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

Dieser Ansatz hatte sicherlich viele Nachteile wie die Semantik und die Notwendigkeit, DOM-Modelle manuell zu erstellen. Mit dem template-Tag parst ein Browser html und erstellt einen DOM-Baum, rendert ihn aber nicht. Auf diesen kann dann über die content-Eigenschaft zugegriffen werden:

<>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 macht sich diesen Ansatz zu eigen und implementiert die TemplateRef-Klasse für die Arbeit mit einer Vorlage. Hier ist, wie es verwendet werden kann:

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

Das Framework entfernt das template-Element aus dem DOM und fügt einen Kommentar an seiner Stelle ein. So sieht es aus, wenn es gerendert wird:

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

An sich ist die Klasse TemplateRef eine einfache Klasse. Sie hält einen Verweis auf ihr Host-Element in der Eigenschaft elementRef und hat eine Methode: createEmbeddedView. Diese Methode ist jedoch sehr nützlich, da sie es uns ermöglicht, einen View zu erstellen und eine Referenz darauf als ViewRef zurückzugeben.

ViewRefLink zu diesem Abschnitt

Diese Art der Abstraktion repräsentiert einen Angular View. In der Angular-Welt ist eine View ein fundamentaler Baustein der Anwendungs-UI. Sie ist die kleinste Gruppierung von Elementen, die gemeinsam erstellt und zerstört werden. Die Angular-Philosophie ermutigt Entwickler, die UI als eine Komposition von Views zu sehen, nicht als einen Baum von einzelnen HTML-Tags.

Angular unterstützt zwei Arten von Views:

  • Eingebettete Views, die mit einem Template verknüpft sind
  • Host-Views, die mit einer Komponente verknüpft sind

Erstellen einer eingebetteten ViewLink zu diesem Abschnitt

Ein Template enthält einfach eine Blaupause für eine View. Eine Ansicht kann von der Vorlage mit der oben erwähnten createEmbeddedView Methode wie folgt instanziiert werden:

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

Erstellen einer Host-AnsichtLink zu diesem Abschnitt

Host-Ansichten werden erstellt, wenn eine Komponente dynamisch instanziiert wird. Eine Komponente kann dynamisch mit ComponentFactoryResolver:

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

In Angular ist jede Komponente an eine bestimmte Instanz eines Injektors gebunden, also übergeben wir die aktuelle Injektorinstanz beim Erstellen der Komponente. Vergessen Sie auch nicht, dass Komponenten, die dynamisch instanziiert werden, zu den EntryComponents eines Moduls oder einer Host-Komponente hinzugefügt werden müssen.

So, wir haben gesehen, wie sowohl eingebettete als auch Host-Views erstellt werden können. Sobald eine Ansicht erstellt ist, kann sie mit ViewContainer in das DOM eingefügt werden. Der nächste Abschnitt beschäftigt sich mit seiner Funktionalität.

ViewContainerRefLink zu diesem Abschnitt

ViewContainerRef stellt einen Container dar, an den eine oder mehrere Views angehängt werden können.

Das erste, was hier erwähnt werden sollte, ist, dass jedes DOM-Element als View-Container verwendet werden kann. Interessant ist, dass Angular die Views nicht innerhalb des Elements einfügt, sondern sie hinter dem an ViewContainer gebundenen Element anhängt. Das ist ähnlich, wie router-outlet Komponenten einfügt.

Ein guter Kandidat, um eine Stelle zu markieren, an der ein ViewContainer erstellt werden soll, ist normalerweise das ng-container-Element. Es wird als Kommentar gerendert und führt somit keine überflüssigen HTML-Elemente in das DOM ein. Hier ein Beispiel für die Erstellung eines ViewContainer an einer bestimmten Stelle in einer Komponentenvorlage:

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

Genauso wie bei anderen DOM-Abstraktionen ist ViewContainer an ein bestimmtes DOM-Element gebunden, auf das über die Eigenschaft element zugegriffen wird. Im obigen Beispiel ist es an das ng-container-Element gebunden, das als Kommentar gerendert wird, und so lautet die Ausgabe template bindings={}.

Manipulation von AnsichtenLink zu diesem Abschnitt

ViewContainer bietet eine bequeme API zur Manipulation von Ansichten:

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

Wir haben bereits gesehen, wie zwei Arten von Ansichten manuell aus einer Vorlage und einer Komponente erstellt werden können. Sobald wir eine Ansicht haben, können wir sie mit der Methode insert in ein DOM einfügen. Hier ein Beispiel für die Erstellung einer eingebetteten Ansicht aus einer Vorlage und das Einfügen an einer bestimmten Stelle, die durch ein ng-container-Element gekennzeichnet ist:

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

Mit dieser Implementierung sieht das resultierende html wie folgt aus:

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

Um eine Ansicht aus dem DOM zu entfernen, können wir die Methode detach verwenden. Alle anderen Methoden sind selbsterklärend und können verwendet werden, um einen Verweis auf eine Ansicht durch den Index zu erhalten, die Ansicht an einen anderen Ort zu verschieben oder alle Ansichten aus dem Container zu entfernen.

Erstellen von AnsichtenLink zu diesem Abschnitt

ViewContainer bietet auch eine API, um eine Ansicht automatisch zu erstellen:

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

Dies sind einfach bequeme Wrapper zu dem, was wir oben manuell gemacht haben. Sie erstellen eine Ansicht aus einer Vorlage oder Komponente und fügen sie an der angegebenen Stelle ein.

ngTemplateOutlet und ngComponentOutletLink zu diesem Abschnitt

Es ist zwar immer gut zu wissen, wie die zugrundeliegenden Mechanismen funktionieren, aber in der Regel ist es wünschenswert, eine Art von Abkürzung zu haben. Diese Abkürzung kommt in Form von zwei Direktiven: ngTemplateOutlet und ngComponentOutlet. Zum Zeitpunkt dieses Artikels sind beide experimentell und ngComponentOutlet wird ab Version 4 verfügbar sein. Aber wenn Sie alles oben Gelesene gelesen haben, wird es sehr einfach zu verstehen sein, was sie tun.

ngTemplateOutletLink zu diesem Abschnitt

Dieser markiert ein DOM-Element als ViewContainer und fügt eine eingebettete Ansicht ein, die von einer Vorlage erstellt wurde, ohne dass dies explizit in einer Komponentenklasse geschehen muss. Das bedeutet, dass das obige Beispiel, in dem wir eine Ansicht erstellt und in ein #vc DOM-Element eingefügt haben, wie folgt umgeschrieben werden kann:

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

Wie Sie sehen, verwenden wir keinen Code zur Instanziierung einer Ansicht in der Komponentenklasse. Sehr praktisch!

ngComponentOutletLink zu diesem Abschnitt

Diese Direktive ist analog zu ngTemplateOutlet mit dem Unterschied, dass sie eine Host-View (instanziiert eine Komponente) erstellt und keine eingebettete View. Sie können sie wie folgt verwenden:

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

AbschlussLink zu diesem Abschnitt

Ich weiß, dass all diese Informationen vielleicht sehr schwer zu verdauen sind. Aber eigentlich ist es ziemlich kohärent und legt ein klares mentales Modell für die Manipulation des DOM über Ansichten dar. Sie erhalten Verweise auf Angular-DOM-Abstraktionen, indem Sie eine ViewChild-Abfrage zusammen mit Template-Variablen-Referenzen verwenden. Der einfachste Wrapper um ein DOM-Element ist ElementRef. Für Templates gibt es TemplateRef, mit dem Sie eine eingebettete Ansicht erstellen können. Auf Host-Views kann über componentRef zugegriffen werden, die mit ComponentFactoryResolver erstellt werden. Die Ansichten können mit ViewContainerRef manipuliert werden. Es gibt zwei Direktiven, die den manuellen Prozess automatisieren: ngTemplateOutlet – für eingebettete Ansichten und ngComponentOutlet für Host-Ansichten (dynamische Komponenten).

Diskutieren Sie mit der Community

Leave a Reply