Plantillas y DOM en componentes

Una de las razones principales por las que nos puede interesar crear un componente en nuestra página es para modularizar una característica o funcionalidad que sabemos que se va a repetir. El componente es una estupenda forma de unir marcado (HTML), estilo (CSS) y funcionalidad (JS) en un mismo sitio, de forma que sea mucho más cómodo de mantener para nosotros, los desarrolladores.

WebComponent: Modular

En los siguientes apartados, vamos a ver las diferentes formas que tenemos de crear marcado (e incluso estilo) en un componente.

Acceso directo al DOM

Probablemente, la forma más rápida de crear marcado en nuestro componente es haciendo uso de la API DOM de Javascript que utilizamos con las etiquetas HTML tradicionales, donde tenemos varias propiedades que se utilizan para reemplazar el DOM de forma rápida, atómica y bastante cómoda para el desarrollador.

Dichas propiedades son las siguientes:

Propiedad Descripción
.innerHTML Reemplaza el marcado del contenido del componente. Permite marcado HTML.
.outerHTML Idem al anterior, pero también reemplaza el componente. Poco usado en WebComponents.
.textContent Reemplaza el contenido textual del componente. No permite marcado HTML.
.innerText Similar al anterior. Utilizar .textContent en su lugar.

Las dos primeras propiedades, .innerHTML y .outerHTML se utilizan para reemplazar el marcado HTML de un componente, aunque .outerHTML no es demasiado práctico en los componentes, puesto que también sustituye la propia etiqueta del componente. Por otro lado, las dos últimas propiedades se utilizan para obtener solo el contenido textual, pero se recomienda usar .textContent ya que .innerText no funciona obteniendo texto de elementos de tipo , por ejemplo.

Veamos un ejemplo de uso de .innerHTML en un componente:

class AppElement extends HTMLElement {

  connectedCallback() {
    this.name = this.getAttribute("name") || "Desconocido";
    this.innerHTML = "<div>" + this.name + "</div>";
  }
}

Esto se puede refactorizar un poco para mejorar la calidad de código. Por ejemplo, podemos sacar el atributo name fuera del método y ponerlo en la parte superior de la clase. También podemos utilizar los backticks para indentar mejor el marcado del y conseguir mayor legibilidad.

Esto nos permite que si el marcado se vuelve más complejo se lea mucho mejor, pudiendo utilizar ${} para agrupar las expresiones o variables JS, evitando también las múltiples concatenaciones:

class AppElement extends HTMLElement {

  name = this.getAttribute("name") || "Desconocido";

  connectedCallback() {
    this.innerHTML = `
      <div class="element">
        <div>${this.name}</div>
      </div>`;
  }
}

Ten en cuenta que si reemplazaramos .innerHTML por .textContent en lugar de renderizarse las etiquetas HTML, se mostrarían literalmente, ya que .textContent interpreta literalmente como texto el marcado.

Esta forma de incluir marcado HTML dentro de strings está muy sujeta a controversia y no gusta a todos los desarrolladores. En ciertas comunidades como las de CSS-in-JS o React es muy utilizada (con considerables diferencias), pero en otras suele causar rechazo. Más adelante veremos otras alternativas, aunque esta es bastante popular y extendida.

Acceso dinámico al DOM

Existe una forma de manejar el marcado del componente a más bajo nivel que, aunque a priori es más laborioso, nos permite realizar tareas dinámicas como bucles u otras de una forma más sencilla y práctica.

Método Descripción
document.createElement(tag) Crea y devuelve una etiqueta HTML tag.
element.appendChild(child) Añade el elemento child dentro de element.
element.insertAdjacentHTML(pos,html) Inserta el código html en element.
element.insertAdjacentElement(pos,node) Inserta el elemento node en element.

En primer lugar, document.createElement() suele utilizarse conjuntamente con .appendChild(). El primero crea el elemento pasado por parámetro y lo devuelve, generalmente para almacenarlo en una variable. Es importante recalcar que ese elemento no se guarda en el documento HTML, aunque se haga sobre document.

Para ello, hay que utilizar el segundo de los métodos de la tabla, que lo que hace es añadir el elemento que se le pasa por parámetro en el elemento donde se llama a .appendChild().

Veamos un ejemplo para hacer lo mismo que el código del apartado anterior:

class AppElement extends HTMLElement {

  name = this.getAttribute("name") || "Desconocido";

  connectedCallback() {
    const element = document.createElement("div");
    element.className = "element";
    this.appendChild(element);

    const innerElement = document.createElement("div");
    innerElement.textContent = this.name;
    element.appendChild(innerElement);
  }
}

Como vemos, el código es algo más «verboso», pero puede ser realmente útil cuando queremos realizar bucles o una lógica dinámica que es más compleja de crear al estilo del apartado anterior, mediante cadenas de texto .

Los métodos .insertAdjacentHTML() o .insertAdjacentElement() son un híbrido entre las formas de manipulación del DOM que hemos visto hasta ahora, donde tenemos más flexibilidad a la hora de insertar, ya que el parámetro pos es un que puede determinar el punto exacto donde se añadirá el elemento.

appendChild() vs insertAdjacentHTML()

Tienes más información de su funcionamiento en Insertar elementos en el DOM.

Acceso mediante templates

Otra forma interesante de trabajar con el marcado en un componente es utilizando la etiqueta <template>, otra de las características independientes de los WebComponents. Esta etiqueta HTML permite crear un fragmento de código de forma aislada del documento, de modo que todo su contenido está inerte y no es procesado ni renderizado por el navegador.

Nota: Ten en cuenta que una etiqueta <template>, al ser inerte, no se procesa por el navegador hasta que se clona (ni sus recursos, como scripts o imágenes). Además, si se define en el HTML, su rendimiento es mayor que otras alternativas como .innerHTML, ya que se evita todo el proceso dinámico de parseo y análisis de a marcado HTML.

Podemos utilizar esta característica para usar ese marcado como código de base que clonaremos posteriormente y utilizaremos en la creación de instancias del componente.

Veamos el mismo ejemplo anterior, utilizando esta mecánica (recuerda que la parte del marcado puede ser definida en el HTML con la etiqueta <template> si se prefiere):

// Marcado HTML del componente
const template = document.createElement("template");
template.innerHTML = `
  <div class="element">
    <div class="name"></div>
  </div>`;

// Lógica Javascript del componente
class AppElement extends HTMLElement {

  name = this.getAttribute("name") || "Desconocido";

  connectedCallback() {
    const markup = template.content.cloneNode((true));
    markup.querySelector(".name").textContent = this.name;
    this.appendChild(markup);
  }
}

En este ejemplo hemos seguido un enfoque más similar al de los ficheros .vue de VueJS, donde colocamos el marcado en la parte superior del componente (fuera de la clase) y el contenido de los métodos de clase los limitamos a trabajar con lógica de programación y gestión de datos, por lo que el rechazo a mezclar fragmentos de cadenas de texto y lógica, no es tan alto, ya que estamos separando ligeramente las preocupaciones (marcado y funcionalidad).

Remarcar el uso de .content.cloneNode(deep) sobre un template. Con esto, lo que estamos haciendo es utilizar el código HTML de un <template> y hacer una copia, clonarlo, para posteriormente insertarlo en nuestro componente.

En el caso de establecer deep a true, haremos una clonación profunda (deep clone), es decir, se clona el elemento y todos sus elementos hijos. En caso de establecerlo a false, se hará una clonación superficial (shallow clone), es decir, clonará sólo el elemento.

Manz
Publicado por Manz

Docente, divulgador informático y freelance. Autor de Emezeta.com, es profesor en la Universidad de La Laguna y dirige el curso de Programación web FullStack y Diseño web FrontEnd de EOI en Tenerife (Canarias). En sus ratos libres, busca GIF de gatos en Internet.