Es muy sencillo manipular directamente elementos del DOM en Angular, solo hay que echar mano de la clase ElementRef
. Pero ¡cuidado! Angular lo etiqueta como una mala práctica. La manipulación directa del DOM crea un acoplamiento indeseado entre la capa de renderizado y la de lógica, que impide por ejemplo lanzar tu app en un web worker.
Para sortear este obstáculo tienes la clase Renderer2
de Angular. Renderer2
proporciona una API para acceder de forma segura a elementos nativos, incluso cuando no están soportados por la plataforma (web workers, server-side rendering, etc).
Angular, el framework multiplataforma
Quizá no lo sabías, pero Angular se define como platform agnostic.
¿Qué quiere decir esto? Pues que Angular está diseñado para abstraerse del renderizado del DOM, y eso le permite funcionar en distintas plataformas, como:
- Navegadores web
- Servidores (Node.js)
- Web Workers
- Apps móviles nativas (NativeScript, React Native)
Por eso, es muy importante que cuando desarrolles en Angular, te acostumbres a NO UTILIZAR las variables globales document o window, o a manipular directamente el DOM con ElementRef.
Ninguno de estos elementos estará disponible en un entorno que no sea el navegador, y por tanto no podrás reutilizar tu código para renderizar desde servidor (con Angular Universal), meterlo en una app nativa, o conseguir el máximo rendimiento de tu interfaz ejecutándolo todo en un Web Worker.
Como he introducido antes, la forma correcta de manipular el DOM es a través de Renderer2
.
Manipulando el DOM con Renderer2, primeros pasos
En la documentación de Angular sobre Renderer2
puedes encontrar todos los métodos que te ofrece esta clase. Yo me voy a centrar en algunas de las manipulaciones más habituales.
Supongamos un componente de Angular que tiene un botón. Para poder manipular este elemento, necesito una referencia al mismo y para eso sí que utilizo ElementRef
, junto con una template reference variable (myButton) el decorador @ViewChild
.
Pero la manipulación no será a través del ElementRef
, sino mediante el servicio Renderer2
, así que lo primero que tengo que hacer es importarlo e inyectarlo.
import { Component, Renderer2 } from '@angular/core';
@Component({
selector: 'app-root',
template: '<button #myButton></button>'
})
export class AppComponent{
@ViewChild("myButton") myButton: ElementRef;
constructor(private renderer: Renderer2) {
}
}
Añadir o eliminar una clase de un elemento
A partir de aquí, podría alterar las clases del elemento myButton con los métodos de Renderer2
:
addClass(el: any, name: string): void
removeClass(el: any, name: string): void
Como puedes imaginar, name hace referencia la clase que quieres añadir/quitar. En cuanto a el, hace referencia al elemento nativo del DOM sobre el que quieres actuar (ElementRef.nativeElement
).
Te enseño como quedaría, y añado en comentarios como sería el acceso directo (mala práctica) a través de ElementRef
.
import { Component, Renderer2 } from '@angular/core';
@Component({
selector: 'app-root',
template: '<button #myButton></button>'
})
export class AppComponent{
@ViewChild("myButton") myButton: ElementRef;
constructor(private renderer: Renderer2) { }
addMyClass(){
//this.myButton.nativeElement.classList.add("my-class"); //BAD PRACTICE
this.renderer.addClass(this.myButton.nativeElement, "my-class");
}
removeMyClass(){
//this.myButton.nativeElement.classList.remove("my-class"); //BAD PRACTICE
this.renderer.removeClass(this.myButton.nativeElement, "my-class");
}
}
Añadir o eliminar un atributo
Pongamos que quiero crear unos métodos para habilitar o deshabilitar mi botón por código. Renderer2
me ofrece los siguientes métodos.
setAttribute(el: any, name: string, value: string, namespace?: string|null): void
removeAttribute(el: any, name: string, namespace?: string|null): void
De nuevo, te enseño a usarlos y añado también la práctica errónea en comentario de código.
//...some stuff...
export class AppComponent{
@ViewChild("myButton") myButton: ElementRef;
constructor(private renderer: Renderer2) { }
disable(){
//this.myButton.nativeElement.setAttribute("disabled", "true"); //BAD PRACTICE
this.renderer.setAttribute(this.myButton.nativeElement, "disabled", "true");
}
enable(){
//this.myButton.nativeElement.removeAttribute("disabled"); //BAD PRACTICE
this.renderer.removeAttribute(this.myButton.nativeElement, "disabled");
}
}
Llamar a un método del elemento
La dinámica anterior es sencilla y muy similar para clases, atributos, propiedades o estilos. Pero cuando quieres llamar a un método del elemento por código (por ejemplo el método click
de mi botón), te pueden entrar dudas.
De nuevo, Renderer2
te ayuda con eso, y te ofrece el método
selectRootElement(selectorOrNode: string|any): any
Que te devuelve una versión platform-safe del elemento nativo del DOM.
El código correcto a continuación, y el incorrecto comentado:
//...some stuff...
export class AppComponent{
@ViewChild("myButton") myButton: ElementRef;
constructor(private renderer: Renderer2) { }
clickButton(){
//this.myButton.nativeElement.click(); //BAD PRACTICE
this.renderer.selectRootElement(this.myButton.nativeElement).click();
}
}
Esto y otras muchas buenas prácticas las encontrarás en mi curso Componentes en Angular – nivel PRO
Bonus Track: Crear contenido en el DOM
Siguiendo con el ejemplo, imagina que quiero añadirle un texto al botón. El texto del botón en realidad no es más que una cadena de texto que se encuentra en el nodo hijo del botón, así que para conseguirlo con Renderer2
, podría seguir esta estrategia:
- Crear un texto
- Añadir ese texto como hijo del botón
No es problema para Renderer2
, dicho y hecho:
//...some stuff...
export class AppComponent{
@ViewChild("myButton") myButton: ElementRef;
constructor(private renderer: Renderer2) { }
addText(){
let text = this.renderer.createText("my button");
this.renderer.appendChild(this.myButton.nativeElement, text);
}
}
Conclusiones
Angular ofrece muchas formas de evitar manipulaciones del DOM. Aún así, si te encuentras en alguna situación en la que no te quede otro remedio, acuérdate de ser platform-agnostic y utiliza Renderer2
. Si en algún momento decides replicar tu código en otra plataforma, tu yo del futuro te lo agradecerá.
Excelente articulo. Perfectamente explicado. Habia leido algo al respecto pero nunca habia tenido la oportunidad de leer tan detenidamente sobre el renderer de Angular
«tu yo del futuro te lo agradecerá» .. que bueno!, grande Enrique.. 🙂
Muy conciso y claro, gracias Enrique!
Hombre, ¡que alegría verte por aquí! ¿Como va por Mallorca?
Me alegro de que te guste 😉
Hola, me gustó tu artículo y es muy útil. Me gusta mucho el framework Angular y recién estoy dejando atrás su versión JS. Tengo una consulta y espero puedas orientarme o decirme cómo lo harías tú:
Ocurre que tengo un sistema web con decenas de formularios distintos. Me es muy útil que el código que dibuja al formulario se gestione por el servidor, no obstante, estoy viendo que con Angular tendría que crear un componente por cada formulario. No me parece práctico ya que el cliente tendría en su lado todos los formularios cargados y no los usaría todos. Pensaba en utilizar HTTP para obtener el código HTML que dibuja un formulario y colocarlo como template en un componente genérico, pero al hacer eso, no se procesan las interpolaciones ni las propiedades como ngModel.
¿Qué puedo hacer en ese caso? ¿Tendría que utilizar el renderizado para ello sabiendo que es una mala práctica? ¿Cómo lo resolverías tú?
Muchas gracias.
Si lo que te preocupa es que el cliente se tenga que bajar todos los formularios pero solo vaya a usar una parte (con el consiguiente derroche de ancho de banda y tiempo de carga) lo que puedes usar son componentes asíncronos que el router carga en modo lazy-loading.
Saludos
Hola. Muchas gracias por la respuesta. Terminé abandonando Angular pra la migración del sistema y me puse a aprender VueJS. Me enamoré del framework por ser un AngularJS muy simple de utilizar. Hasta ahora me va bien salvo algunos detallitos. Espero haber tomado un buena decisión.
Creo que en esto no hay decisiones buenas ni malas. Angular, VueJS y React (por nombrar los más populares) son frameworks muy válidos para desarrollar en front. Todos tienen un ecosistema potente, y todos tendrán detallitos que no te gustarán o que te gustaría que se aproximasen de otro modo. El tema es quedarte con el que te sientas más cómodo 😉
hola parece bastante util pero desarrollando me paso lo siguiente tengo unos checkbox que estan en un ngfor el nombre #Check en la etiqueta del input no funciona ya que el viewchild se queda con el primero que encuentra y manipula solo el primer como hago en ese caso en que quiera manipular todos de un solo click ???
Hernan, para esto tienes que utilizar @ViewChildren, que sirve para procesar mas de un elemento:
Ejemplo:
en el html:
y en el componente:
@ViewChildren(‘check1’) checkbox1: QueryList;
marcar() {
let _this = this;
this.checkbox1.forEach(function(a) {
_this.renderer.setProperty(a.nativeElement, ‘checked’, true);
});
Hola Carlos,
He intentado utilizar viewchildren pero los cambios no se reflejan en el dom, ¿Podrias subir un ejemplo a plunker?
Muchas gracias.
gracias… mejor explicación no podría haber
Hola Enrique. Lo primero, gracias por el tutorial o tutoriales porque tienes muy buenos.
Tengo una duda respecto a este artículo. Cuando añades un atributo al objeto sobre el que estemos trabajando siempre lo haces sobre la propiedad nativeElement, eso porque es?
Y otra cosa. Si yo hago this.renderer.setAttribute(this.myButton.nativeElement, «disabled», «true»); ¿Cómo accedo a la nueva propiedad disbled? ¿Con this.myButton.disabled?
Gracias por la ayuda. Un saludo. =)
Hola Pablo,
nativeElement
te devuelve una referencia al objeto del DOM. En cuanto a la propiedaddisabled
, no es nueva, los elementos botón la tienes, y puedes acceder a través denativeElement
.Como detecto cuando el scroll llego abajo del todo sin usar las variables globales window y document? Y como detecto cuando no hay un scroll porque tengo pocos elementos? Si me podes ayudar te lo agradecería muchísimo.
Hola. Gracias por el post. Segui los pasos indicados, pero estoy tratando de establecer un atributo de un elemento y me dice que es indefinido, a pesar que lo estoy referenciando con viewChild:
…
this.renderer.setAttribute(this.resultSelector.nativeElement, «size», «10»);
// resultSelector apunta a un elemento select, pero aqui resulta indefinido
…
esto lo hago en ngOnInit(). Tambien lo intente en ngAfterViewInit() … igual.
En ngOnInit aún no está instanciado.
En ngAfterViewInit, sí debería existir la referencia, pero te dará un error por estar modificando el DOM en medio de un ciclo de detección de cambios.
Si te interesa entenderlo en profundidad, en mi curso de componentes se explica muy bien este tema 😉
En ngOnInit aún no está instanciado.
En ngAfterViewInit, sí debería existir la referencia, pero te dará un error por estar modificando el DOM en medio de un ciclo de detección de cambios.
Si te interesa entenderlo en profundidad, en mi curso de componentes Angular se explica muy bien este tema 😉
Muchísimas gracias Enrique, estuve como loco un tiempo intentando manejar el dom de manera más adecuada, y gracias a este post, pude hacerlo, saludos desde Venezuela!
Me ha encantado esta solución que has comentado. Sobretodo la parte de poder ejecutar un click() desde el archivo .ts
Habría alguna posibilidad de ejecutar un mouseenter() o algo por el estilo, en un botón, para evitar navegación pero que aparezca un tooltip en pantalla.
Y si por ejemplo el click al elemento boton o enlace esta en otro componente?
Por cierto, no es mejor usa el metodo listener (equivalente al addevenlistener) del renderer 2?
Buenas tardes Enrique, tengo una pregunta nose si podrias ayudarme, necesito hacer algo asi
this.renderer2.createElement();
nose si se entiende lo que quiero es crear el elemento y agregarle el ngDraggable para que cuando el elemento aparesca pueda moverlo libremente.
pero aun no pude encontrar la respuesta.
PD: con la ayuda de tu post pude crear de manera segura los elementos ya que los hacia con elementRef y document.get… Gracias por la ayuda.
Muy buena información, solo para acotar un poco más y resolver una duda, llevo unos meses en angular y la verdad es que he sufrido un poco para aprender muchas cosas, en cuanto al uso de renderer2 y siguiendo tu recomendación de no hacer uso de ElementRef o acceder al DOM, en la documentación de Angular mencionan que solo se debería de usar en ultima instancia ya que podríamos incurrir en vulnerar nuestra aplicación a ataques XSS (https://angular.io/api/core/ElementRef#description). Ahora vengo trabajando en un proyecto y quise crear un componente(modal) que una vez abierto altere los estilos de su componente padre, supongo que hay otras maneras de realizar la tarea pero aun así me queda la duda si se esta aplicando correctamente el uso de ElementRef y NativeElement en Renderer2.
Para crear un svg, no olvidar el namespace «http://www.w3.org/2000/svg», por ejemplo para crear una etiqueta «g»: «this.renderer.createElement(«g», «http://www.w3.org/2000/svg»);»
Puedo quitar o colocar columnas y filas dinámicamente de una tabla html?
Muchas gracias por la información.
Hola, ElementRef o Render2 pueden obtener los valores que están fuera de el componente, como un componente padre?
Hola que tal, tengo una consulta, intenté eso pero no me muestra nada:
const header: any = `
`;
Es porque se trata de un componente, etiquetas propias como «div» normal se muestran. ¿Hay forma de hacer que funcione?