Explorer les techniques de manipulation du DOM en Angular à l’aide de ViewContainerRef

Chaque fois que je lis sur le travail avec le DOM en Angular, je vois toujours une ou plusieurs de ces classes mentionnées : ElementRef, TemplateRef, ViewContainerRef et d’autres. Malheureusement, bien que certaines d’entre elles soient couvertes dans les docs Angular ou des articles connexes, je n’ai pas encore trouvé la description du modèle mental global et des exemples de la façon dont ceux-ci fonctionnent ensemble. Cet article vise à décrire un tel modèle.

Si vous cherchez des informations plus approfondies sur la manipulation du DOM dans Angular en utilisant Renderer et View Containers, consultez ma conférence à NgVikings. Ou lisez un article approfondi sur la manipulation dynamique du DOM Travailler avec le DOM en Angular : conséquences inattendues et techniques d’optimisation

Si vous venez du monde angular.js, vous savez qu’il était assez facile de manipuler le DOM. Angular injectait le DOM element dans la fonction link et vous pouviez interroger n’importe quel nœud dans le modèle du composant, ajouter ou supprimer des nœuds enfants, modifier les styles, etc. Cependant, cette approche avait un défaut majeur – elle était étroitement liée à une plateforme de navigateur.

La nouvelle version d’Angular fonctionne sur différentes plateformes – dans un navigateur, sur une plateforme mobile ou à l’intérieur d’un web worker. Ainsi, un niveau d’abstraction est nécessaire pour se tenir entre l’API spécifique à la plate-forme et les interfaces du framework. Dans Angular, ces abstractions se présentent sous la forme des types de référence suivants : ElementRef, TemplateRef, ViewRef, ComponentRef et ViewContainerRef. Dans cet article, nous allons examiner chaque type de référence en détail et montrer comment ils peuvent être utilisés pour manipuler le DOM.

@ViewChildLink to this section

Avant d’explorer les abstractions DOM, comprenons comment nous accédons à ces abstractions à l’intérieur d’une classe de composant/directive. Angular fournit un mécanisme appelé requêtes DOM. Il se présente sous la forme de décorateurs @ViewChild et @ViewChildren. Ils se comportent de la même manière, seulement le premier renvoie une référence, tandis que le second renvoie plusieurs références sous la forme d’un objet QueryList. Dans les exemples de cet article, j’utiliserai principalement le décorateur ViewChild et je n’utiliserai pas le symbole @ qui le précède.

En général, ces décorateurs sont associés à des variables de référence de modèle. Une variable de référence de modèle est simplement une référence nommée à un élément DOM dans un modèle. Vous pouvez la considérer comme quelque chose de similaire à l’attribut id d’un élément html. Vous marquez un élément DOM avec une référence de modèle et l’interrogez ensuite à l’intérieur d’une classe en utilisant le décorateur ViewChild. Voici l’exemple de base:

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

La syntaxe de base du décorateur ViewChild est :

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

Dans cet exemple, vous pouvez voir que j’ai spécifié tref comme nom de référence du modèle dans le html et recevoir le ElementRef associé à cet élément. Le deuxième paramètre read n’est pas toujours nécessaire, car Angular peut déduire le type de référence par le type de l’élément DOM. Par exemple, s’il s’agit d’un élément html simple comme span, Angular renvoie ElementRef. Si c’est un élément template, il renvoie TemplateRef. Certaines références, comme ViewContainerRef ne peuvent pas être inférées et doivent être demandées spécifiquement dans le paramètre read. D’autres, comme ViewRef ne peuvent pas être retournées depuis le DOM et doivent être construites manuellement.

Ok, maintenant que nous savons comment interroger les références, commençons à les explorer.

ElementRefLien vers cette section

C’est l’abstraction la plus basique. Si vous observez sa structure de classe, vous verrez qu’elle ne détient que l’élément natif auquel elle est associée. C’est utile pour accéder à l’élément DOM natif comme nous pouvons le voir ici :

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

Cependant, une telle utilisation est découragée par l’équipe Angular. Non seulement cela pose un risque de sécurité, mais cela couple étroitement votre application et les couches de rendu, ce qui rend difficile l’exécution d’une application sur plusieurs plateformes. Je pense que ce n’est pas l’accès à nativeElement qui casse l’abstraction, mais plutôt l’utilisation d’une API DOM spécifique comme textContent. Mais comme vous le verrez plus tard, le modèle mental de manipulation du DOM mis en œuvre dans Angular ne nécessite pratiquement jamais un tel accès de niveau inférieur.

ElementRef peut être retourné pour tout élément du DOM en utilisant le décorateur ViewChild. Mais puisque tous les composants sont hébergés à l’intérieur d’un élément DOM personnalisé et que toutes les directives sont appliquées aux éléments DOM, les classes de composants et de directives peuvent obtenir une instance de ElementRef associée à leur élément hôte par le biais de l’injection de dépendance (DI) :

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

Alors qu’un composant peut obtenir un accès à son élément hôte par DI, le décorateur ViewChild est utilisé le plus souvent pour obtenir une référence à un élément DOM dans sa vue (template). Mais, c’est inversé pour les directives – elles n’ont pas de vues et elles travaillent généralement directement avec l’élément auquel elles sont attachées.

TemplateRefLien vers cette section

La notion de template devrait être familière pour la plupart des développeurs web. C’est un groupe d’éléments DOM qui sont réutilisés dans des vues à travers l’application. Avant que la norme HTML5 n’introduise la balise template, la plupart des modèles arrivaient au navigateur enveloppés dans une balise script avec une variation de l’attribut type:

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

Cette approche avait certainement de nombreux inconvénients comme la sémantique et la nécessité de créer manuellement des modèles DOM. Avec la balise template, un navigateur analyse html et crée un arbre DOM mais ne le rend pas. On peut alors y accéder par la propriété content:

<>Copie
<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 embrasse cette approche et implémente la classe TemplateRef pour travailler avec un modèle. Voici comment elle peut être utilisée:

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

Le framework supprime l’élément template du DOM et insère un commentaire à sa place. Voici à quoi cela ressemble une fois rendu:

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

En soi, la classe TemplateRef est une classe simple. Elle détient une référence à son élément hôte dans la propriété elementRef et possède une méthode : createEmbeddedView. Cependant, cette méthode est très utile car elle nous permet de créer une vue et de retourner une référence à celle-ci en tant que ViewRef.

ViewRefLien vers cette section

Ce type d’abstraction représente une vue Angular. Dans le monde Angular, une vue est une brique fondamentale de l’interface utilisateur de l’application. C’est le plus petit regroupement d’éléments qui sont créés et détruits ensemble. La philosophie Angular encourage les développeurs à voir l’interface utilisateur comme une composition de vues, et non comme un arbre de balises HTML autonomes.

Angular prend en charge deux types de vues :

  • Vues intégrées qui sont liées à un modèle
  • Vues hôtes qui sont liées à un composant

Création d’une vue intégréeLien vers cette section

Un modèle détient simplement un plan directeur pour une vue. Une vue peut être instanciée à partir du modèle en utilisant la méthode createEmbeddedView susmentionnée comme ceci :

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

Création d’une vue hôteLien vers cette section

Les vues hôtes sont créées lorsqu’un composant est instancié dynamiquement. Un composant peut être créé dynamiquement en utilisant ComponentFactoryResolver:

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

En Angular, chaque composant est lié à une instance particulière d’un injecteur, donc nous passons l’instance actuelle de l’injecteur lors de la création du composant. De plus, n’oubliez pas que les composants qui sont instanciés dynamiquement doivent être ajoutés aux EntryComponents d’un module ou d’un composant d’hébergement.

Donc, nous avons vu comment les vues embarquées et les vues d’hébergement peuvent être créées. Une fois qu’une vue est créée, elle peut être insérée dans le DOM en utilisant ViewContainer. La section suivante explore sa fonctionnalité.

ViewContainerRefLien vers cette section

ViewContainerRef représente un conteneur où une ou plusieurs vues peuvent être attachées.

La première chose à mentionner ici est que n’importe quel élément du DOM peut être utilisé comme conteneur de vue. Ce qui est intéressant, c’est qu’Angular n’insère pas les vues à l’intérieur de l’élément, mais les ajoute après l’élément lié à ViewContainer. C’est similaire à la façon dont le router-outlet insère des composants.

En général, un bon candidat pour marquer un endroit où un ViewContainer devrait être créé est l’élément ng-container. Il est rendu comme un commentaire et donc il n’introduit pas d’éléments HTML redondants dans le DOM. Voici l’exemple de la création d’un ViewContainer à un endroit spécifique dans un modèle de composant:

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

Comme avec les autres abstractions DOM, le ViewContainer est lié à un élément DOM particulier auquel on accède par la propriété element. Dans l’exemple ci-dessus, il est lié à l’élément ng-container rendu comme un commentaire, et la sortie est donc template bindings={}.

Manipulation des vuesLien à cette section

ViewContainer fournit une API pratique pour manipuler les vues:

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

Nous avons vu précédemment comment deux types de vues peuvent être créées manuellement à partir d’un modèle et d’un composant. Une fois que nous avons une vue, nous pouvons l’insérer dans un DOM en utilisant la méthode insert. Voici l’exemple de la création d’une vue intégrée à partir d’un modèle et de son insertion dans un endroit spécifique marqué par un élément ng-container :

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

Avec cette mise en œuvre, le html résultant ressemble à ceci:

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

Pour supprimer une vue du DOM, nous pouvons utiliser la méthode detach. Toutes les autres méthodes sont auto-explicatives et peuvent être utilisées pour obtenir une référence à une vue par l’index, déplacer la vue à un autre emplacement, ou supprimer toutes les vues du conteneur.

Création de vuesLien à cette section

ViewContainer fournit également une API pour créer une vue automatiquement:

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

Ce sont simplement des enveloppes pratiques à ce que nous avons fait manuellement ci-dessus. Ils créent une vue à partir d’un modèle ou d’un composant et l’insèrent à l’emplacement spécifié.

ngTemplateOutlet et ngComponentOutletLien vers cette section

Bien qu’il soit toujours bon de savoir comment les mécanismes sous-jacents fonctionnent, il est généralement souhaitable d’avoir une sorte de raccourci. Ce raccourci se présente sous la forme de deux directives : ngTemplateOutlet et ngComponentOutlet. Au moment où nous écrivons ces lignes, les deux sont expérimentales et ngComponentOutlet sera disponible à partir de la version 4. Mais si vous avez lu tout ce qui précède, il sera très facile de comprendre ce qu’elles font.

ngTemplateOutletLien vers cette section

Celle-ci marque un élément DOM comme un ViewContainer et insère une vue intégrée créée par un modèle sans qu’il soit nécessaire de le faire explicitement dans une classe de composant. Cela signifie que l’exemple ci-dessus où nous avons créé une vue et l’avons insérée dans un élément DOM #vc peut être réécrit comme ceci:

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

Comme vous pouvez le voir, nous n’utilisons aucun code d’instanciation de vue dans la classe de composant. Très pratique !

ngComponentOutletLien vers cette section

Cette directive est analogue à ngTemplateOutlet à la différence qu’elle crée une vue hôte (instancie un composant), et non une vue embarquée. Vous pouvez l’utiliser comme suit :

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

Enveloppe Lien vers cette section

Je réalise que toutes ces informations peuvent être beaucoup à digérer. Mais en fait, c’est assez cohérent et expose un modèle mental clair pour manipuler le DOM via les vues. Vous obtenez des références aux abstractions DOM d’Angular en utilisant une requête ViewChild avec des références de variables de modèle. L’enveloppe la plus simple autour d’un élément DOM est ElementRef. Pour les templates, vous avez TemplateRef qui vous permet de créer une vue embarquée. Les vues hôtes sont accessibles sur componentRef créées à l’aide de ComponentFactoryResolver. Les vues peuvent être manipulées avec ViewContainerRef. Il existe deux directives qui rendent le processus manuel automatique : ngTemplateOutlet – pour les vues embarquées et ngComponentOutlet pour les vues hôtes (composants dynamiques).

Discutez avec la communauté.

Leave a Reply