Partes de un WebComponent

En este artículo vamos a analizar las partes de un WebComponent para comprender mejor su funcionamiento. En principio, nuestra etiqueta no es más que una etiqueta HTML propia, definida por el programador, que puede tener su propio funcionamiento personalizado.

Al cargar la página, si hemos registrado un custom element en el navegador (una clase asociada a su etiqueta personalizada), todas las etiquetas HTML que utilicen dicho nombre, pasan a ser actualizadas como custom element. Es entonces donde empiezan a actuar como componentes.

WebComponents: Lifecycle

Componente

Como cualquier etiqueta HTML, tiene una serie de características que existen en cualquier tipo de etiqueta HTML como podrían ser las clases y los ids, los atributos o los estilos en línea, entre otras:

<app-element atributo="valor"></app-element>

Sin embargo, en un componente los atributos juegan un papel importantísimo, ya que pueden existir atributos específicos con una misión concreta, habitualmente, pasar información a la lógica del componente.

Nota: A diferencia de muchos frameworks Javascript, el estándar de WebComponent indica que sólo se puede pasar información de texto a través de sus atributos. Sin embargo, ya veremos que las propiedades de Javascript si que pueden almacenar datos más complejos.

Clase del componente

Como habíamos visto hasta el momento, un componente básico (más concretamente, un custom element) mínimo tendría la siguiente estructura, donde el constructor sólo se especifica si tiene lógica en su interior:

class AppElement extends HTMLElement {

  constructor() {
    super();
  }

}

customElements.define("app-element", AppElement);

Sin embargo, como toda estructura de clase de programación, podemos establecer propiedades de clase (miembros) para guardar información, así como métodos de clase (funciones de clase) para separar y dividir correctamente la lógica de nuestra clase.

Propiedades del componente

En Javascript, las propiedades o miembros se añaden desde el constructor, añadiendo la palabra clave this. antes del nombre del miembro (con el que estamos haciendo referencia al propio custom element particular). Por defecto, en Javascript las propiedades son públicas:

class AppElement extends HTMLElement {
  constructor() {
    super();
    this.name = "Manz";
    this.life = 5;
  }
}

Como añadido, en Javascript se introduce la posibilidad de definir propiedades fuera del constructor, en la parte superior de la clase. Además, al igual que otros lenguajes de programación como Java, podemos definir miembros privados, aunque con una sintaxis diferente: añadiendo siempre el carácter # antes del nombre de la propiedad.

Sin embargo, hay que tener mucho cuidado al utilizar esta sintaxis. En el momento de escribir este artículo, Firefox no la soporta y Safari la comenzará a soportar en su próxima versión: private class fields.

class AppElement extends HTMLElement {

  name = "Manz";
  life = 5;
  #role = "Developer";

  constructor() {
    super();
    this.life = 10;
    this.#role = "JS Developer";
  }
}

En la parte superior de la clase vemos como se declararían las propiedades, tanto públicas como privadas, y en el interior del constructor (o cualquier otro método), vemos como podríamos modificarlas si lo necesitáramos, incluyendo el this. antes del nombre.

Nota: Las propiedades privadas siempre deben declararse en la parte superior de la clase, mientras que las propiedades públicas no es obligatorio que se declaren en dicha zona.

Si intentamos acceder desde fuera de la clase a las propiedades públicas, podremos hacerlo, mientras que si intentamos acceder a las propiedades privadas (incluyendo el #), obtendremos un mensaje de error similar al siguiente:

Uncaught SyntaxError: Private field 'role' must be declared in an enclosing class

Recuerda que también puedes definir propiedades estáticas, con las que permites acceder a ellas sin necesidad de crear una instancia de la clase, sino de forma directa.

Métodos del componente

De la misma forma que la clase del custom element puede tener propiedades, también puede tener métodos, y pueden ser públicos o privados. Siguen la misma mecánica que las propiedades que vimos anteriormente:

class AppElement extends HTMLElement {

  name = "Manz";

  test() {
    console.log("Este método es público.");
  }

  #privateTest() {
    console.log("Este método es privado.");
  }
}

En el caso de tener un custom element del componente anterior en el HTML, podríamos incluso ejecutar uno de sus métodos, utilizando un evento:

<app-element onClick="this.test()"></app-element>

En este caso, this hace referencia a la instancia del elemento, por lo que podríamos aprovechar para ejecutar métodos públicos. Los métodos privados sin embargo, sólo se podrán ejecutar desde el interior de la clase.

Por su parte, los métodos estáticos se podrán ejecutar sin necesidad de crear elementos personalizados, sino ejecutándolos sobre la clase (siempre y cuando se tenga acceso a ella de alguna forma).

Ciclo de vida de un componente

Durante la carga de una página y el tiempo en el que la utilizamos, los WebComponent pasan por una serie de fases que se conocen como el ciclo de vida del WebComponent. Para entender bien este ciclo de vida lo mejor es conocer una serie de métodos que tenemos a nuestra disposición:

WebComponents: Lifecycle Diagram

Estos métodos especiales los podemos definir dentro de la clase del componente. Dichos métodos no se llaman manualmente (como los que creamos los desarrolladores), sino que son unos métodos que se disparan automáticamente cuando el componente llega a una fase concreta de su «ciclo de vida».

Básicamente, el componente puede pasar por las siguientes fases:

Característica Descripción
constructor() Se ha creado un custom element concreto, ya definido en el registro.
connectedCallback() El custom element se ha conectado al DOM del documento HTML. !
disconnectedCallback() El custom element se ha desconectado del DOM del documento HTML. !
adoptedCallback() El custom element se mueve a un nuevo documento (Común en iframes).
attributeChangedCallback() Se ha modificado un atributo observado del custom element.

Inicialización

El método constructor() en un WebComponent tiene la misma función que en una clase. Se ejecutará cada vez que se cree un custom element particular, que previamente haya sido definido en el registro con customElements.define(). Por lo tanto, si creamos varias instancias de un componente, se ejecutará por cada una de ellas.

Esto ocurrirá de forma automática si el custom element ya existía en el documento HTML antes de definirlo en el registro, o de forma manual cuando creamos el elemento con document.createElement() o con un new AppElement().

El constructor sólo suele hacer tareas de inicialización, listeners de eventos o creación de Shadow DOM (lo veremos más adelante), sin olvidar el super(), ya que extendemos de HTMLElement.

Importante: Es esencial que en el constructor del componente se hagan sólo las tareas más importantes: de inicialización y ligeras. Como norma general, si es posible aplazar lógica al método connectedCallback(), mejor.

En el caso de no evitarse y se incluya lógica en el constructor que pueda llegar a modificar el DOM o los atributos de un componente, podrían aparecernos errores como el siguiente:

Uncaught DOMException: Failed to construct 'CustomElement': The result must not have children

Conexión al DOM

El método connectedCallback() es llamado cada vez que insertas un custom element en la página, por lo que es interesante para ciertas tareas de nuestra lógica. Ten en cuenta que este método no es una inicialización como constructor(), sino una inicialización más tardía, que se ejecutará cuando vaya a formar parte del documento HTML y, probablemente, a renderizarse.

Ten en cuenta que si un custom element es movido a otra parte del DOM, se desconectará y volverá a conectarse al DOM, pasando por los métodos correspondientes.

Desconexión del DOM

Por contrapartida, el método disconnectedCallback() es el opuesto de connectedCallback(). Mientras que este último es llamado cuando insertas un custom element en la página, el primero es llamado cuando es eliminado del DOM de la página HTML.

Puede ser realmente útil para descargar de tareas importantes que están consumiendo recursos respecto a ese elemento así como tareas de finalización.

Adopción de nodos

El método adoptedCallback() es de un uso menos frecuente, se dispara cuando un custom element se mueve de un documento HTML a otro, utilizando el método .adoptNode() de la API de Javascript. Algo que puede ser útil, por ejemplo, trabajando con elementos <iframe>.

Atributos

Por norma general, los atributos de nuestro custom element se utilizarán para pasar información desde el exterior al propio componente. Esta información puede ser de tipo textual o simplemente no tener valor y existir sólo para indicar alguna característica de verdadero o falso.

Por ejemplo, observemos el siguiente ejemplo:

<app-element value="15" isEnabled></app-element>
  • El atributo value contiene un valor numérico 15, sin embargo, ten en cuenta que los atributos siempre se reciben como texto .

  • El atributo isEnabled aunque no especifíca un valor, contiene un de tamaño 0, pero suele utilizarse como booleano .

Métodos de atributos (DOM)

Para trabajar con los atributos de un elemento HTML estándar, tenemos los siguientes métodos (los cuales podemos utilizar también para custom elements):

Métodos Descripción
.hasAttributes() ¿El elemento tiene atributos?
.getAttributeNames() Devuelve un de atributos (lowercase).
.hasAttribute(name) ¿El atributo name existe?
.getAttribute(name) Devuelve el valor de name o si no existe.
.removeAttribute(name) Elimina el atributo name.
.setAttribute(name,value) Fija el atributo name a value.
.toggleAttribute(name,force) Si existe, lo elimina y viceversa.

Como en nuestra clase, this es una referencia al propio custom element, podemos utilizar estos métodos de trabajo sobre etiquetas habituales en nuestros componentes, ya que estamos extendiendo de HTMLElement.

Veamos una posible implementación donde utilicemos algunos de estos métodos:

class AppElement extends HTMLElement {

  constructor() {
    super();
    console.log("Este componente tiene los atributos: ", this.getAttributeNames());
    console.log("El valor del atributo «value» es ", this.getAttribute("value"));
    console.log("¿El atributo «isEnabled» existe? ", this.hasAttribute("isEnabled"));
  }
}

Algunas consideraciones interesantes:

  • El método getAttributeNames() devuelve un array con todos los atributos del elemento. Los strings están en minúsculas (lowercase).

  • El método .getAttribute() devuelve cuando no existe el atributo indicado. Si existe pero no tiene valor, devuelve una cadena de texto vacía.

  • El método .setAttribute() establecerá el atributo con el valor como , aunque se le pasen otros. Si el valor establecido es cadena vacía, simplemente se añadirá el atributo, sin valor.

  • El método .toggleAttribute() añade un atributo si no existía previamente, o lo elimina si ya existía previamente. Si añadimos el segundo parámetro force , simplemente forzaremos a añadir o eliminar el atributo, sin tener en cuenta su estado previo. Este método devuelve si el atributo, tras las operaciones realizadas, existe o no.

Observación de atributos

Los WebComponents incorporan una interesante forma de detectar cambios en los atributos del custom element de forma automática, para que así podamos crear lógica que reaccione a dichos cambios.

Por defecto, si establecemos un valor inicial al atributo de un componente y durante el transcurso de la sesión del usuario lo modificamos, no sabremos cuando ha cambiado el valor, ya que tendríamos que consultarlo de forma manual para tener la información actualizada.

Característica Descripción
static get observedAttributes() Observa atributos para notificar cambios.
attributeChangedCallback(name,old,now) Se dispara cuando cambian.

En WebComponents podemos utilizar el getter estático observedAttributes(), que deberá devolver un con los nombres de los atributos que queremos observar. De esta forma, los atributos que estén siendo observados, cada vez que cambien, se disparará un método especial denominado attributeChangedCallback().

Detección de cambios

El método attributeChangedCallback() es un método especial que se dispara cuando un atributo observado ha sido modificado. El método nos pasará por parámetro el nombre del atributo en name, así como el valor que tenía antes en old y el valor que tiene actualmente en now.

Veamos una posible implementación de este funcionamiento:

class AppElement extends HTMLElement {

  static get observedAttributes() {
    return ["value", "isEnabled"];
  }

  attributeChangedCallback(name, old, now) {
    console.log(`El atributo ${name} ha sido modificado de ${old} a ${now}.`);
  }
}

De esta forma, cada vez que el valor de los atributos observados cambien, se lanzará el método attributeChangedCallback() con los valores específicos en sus parámetros.

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.