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

Datos inmutables en Javascript

Datos inmutables en Javascript

En Javascript todo parece mutable, es decir, que se puede cambiar, pero lo cierto es que también nos ofrece varios mecanismos para conseguir que los datos que manejamos, especialmente los objetos, sean inmutables. Te invitamos a descubrir cómo…

Copiar objetos en Javascript

Copiar objetos en Javascript

Copiar objetos no es algo sencillo, incluso se podría decir que en si mismo no es posible, ya que el concepto «copiar» no entra dentro del paradigma de los objetos. No obstante, por medio de instrucciones como Object.assign() hemos aprendido como obtener objetos con las mismas propiedades, pero está técnica no se puede aplicar a todos los tipos de objetos disponibles en Javascript. Vamos a ver cómo podemos copiar cualquier tipo de objeto…

Descubre los Javascript Array Internals

Descubre los Javascript Array Internals

El Array es una de las estructuras más utilizadas en Javascript y no siempre bien comprendida. Hoy os invitamos a analizar el comportamiento interno de este objeto y descubrir cómo Javascript implementa las diferente acciones con los Array y que operaciones internas se realizan en cada caso.

Web Components: pasado, presente y futuro

Web Components: pasado, presente y futuro

Los Web Components aparecieron en el panorama de desarrollo hace ya bastante tiempo. Desde su presentación se les ha prestado mucha atención, pero lo cierto es que no han sido acogidos de forma generalizada, quizás por la difusión de nuevos y potentes frameworks. Nos preguntamos qué ha pasado con este estándar y, sobre todo, que puede pasar de aquí en adelante con el uso práctico de los componentes web.