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:
<>KopierenngAfterViewInit() { 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
:
<>Kopierenconstructor(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:
<>Kopierenclass 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:
<>Kopierenclass 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