Explorando las técnicas de manipulación del DOM en Angular usando ViewContainerRef

Siempre que leo sobre cómo trabajar con el DOM en Angular veo que se mencionan una o varias de estas clases: ElementRef, TemplateRef, ViewContainerRef y otras. Desafortunadamente, aunque algunas de ellas están cubiertas en los docs de Angular o en artículos relacionados, todavía no he encontrado la descripción del modelo mental general y ejemplos de cómo funcionan juntos. Este artículo pretende describir dicho modelo.

Si buscas información más profunda sobre la manipulación del DOM en Angular usando Renderer y View Containers consulta mi charla en NgVikings. O lee un artículo en profundidad sobre la manipulación dinámica del DOM Trabajar con el DOM en Angular: consecuencias inesperadas y técnicas de optimización

Si vienes del mundo angular.jssabrás que era bastante fácil manipular el DOM. Angular inyectaba el DOM element en la función linky podías consultar cualquier nodo dentro de la plantilla del componente, añadir o eliminar nodos hijos, modificar estilos, etc. Sin embargo, este enfoque tenía un defecto importante: estaba estrechamente vinculado a una plataforma de navegador.

La nueva versión de Angular se ejecuta en diferentes plataformas – en un navegador, en una plataforma móvil o dentro de un web worker. Así que se requiere un nivel de abstracción para estar entre la API específica de la plataforma y las interfaces del framework. En Angular estas abstracciones vienen en forma de los siguientes tipos de referencia: ElementRef, TemplateRef, ViewRef, ComponentRef y ViewContainerRef. En este artículo echaremos un vistazo a cada tipo de referencia en detalle y mostraremos cómo se pueden utilizar para manipular el DOM.

@ViewChildLink a esta sección

Antes de explorar las abstracciones del DOM, vamos a entender cómo accedemos a estas abstracciones dentro de una clase componente/directiva. Angular proporciona un mecanismo llamado consultas DOM. Viene en forma de decoradores @ViewChild y @ViewChildren. Se comportan igual, sólo que el primero devuelve una referencia, mientras que el segundo devuelve múltiples referencias como un objeto QueryList. En los ejemplos de este artículo utilizaré principalmente el decorador ViewChild y no utilizaré el símbolo @ que lo precede.

Por lo general, estos decoradores se emparejan con variables de referencia de plantilla. Una variable de referencia de plantilla es simplemente una referencia con nombre a un elemento DOM dentro de una plantilla. Puedes verlo como algo similar al atributo id de un elemento html. Usted marca un elemento DOM con una referencia de plantilla y luego lo consulta dentro de una clase utilizando el decorador ViewChild. Este es el ejemplo básico:

<>Copia
@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 sintaxis básica del decorador ViewChild es:

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

En este ejemplo puedes ver que he especificado tref como nombre de referencia de la plantilla en el html y recibir el ElementRef asociado a este elemento. El segundo parámetro read no siempre es necesario, ya que Angular puede inferir el tipo de referencia por el tipo del elemento DOM. Por ejemplo, si es un elemento html simple como span, Angular devuelve ElementRef. Si es un elemento template, devuelve TemplateRef. Algunas referencias, como ViewContainerRef no se pueden inferir y hay que pedirlas específicamente en el parámetro read. Otras, como ViewRef no pueden ser devueltas desde el DOM y tienen que ser construidas manualmente.

Bien, ahora que sabemos cómo consultar las referencias, empecemos a explorarlas.

ElementRefEnlace a esta sección

Esta es la abstracción más básica. Si observas su estructura de clases, verás que sólo contiene el elemento nativo al que está asociado. Es útil para acceder a elementos nativos del DOM como podemos ver aquí:

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

Sin embargo, este uso está desaconsejado por el equipo de Angular. No sólo plantea un riesgo de seguridad, sino que también acopla estrechamente su aplicación y las capas de renderizado, lo que dificulta la ejecución de una aplicación en múltiples plataformas. Creo que no es el acceso a nativeElement lo que rompe la abstracción, sino el uso de una API DOM específica como textContent. Pero como verás más adelante, el modelo mental de manipulación del DOM implementado en Angular casi nunca requiere un acceso de tan bajo nivel.

ElementRef puede ser devuelto para cualquier elemento del DOM usando el decorador ViewChild. Pero como todos los componentes se alojan dentro de un elemento DOM personalizado y todas las directivas se aplican a elementos DOM, las clases de componentes y directivas pueden obtener una instancia de ElementRef asociada a su elemento anfitrión a través de la inyección de dependencia (DI):

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

Así que mientras un componente puede obtener acceso a su elemento anfitrión a través de DI, el decorador ViewChild se utiliza más a menudo para obtener una referencia a un elemento DOM en su vista (plantilla). Pero, es al revés para las directivas – no tienen vistas y por lo general trabajan directamente con el elemento al que se adjuntan.

TemplateRefLink a esta sección

La noción de una plantilla debe ser familiar para la mayoría de los desarrolladores web. Es un grupo de elementos del DOM que se reutilizan en las vistas de toda la aplicación. Antes de que el estándar HTML5 introdujera la etiqueta template, la mayoría de las plantillas llegaban al navegador envueltas en una etiqueta script con alguna variación del atributo type:

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

Este enfoque tenía ciertamente muchos inconvenientes como la semántica y la necesidad de crear manualmente modelos DOM. Con la etiqueta template un navegador analiza html y crea un árbol DOM pero no lo renderiza. Luego se puede acceder a él a través de la propiedad content:

<>Copia
<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 adopta este enfoque e implementa la clase TemplateRef para trabajar con una plantilla. Así es como se puede utilizar:

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

El framework elimina el elemento template del DOM e inserta un comentario en su lugar. Así es como se ve cuando se renderiza:

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

Por sí misma la clase TemplateRef es una clase simple. Mantiene una referencia a su elemento anfitrión en la propiedad elementRef y tiene un método: createEmbeddedView. Sin embargo, este método es muy útil ya que nos permite crear una vista y devolver una referencia a ella como ViewRef.

ViewRefLink a esta sección

Este tipo de abstracción representa una Vista de Angular. En el mundo Angular una Vista es un bloque de construcción fundamental de la UI de la aplicación. Es la agrupación más pequeña de elementos que se crean y destruyen juntos. La filosofía de Angular anima a los desarrolladores a ver la UI como una composición de Vistas, no como un árbol de etiquetas HTML independientes.

Angular soporta dos tipos de vistas:

  • Vistas incrustadas que están vinculadas a una Plantilla
  • Vistas anfitrionas que están vinculadas a un Componente

Creación de una vista incrustadaEnlace a esta sección

Una plantilla simplemente contiene un plano para una vista. Una vista puede ser instanciada desde la plantilla utilizando el método createEmbeddedView antes mencionado, así:

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

Crear una vista anfitrionaEnlace a esta sección

Las vistas anfitrionas se crean cuando un componente es instanciado dinámicamente. Un componente puede ser creado dinámicamente usando ComponentFactoryResolver:

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

En Angular, cada componente está ligado a una instancia particular de un inyector, por lo que estamos pasando la instancia actual del inyector al crear el componente. Además, no hay que olvidar que los componentes que se instancian dinámicamente deben añadirse a los EntryComponents de un módulo o componente anfitrión.

Así, hemos visto cómo se pueden crear tanto vistas incrustadas como anfitrionas. Una vez que se crea una vista se puede insertar en el DOM utilizando ViewContainer. La siguiente sección explora su funcionalidad.

ViewContainerRefEnlace a esta sección

ViewContainerRef representa un contenedor donde se pueden adjuntar una o más vistas.

Lo primero que hay que mencionar aquí es que cualquier elemento del DOM se puede utilizar como contenedor de vistas. Lo interesante es que Angular no inserta las vistas dentro del elemento, sino que las anexa después del elemento ligado a ViewContainer. Esto es similar a cómo el router-outlet inserta componentes.

Usualmente, un buen candidato para marcar un lugar donde se debe crear un ViewContainer es el elemento ng-container. Se renderiza como un comentario y así no introduce elementos HTML redundantes en el DOM. Aquí está el ejemplo de la creación de un ViewContainer en un lugar específico en una plantilla de componentes:

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

Al igual que con otras abstracciones del DOM, ViewContainer está vinculado a un elemento DOM particular al que se accede a través de la propiedad element. En el ejemplo anterior está ligado al elemento ng-container renderizado como un comentario, por lo que la salida es template bindings={}.

Manipulación de vistasEnlace a esta sección

ViewContainerproporciona una práctica API para manipular vistas:

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

Hemos visto antes cómo se pueden crear manualmente dos tipos de vistas a partir de una plantilla y un componente. Una vez que tenemos una vista, podemos insertarla en un DOM utilizando el método insert. Este es el ejemplo de crear una vista incrustada a partir de una plantilla e insertarla en un lugar específico marcado por un elemento ng-container:

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

Con esta implementación, el html resultante tiene el siguiente aspecto:

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

Para eliminar una vista del DOM, podemos utilizar el método detach. Todos los demás métodos se explican por sí mismos y se pueden utilizar para obtener una referencia a una vista por el índice, mover la vista a otra ubicación, o eliminar todas las vistas del contenedor.

Creación de VistasEnlace a esta sección

ViewContainer también proporciona una API para crear una vista automáticamente:

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

Estos son simplemente envoltorios convenientes a lo que hemos hecho manualmente arriba. Crean una vista a partir de una plantilla o componente y la insertan en la ubicación especificada.

ngTemplateOutlet y ngComponentOutletLink a esta sección

Aunque siempre es bueno saber cómo funcionan los mecanismos subyacentes, suele ser deseable tener algún tipo de atajo. Este atajo viene en forma de dos directivas: ngTemplateOutlet y ngComponentOutlet. En el momento de escribir esto, ambas son experimentales y ngComponentOutlet estará disponible a partir de la versión 4. Pero si has leído todo lo anterior, será muy fácil entender lo que hacen.

ngTemplateOutletEnlace a esta sección

Esta marca un elemento del DOM como un ViewContainer e inserta una vista incrustada creada por una plantilla sin necesidad de hacerlo explícitamente en una clase componente. Esto significa que el ejemplo anterior en el que creamos una vista y la insertamos en un elemento DOM #vc puede reescribirse así:

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

Como puedes ver no utilizamos ningún código de instanciación de la vista en la clase componente. Muy útil!

ngComponentOutletEnlace a esta sección

Esta directiva es análoga a ngTemplateOutlet con la diferencia de que crea una vista anfitriona (instanciando un componente), y no una vista incrustada. Puedes usarla así:

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

ContinuaciónEnlace a esta sección

Me doy cuenta de que toda esta información puede ser mucho para digerir. Pero en realidad es bastante coherente y establece un modelo mental claro para manipular el DOM a través de las vistas. Obtienes referencias a las abstracciones del DOM de Angular utilizando una consulta ViewChild junto con referencias a variables de plantilla. La envoltura más simple alrededor de un elemento DOM es ElementRef. Para las plantillas tienes TemplateRef que te permite crear una vista incrustada. Se puede acceder a las vistas incrustadas en componentRef creado con ComponentFactoryResolver. Las vistas se pueden manipular con ViewContainerRef. Hay dos directivas que hacen que el proceso manual sea automático: ngTemplateOutlet – para las vistas incrustadas y ngComponentOutlet para las vistas host (componentes dinámicos).

Discute con la comunidad

Leave a Reply