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á.