Los Web Components están formados por un conjunto de diferentes tecnologías que encapsulan la estructura interna de elementos HTML y su correspondiente funcionalidad con el fin de que puedan ser reutilizados tanto en webs como aplicaciones. Entre estas tecnologías normalmente se encuentran CSS y JavaScript. En este post os damos una introducción a Web Components en Vanilla JS.

Especificaciones

Los Web Components están basados en cuatro especificaciones principales:

Ciclo de vida

Los cuatro estados principales del ciclo de vida de un Web Component son:

window.customElements.define (my-first-component, MyFirstComponent);

A su vez hay otras funciones que también están dentro del ciclo de vida y nos pueden resultar útiles:

Creación de un Web Component

Vamos a crear un componente sencillo, que llamaremos “tag” y que servirá para mostrar un valor informativo.

Lo primero que tenemos que hacer es crear un archivo para el componente.

my-tag.js

class MyTag extends HTMLElement {
}
export default MyTag;

A continuación vamos a crear los métodos básicos constructor() y connectedCallback(). En este último hemos asociado las funciones que queremos que se ejecuten cuando inyectamos el componente.

  constructor() {
       super();
       this.shadowDOM = this.attachShadow({mode: 'open'});
   }

   connectedCallback() {
       this.mapComponentAttributes();
       this.render();
       this.initComponent();
   }

La primera función es mapComponentAttributes() que nos sirve para definir tantos atributos como necesitemos dentro del componente, por ejemplo, text=’Paradigma’.

mapComponentAttributes() {
       const attributesMapping = [
           'text',
       ];
       attributesMapping.forEach(key => {
           if (!this.attributes[key]) {
               this.attributes[key] = {value: ''};
           }
       });
   }

Otro ejemplo podría ser color=’red’, como se muestra a continuación:

<my-tag color='red' text='Mi componente'></my-tag>

A continuación vamos a implementar la función render() que utilizamos para visualizar el componente. Habiendo asociado anteriormente todos los atributos, podemos acceder a ellos y renderizar el contenido.

render() {
       this.shadowDOM.innerHTML = `
           ${this.templateCss()}
           ${this.template()}
       `;
   }

Para tener organizado el componente, incluimos en su fichero de definición dos funciones que devuelven el código HTML y el código CSS: template() y templateCSS().

 template() {
       return `
           <div class="tag">
               ${this.attributes.text.value}
           </div>
       `;
   }

   templateCss() {
       return `
           <style>
            [...]
            [...]
           </style>
       `;
   }

También nos apoyamos de la función initComponent() para inicializar todas las variables internas, así como referencias para utilizarlas dentro de todo el componente.

 initComponent() {
       this.$tag = this.shadowDOM.querySelector('.tag');
   }

Por último, está disconnectedCallback(), propio del ciclo de vida que sirve para eliminar el componente del DOM.

disconnectedCallback() {
       this.remove();
   }

Una vez creado el componente, lo tenemos que definir y asignar una etiqueta para utilizarlo.

<script type="module">
   import {MyTag} from './my-tag.js';
   window.customElements.define('my-tag', MyTag);
</script>

Cuando ya lo hemos definido, lo podemos usar tantas veces como queramos, por todo nuestro código, sin tener que repetir nada de código, solamente poniendo la etiqueta definida podríamos utilizarlo.

A continuación se muestra un ejemplo de cómo utilizar el componente que hemos creado:

<my-tag text='Hello Webcomponent></mi-tag>
<my-tag text='Este es otro Webcomponent></mi-tag>
<my-tag text='Otro más'></mi-tag>

Y cuál sería su resultado en el navegador:

Herencia

Cuando vas creando componentes terminas por darte cuenta de que gran parte del código se repite y te preguntas si no habrá alguna manera de heredar ciertas partes del componente. Pues sí, las hay y se consigue mediante el uso de extends.

Vamos a crear un componente nuevo y a modificar el que acabamos de crear, heredando de la misma clase.

Además, en esta clase padre, vamos a añadir todos las funciones que son comunes.

schema.js

class Schema extends HTMLElement {
   constructor() {
       super();
       this.shadowDOM = this.attachShadow({mode: 'open'});
   }

   disconnectedCallback() {
       this.remove();
   }

   connectedCallback() {
       this.mapComponentAttributes();
       this.render();
       this.initComponent();
   }

   render() {
       this.shadowDOM.innerHTML = `
           ${this.templateCss()}
           ${this.template()}
       `;
   }

   mapComponentAttributes() {}
   templateCss() {}
   template() {}
   initComponent() {}
}
export default Schema;

Como se puede comprobar, en la nueva clase Schema se han incluido todas las funciones del ciclo de vida del componente MyTag creado anteriormente, además de otras funciones como render(), template(), templateCss() e initComponent() que también van a ser comunes o implementados a posteriori.

A continuación vamos a crear nuevos componentes extendiendo nuestro esquema:

my-tag-extend-schema.js

import Schema from './schema/schema.js';

class MyTagExtendSchema extends Schema {

   initComponent() {
       this.$text = this.shadowDOM.querySelector('.tag');
   }

   template() {
       return `
           <div class="tag">
               ${this.attributes.text.value}
           </div>
       `;
   }

   templateCss() {
       return `
           <style>
              [...]
              [...]
           </style>
       `;
   }

   mapComponentAttributes() {
       const attributesMapping = [
           'text',
        ];
        attributesMapping.forEach(key => {
            if (!this.attributes[key]) {
                this.attributes[key] = {value: ''};
            }
         });
     }
}

export default MyTagExtendSchema;

En el componente MyTag quedaría únicamente su funcionalidad propia.

my-button-extend-schema.js

import Schema from './schema/schema.js';

class MyButtonExtendSchema extends Schema {
   initComponent() {
       this.$buttonElement = this.shadowDOM.querySelector('button');
   }

   template() {
       return `
           <button>${this.attributes.text.value}</button>
       `;
   }

   templateCss(){
       return ``;
   }

   mapComponentAttributes() {
       const attributesMapping = [
           'text',
       ];
       attributesMapping.forEach(key => {
           if (!this.attributes[key]) {
               this.attributes[key] = {value: ''};
           }
       });
   }
}

export default MyButtonExtendSchema; 

De esta manera, hemos definido un botón extendiendo del schema y, como se puede ver, se heredan todas las funciones del mismo, evitando así la duplicidad de código y pudiendo editarlas en único lugar y propagarse automáticamente a todos los componentes que extienden de él.

Setters, getters y constructor

Se pueden crear todo los setters y getters que se necesiten. Son unas herramientas muy útiles para interactuar con el componente, ya sea editando parámetros o recogiendolos.

Nosotros vamos a crear en el componente MyTag un set y un get del text.

 set text(value) {
       this.$text.innerHTML = value;
   }

 get text() {
       return this.$text.innerHTML;
   }

Con este set, lo que hacemos es cambiar el texto de nuestro tag de la siguiente manera:

myTag.text = 'Cambiar el texto';

Directamente se ejecuta el set y se cambiará el atributo con el valor que le indiquemos. Por otro lado, en el get, devolverá el valor del texto.

myTag.text // return 'Cambiar el texto'

Listeners

Se pueden añadir listeners y lanzar tantos eventos como queramos. Vamos a escuchar el evento del click del botón y lanzar un evento nuevo. Para ello, en la función initComponent, se define el evento que queremos escuchar de nuestro componente y se asocia un evento nuevo para que se ejecute.

initComponent() {
       this.$buttonElement = this.querySelector('button');
       this.$buttonElement.addEventListener('click', this.onClickBtn.bind(this));
   }

   onClickBtn() {
       this.dispatchEvent(new CustomEvent('my-event-click', {detail: 'Se ha ejecutado el evento'}));
   }

Ya tenemos escuchando el click en el botón y, al ejecutarlo, nos manda a la función onClickBtn() en la que creamos nuestro propio evento llamado my-event-click y nos devuelve un detalle con un valor. Esto es muy útil si necesitamos recoger valores en los propios eventos.

Para recoger este evento solo tendríamos que escuchar nuestro componente y el custom event que hemos generado, de tal manera que se ejecutará la función que queremos realizar después.

myButton.addEventListener('my-event-click', myFunction());

Conclusión

Desde nuestro punto de vista, utilizar Web Components es apostar por una tecnología agnóstica y que no tiene dependencias de ciclos de vida y evoluciones de los diferentes frameworks disponibles. Por otro lado, su curva de aprendizaje suele ser menor, facilitan la reducción de complejidad y fomentan la reutilización respecto a otras alternativas. Y, lo más importante, son compatibles en cualquier otro proyecto construido con cualquier framework (React, Vue…).

Para finalizar, y no menos importante, nos da la seguridad de que están basados en los estándares web y son soportados por la mayoría de navegadores.

Aquí podéis ver los recursos.

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete

Estamos comprometidos.

Tecnología, personas e impacto positivo.