Plantillas y HTML en componentes

Cómo escribir código HTML en un WebComponent nativo

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

¿Por qué plantillas?

Una de las finalidades de un WebComponent es encapsular un marcado HTML más complejo dentro de una etiqueta HTML personalizada más sencilla, simplificando mucho su utilización, reduciendo la cantidad de código que tenemos que escribir y permitiendo reutilizarlo.

Pero hay que recordar que un WebComponent, al fin y al cabo, es un fichero javascript .js, por lo que no podemos escribir código HTML directamente, sino que tenemos que utilizar alguna de las siguientes estrategias.

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

HTML a través de strings

Probablemente, la forma más rápida de crear marcado en nuestro componente es haciendo uso de la API Javascript que permite escribir etiquetas HTML tradicionales dentro de un . Tenemos varias propiedades que se utilizan para trabajar con el DOM de forma rápida, atómica y bastante cómoda para el desarrollador.

Dichas propiedades son las siguientes:

Propiedad Descripción
.innerHTML Añade o reemplaza el marcado HTML del componente.
.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.

Más información en Insertar elementos en el DOM

De las anteriores, una de las más utilizadas es .innerHTML. Veamos un ejemplo usándolo en un componente:

class AppElement extends HTMLElement {

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

El método connectedCallback() es un método especial que se ejecuta cuando el componente se inserta en el documento HTML. Lo explicamos más adelante, en el apartado de ciclo de vida de un WebComponent.

Esto se puede refactorizar un poco para mejorar la calidad de código y hacerlo más legible. 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 = /* html */`
<div class="container">
<div class="name">
${this.name}</div>
</div>
`
;
}
}

Consejo: Observa que justo antes del string template hemos añadido el comentario /* html */. Esto es una práctica muy utilizada en WebComponents. Con extensiones para Visual Studio Code como ES6-strings-html podemos indicarle al editor que lo que contiene el que viene a continuación es código HTML, activando el resaltado de sintaxis en color o permitiendo otras características de HTML. También lo podemos hacer con código CSS, SVG, SQL, XML o GLSL (OpenGL Shading).

Es importante tener en cuenta que si en el ejemplo anterior utilizaramos .textContent en lugar de .innerHTML no se renderizarían las etiquetas HTML, sino que se mostrarían literalmente, ya que .textContent interpreta literalmente como texto el marcado HTML.

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. Sin embargo, se ha convertido en la forma más extendida. Más adelante veremos otras alternativas.

EMMET en String templates

Es muy posible que queramos utilizar la posibilidad de utilizar EMMET en los strings templates al igual que lo hacemos en un documento HTML, de forma que al escribir p y pulsar TAB, nos lo reemplace por <p></p> y posicione el cursor en su interior.

En Visual Studio Code es muy sencillo. Basta con pulsar F1 y buscar la opción Open User Settings (JSON). Nos aparece un fichero .json con la configuración actual del usuario. Buscaremos o añadiremos la siguiente línea:

{
[...],
"emmet.includeLanguages": {
"javascript": "html"
},
[...]
}

Esto hará que EMMET pueda ser escrito en ficheros Javascript, algo que junto a la extensión de VSCode mencionada anteriormente, hará que escribir código HTML en WebComponents sea más sencillo y cómodo.

HTML a través del DOM

Otra forma de manejar el marcado HTML del componente es a través de la API del DOM de Javascript. En principio es un poco más verboso y puede resultar tedioso, pero ofrece mejor rendimiento y puede ser muy interesante si somos organizados.

Algunos de los métodos que nos puede interesar usar:

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 container = document.createElement("div");
container.classList.add("container");
this.appendChild(container);

const nameContainer = document.createElement("div");
nameContainer.classList.add("name");
nameContainer.textContent = this.name;
container.appendChild(nameContainer);
}
}

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 .

Por otro lado, 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.

HTML mediante templates

Otra forma interesante de trabajar con el marcado HTML en un componente es utilizando la etiqueta <template>. 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 inmediatamente por el navegador, sino que se pospondrá hasta que clonemos su contenido.

Nota: Recalcar que el navegador no procesa el contenido de una etiqueta <template> (ni carga 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 etiqueta <template> puede definirse en el archivo .html si se prefiere y localizar mediante un .querySelector()):

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

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

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

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

En este ejemplo hemos colocado el marcado en la parte superior del componente (fuera de la clase). De esta forma, se creará una sola vez y se utilizará cuando sea necesario. El contenido de los métodos de clase los limitamos a trabajar con lógica de programación y gestión de datos.

Remarcar el uso de .content.cloneNode(deep) sobre un template. Con esto, lo que estamos haciendo es clonar el código HTML de un <template>, 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 indicado (y no sus hijos, los cuales serán una referencia a los originales).

Tabla de contenidos