Seleccionar página
Cuando uno ha desarrollado un Web Component le interesa poder utilizar este componente de una forma sencilla en otros proyectos, es decir, utilizar algún tipo de import. Copiar y pegar el código es una técnica que todos hemos utilizado en alguna ocasión, pero no es muy práctica, ya que no podemos mantener el código organizado y gestionar su evolución de forma razonable.

Lo ideal es que los proyectos puedan importar los componentes que hemos desarrollado y que estos queden recogidos en un fichero independiente que pueda ser mantenido de forma eficiente. Aunque hay varias soluciones fuera de los estándares, también tenemos algunas opciones soportadas por Javascript e implementadas por los navegadores de forma nativa.

Pero antes de llegar a estas soluciones, vamos a ver lo que pudo ser y no fue en los componentes web: el HTML import:

HTML import

Originalmente, la especificación de los Web Components estableció un mecanismo de importación por medio de la etiqueta link. Para ello añadió el atributo rel="import" y utilizaba el atributo href para apuntar a un fichero HTML donde teníamos las etiquetas template con la plantilla y script con el código del componente.

Lo cierto es que resultaba extremadamente sencillo de utilizar. Normalmente estas etiquetas se incluyen en el elemento head y quedan más o menos de esta forma:

<head>
  <meta charset="UTF-8">
  <title>HTML Import</title>
  <link ref="import" href="./components/my-component.html">
</head>

Poco duró esta aproximación, en estos momentos se considera una funcionalidad deprecated, es decir, no recomendada y descontinuada. Todo surgió cuando Mozilla puso en entredicho la idoneidad de esta solución y anunció que no la implementaría en Firefox. Se produjo una reacción en cadena, que ha terminado con el consenso de todos los fabricantes: lo considerarán una mala idea. Incluso Google, que había implementado de forma nativa esta funcionalidad, la retiró en Chrome 73 (abril de 2019). Aunque todavía funciona el polyfill desarrollado para dar soporte a esta instrucción, esta es una vía muerta y no debemos insistir en ella.

Es realmente una pena. Una importación basada en HTML permitía a los maquetadores que tienen poco conocimiento de Javascript importar custom elements con sencillez. Las razones de este abandono son complejas, relacionadas con el rendimiento en la carga de los elementos de las páginas, y seguramente Mozilla tenia muchas e importantes razones para negarse a esta funcionalidad, pero -en mi modesta opinión- hemos salido perdiendo todos.

Javascript import

La solución que nos ofrecen desde el consorcio que promueve los Web Components (webcomponents.org) es utilizar la instrucción import de Javascript (ES6). De esta forma se traslada la importación, desde el ámbito declarativo del HTML, al ámbito del lenguaje de programación.

La instrucción import permite importar un módulo escrito en Javascript. Hasta ES6 la única forma que teníamos de cargar un programa JS en el navegador era por medio de la etiqueta script. Con la implementación de ES6, que ya tienen todos los navegadores modernos, podemos utilizar import para cargar otro programa.

La única restricción que tenemos es que import sólo se puede incluir en programas que carguemos como módulos, es decir, que el elemento script tenga la propiedad type="module", por ejemplo:

<script type="module" src="./modules/my-module.js">

También puede ser utilizado de esta forma:

<script type="module">
  import MyComponent from './modules/my-module.js';
  customElements.define('my-component', MyComponent);
</script>

La gran virtud es que los elementos definidos como globales dentro del módulo no afectan al contexto global del navegador, es decir, una variable o una clase definida como global en el módulo no queda definida en el objeto window del navegador, queda retenida en el ámbito del módulo.

Si queremos que un determinado elemento definido en el módulo pueda ser utilizado fuera de él, es decir, que se pueda obtener una referencia en el módulo que lo importa, entonces debe utilizarse la expresión export. Esta instrucción indica que elementos del módulo son accesibles desde el programa que lo importa.

export default class MyComponent extends HTMLElement {

};

Hay algunas otras características de los módulos que seguramente sea interesante que conozcas, como los valores por defecto, cómo aplicar alias a los nombres exportados, etc. Te recomendamos que le des un vistazo a la documentación de MDN para obtener algo más de información.

¿Quién debe hacer la llamada a customElement.define()?

Cuando analizamos customElements a fondo, vimos que, una vez registrado un componente con un nombre concreto, no podemos volverlo a registrar con otra clase. El nombre queda registrado en el contexto general de custom tag y no puede ser reescrito. También vimos que una clase no puede ser utilizada más de una vez para crear varios custom tag, quedando reservada sólo para ese nombre de componente. Por lo tanto, sobre todo en proyectos de cierta envergadura, es importante establecer con claridad quien debe registrar el componente y con que nombre, para evitar conflictos.

Si el componente lo registra el módulo que lo contiene, es decir, el componente se registra a si mismo, este debe encargarse de evitar una carga más de una vez, por ejemplo, porqué dos programas tienen esta dependencia y realizan la importación del mismo fichero. El programador debe dejar claro en la documentación el nombre que se va a registrar y debemos asegurarnos qué no colisiona con otros nombres de nuestros proyectos, por ejemplo, por medio de algún prefijo.

class MyComponent extends HTMLElement {

}

if (!customElements.get ('generic-component')) {
  customElements.define ('generic-component', MyComponent);
}

En el caso que exportemos la clase y sea el programa que hace la importación quien registra el componente con el nombre que él decida, seguramente debamos asegurarnos que hacemos un nivel adicional de herencia para evitar utilizar la misma clase en más de un componente.

import MyComponent from './modules/my-module.js';
customElements.define('my-component', class extends MyComponent {});

Sinceramente, no tengo claro cuál de los dos sistemas es más adecuado para proyectos grandes. Tengo cierta tendencia a pensar que es mejor exportar la clase y que registre el componente el programa que lo importa decidiendo cómo debe llamarse, pero en la práctica estamos usando más el modelo de cargar el módulo y que él registre el componente con el nombre que su desarrollar ha elegido.

También se puede utilizar script

Aunque la instrucción import nos parezca muy atractiva, lo cierto es que nada nos impide cargar nuestros componentes con una simple instrucción script del HTML, especialmente si el componente se registra así mismo.

Lo único que debemos tener en cuenta es se debería comportar como un módulo, es decir, no definir variables u otros elementos en el contexto global. Para controlar esto hemos utilizado en muchas ocasiones el Immediately-Invoked Function Expression (IIFE), es decir, expresiones de invocación directa de funciones, por ejemplo:

(function () {
  // my component code
})();

En ES6 ya no es necesario utilizar una función y podemos utilizar simplemente un par de { } para envolver nuestro código y utilizar const y let para definir las constantes y variables dentro de ese alcance. Quedarán recogidas dentro de la closure definida por el par de corchetes.

{
  class MyComponent extends HTMLElement {
  
  }
  
  if (!customElements.get ('generic-component')) {
    customElements.define ('generic-component', MyComponent);
  }
}

¿Dónde ponemos nuestro template?

Tal y como adelantamos cuando analizamos las diferentes formas de utilizar plantillas o templates para los componentes web, la limitación que tenemos con la instrucción import es que sólo importa Javascript, es decir, no podemos utilizarla para importar directamente el elemento template que contenga la plantilla de nuestro componente.

Hay bastante ejemplos que crean un elemento template desde Javascript, pero como ya explicamos, es mejor utilizar las alternativas que ofrecen los template string o crear los elementos que necesitemos directamente con document.createElement(). No tiene demasiada utilidad es crear el elemento template y luego clonarlo, aunque es perfectamente posible.

En cualquier caso, todo el código de nuestro componente queda dentro del código, en un fichero .js o .mjs (que es cómo se suele identificar a los Módulos JavaScript) y no tenemos que “mezclar” HTML y Javascript en el fichero de nuestro componente o teniendo que cargar un fichero HTML y otro JS por diferentes medios.

Un momento, yo he visto utilizar import con ficheros CSS y HTML

Hay herramientas como WebPack que definen loaders específicos para cada tipo de fichero y permite que se puedan cargar con import ficheros .html, .css e incluso .vue o .jsx. Todo esto es parte de la mágia que hacen este tipo de herramientas, pero están muy alejadas del estándar y del soporte nativo por los navegadores.

En el comité TC39, el que estandariza el Javascript, se está discutiendo sobre la posibilidad de utilizar la instrucción import para cargar JSON, CSS o HTML. Es todavía una discusión preliminar, por lo que no podemos esperar a tener este tipo de soluciones como parte del estándar e implementadas en los navegadores hasta dentro de bastante tiempo.

Por supuesto podemos utilizar herramientas de compilación que resuelvan la importación, lo hacen de forma muy eficiente. Prácticamente todos los frameworks modernos los utilizan, pero con los web components no es estrictamente necesario la utilización de estos complementos, aunque se pueden utilizar sin problemas.

Loader personalizado

También podemos plantearnos hacer un loader personalizado. No es complicado implementar su propio cargador de componentes con XMLHttpRequest o fetch y, por ejemplo, un objeto DOMParse. Básicamente lo que podemos hacer es escribir en un fichero una etiqueta template y una etiqueta script, cargamos este fichero, lo interpretamos como un fragmento HTML e insertamos su contenido en la página.

Realmente son unas pocas líneas de código. Aquí te tienes un ejemplo bastante completo:

export default function loadComponent (file) {
  return new Promise ((resolve, reject) => {
    fetch (file)
      .then (result => {
        return result.text ();
      })
      .then (txt => {
        const parser           = new DOMParser ();
        const fragment         = parser.parseFromString (txt, 'text/html');
        const originalTemplate = fragment.getElementsByTagName ('TEMPLATE')[ 0 ];
        let template;
        if (originalTemplate) {
          template           = document.createElement ('template');
          template.innerHTML = originalTemplate.innerHTML;
          template.id        = originalTemplate.id;
          document.body.appendChild (template);
        }
        const originalScript = fragment.getElementsByTagName ('SCRIPT')[ 0 ];
        let script;
        if (originalScript) {
          script           = document.createElement ('script');
          script.innerHTML = originalScript.innerHTML;
          document.body.appendChild (script);
        }
        resolve ({template, script});
      })
      .catch (reject);
  });
}

Ahora podemos cargar un componente simplemente de esta forma:

<script type="module">
  import loadComponent from './loadComponent.js';
  loadComponent('./components/wc-countries.html');
</script>

Este es un cargador de ejemplo con una funcionalidad básica. Se pueden añadir otras funcionalidades, como poder disponer de más de una etiqueta template, es bastante sencillo y puedes adaptar este programa a tus necesidades.

Conclusiones

Aunque fue una decepción el establecimiento como deprecated del HTML import, lo cierto es que el Javascript import permite resolver la carga de los componentes razonablemente, sobre todo cuando tenemos todo el contenido en un solo fichero .js o .mjs. Este cambio ha provocado cierta confusión en la comunidad, pero una vez aclarado, sabemos qué podemos utilizar.

Aunque no es del todo necesario, podemos construir nuestro propio cargador de componentes es bastante sencillo y aprovechar esta función para crear nuestra propia estructura de fichero, con los elementos y características que interesen en nuestra organización.

Siempre podemos utilizar alguna de las herramientas de compilación que permiten realizar import de prácticamente cualquier tiempo de fichero. Además de gestionar su carga, nos ayudarán a empaquetar todos los elementos de forma eficiente para la ejecución de nuestro proyecto.

Novedades

Template a fondo

Template a fondo

Hay dos formas estándar de crear contenido en un componente de forma flexible: la etiqueta template, que se considera como uno de los pilares de los Web Components y las template string de Javascript, que son una buena alternativa para generar el Shadow DOM con interpolación de datos.
Light DOM a fondo

Light DOM a fondo

El Light DOM es un espacio compartido entre nuestro componente web y el DOM general, que podemos utilizar para insertar contenido o configurar nuestro componente. Es una muy interesante característica que debemos conocer.
Shadow DOM a fondo

Shadow DOM a fondo

Para que los componentes web no colisionen unos con otros es muy útil utilizar el Shadow DOM para aislar el DOM y el CSS de cada componente. Esta característica se puede aplicar también a elementos HTML sin necesidad de utilizar Custom Elements, pero es con estos donde cobra todo su potencial. Demos un repaso profundo a las capacidades del Shadow DOM.
HTMLElement a fondo

HTMLElement a fondo

HTMLElement es una pieza clave, ya que de él heredan todos los componentes web, pero en muchas ocasiones no conocemos bien sus características. Os invitamos a dar un repaso a fondo a sus capacidades y descubrir cómo sacarle todo el partido en nuestros componentes.