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.js
sabrás que era bastante fácil manipular el DOM. Angular inyectaba el DOM element
en la función link
y 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í:
<>CopiarngAfterViewInit() { 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
:
<>Copiarconstructor(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
ViewContainer
proporciona una práctica API para manipular vistas:
<>Copiarclass 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:
<>Copiarclass 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