En este artículo crearemos un completo elemento personalizado (custom element), que incluye trabajo con Shadow DOM y uso de slots, usando el estándar Javascript Web Components.
Estamos actualizando el Manual de Web Components a la versión más reciente y definitiva del estándar de Javascript, Web Components V1, ya que inicialmente se escribió para la versión previa (V0) que actualmente ya no está soportada. Además lo estamos ampliando para agregar nuevos ejemplos como el que nos ocupa en este artículo.
En este ejercicio vamos a realizar un componente nativo de un botón con animación, que además es capaz de atender a diversos estados. Básicamente es un botón que, cuando se pulsa, crea una pequeña animación y que además tiene un atributo llamado "status" que permite actualizar el aspecto del botón, de modo que de un poco de feedback visual al usuario.
En este ejercicio queremos repasar:
- El método de crear shadow DOM
- Novedades del método del ciclo de vida para el atributo "attributeChangedCallback"
- Cómo usar templates nativos de Javascript, gracias a ES6 Template Strings.
- Cómo usar slot para reutilizar el contenido del tag host
Realizaremos estos ejemplos de componentes usando únicamente Javascript nativo.
Creación de la clase para implementar el componente
Una de las novedades del estándar Web Components V1 es que usa clases (de programación orientada a objetos) para implementar los componentes. La clase puede extender cualquier elemento nativo del HTML, pero lo común será extender HTMLElement.
Nuestra clase tendrá este aspecto:
class BotonStatus extends HTMLElement {
// implementar el componente
}
Luego registramos el componente con el nombre de la etiqueta, que tiene que contener un guión, y el nombre de la clase usada para implementarlo.
window.customElements.define('boton-status', BotonStatus);
Creación de un template con ES6 template strings
Podemos aprovechar una de las herramientas más útiles de ES6 como son los template strings para la creación del template del componente. Esto nos permite interpolar variables o propiedades del componente de una manera muy sencilla, que además produce un código muy legible.
Para facilitar la utilización del template dentro del componente me voy a apoyar en un método getter de Javascript, lo que me permitirá usar este template dentro de la clase del componente, tal como se usaría una propiedad común.get template() {
return `
<style>
div {
display: inline-block;
color: #fff;
border-radius: 3px;
padding: 10px;
cursor:pointer;
outline:none;
animation-duration: 0.3s;
animation-timing-function: ease-in;
background-color: #000;
}
div:active{
animation-name: anim;
}
@keyframes anim {
0% {transform: scale(1);}
10%, 40% {transform: scale(0.7) rotate(-1.5deg);}
100% {transform: scale(1) rotate(0);}
}
.neutral {
background-color: #888;
}
.danger {
background-color: #d66;
}
.success {
background-color: #3a6;
}
</style>
<div class="${this.status}"><slot></slot></div>
`;
}
La parte más interesante del template lo tenemos en la línea siguiente:
<div class="${this.status}"><slot></slot></div>
Aquí está interesante apreciar como se ha embutido el valor de la propiedad status del componente. Esta propiedad pertenece al objeto botón, una vez instanciado. Además en breve veremos cómo poblar esa propiedad con el valor introducido en el atributo "status", indicado al usar el componente.
Trabajo con slots
Además, muy interesante tambén es la etiqueta SLOT
, que sirve para colocar en este punto el contenido que tenga la etiqueta del componente.
Por ejemplo, al usar el componente podemos tener algo como esto:
<boton-status>Haz clic aquí</boton-status>
El texto "Haz clic aquí" será lo que se introduzca dentro del template gracias a la etiqueta SLOT.
Cómo crear Shadow DOM
El constructor de la clase es el lugar más apropiado para construir el Shadow DOM del componente. Además es el lugar donde se deben inicializar las propiedades y donde podemos hacer otras cosas, como el acceso a los atributos seteados en la etiqueta del componente, para inicializar las propiedades con ellos.
En este caso vamos a tener el siguiente constructor:
constructor() {
super();
let currentStatus = this.getAttribute('status');
if(currentStatus) {
this.status = currentStatus;
} else {
this.status = 'neutral';
}
let shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = this.template;
}
- El constructor debe llamar a super() como primer paso, para invocar a cualquier constructor de la clase padre. Esto es super importante para que todo funcione correctamente.
- Inicializamos la propiedad "this.status" con el valor del atributo status, que hemos obtenido con this.getAttribute('status'). Sin embargo, si el atributo no estaba definido en la etiqueta, simplemente lo inicializamos con un valor adecuado, en nuestro caso la cadena "neutral".
- Luego creamos el shadow DOM con el método this.attachShadow y agregamos el template dentro del shadowDOM gracias su propiedad inner.HTML.
Cómo reaccionar a los cambios en los atributos de la etiqueta del componente
Ahora nos queda hacer que nuestro componente sea reactivo y que pueda actualizar su estado cada vez que el atributo "status" de la etiqueta del componente cambie.
Esto lo tenemos que hacer con el método del ciclo de vida "attributeChangedCallback", que recibe el nombre del método, con sus valores anterior y nuevo.
attributeChangedCallback(attr, oldVal, newVal) {
console.log('attributeChangedCallback');
if(attr == 'status' && oldVal != newVal) {
this.status = newVal;
console.log(this.status);
this.shadowRoot.innerHTML = this.template;
}
}
En este ejemplo estamos reaccionando cuando el atributo actualizado sea "status" y cuando el contenido antiguo sea distinto que el nuevo seteado (aunque esta comprobación quizás sea un poco innecesaria, porque el método del ciclo de vida sólo debería invocarse cuando realmente haya cambios).
En caso que se detecten cambios lo que hacemos es actualizar el valor de la propiedad this.status y a continuación hacer que se renderice de nuevo el template, asignando la propiedad this.template a el innerHTML del shadowRoot. Esto provocará que se procese de nuevo el contenido de todo el template y se asigne como Shadow DOM.
Solo que nos falta un detalle muy importante, por motivos de optimización, el estándar de Web Components V1 nos obliga a crear un método getter llamado "observedAttributes", en el que tenemos que devolver un array de los atributos que en verdad se desean observar.
static get observedAttributes() {
return ['status'];
}
De este modo, nuestro Javascript solamente estará pendiente del atributo "status", para invocar al attributeChangedCallback() solamente cuando éste cambie. De este modo conseguiremos que attributeChangedCallback() se ejecute solamente cuando es estrictamente necesario.
Usando el componente personalizado
Con esto hemos terminado nuestro web component, que será capaz de mostrarse con un estilo determinado, dependiendo de su atributo status (estilos válidos serán "neutral" en color gris, "success" en color verde y "danger" en color rojo, aparte de que se usará el color negro para cualquier status desconocido).
El componente lo ideal es que lo guardemos en un archivo con extensión .js, con el mismo nombre que el nombre del componente. En este caso sería "boton-status.js". El código completo sería el siguiente:
class BotonStatus extends HTMLElement {
constructor() {
super();
let currentStatus = this.getAttribute('status');
if(currentStatus) {
this.status = currentStatus;
} else {
this.status = 'neutral';
}
let shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = this.template;
}
static get observedAttributes() {
return ['status'];
}
attributeChangedCallback(attr, oldVal, newVal) {
console.log('attributeChangedCallback');
if(attr == 'status' && oldVal != newVal) {
this.status = newVal;
console.log(this.status);
this.shadowRoot.innerHTML = this.template;
}
}
get template() {
return `
<style>
div {
display: inline-block;
color: #fff;
border-radius: 3px;
padding: 10px;
cursor:pointer;
outline:none;
animation-duration: 0.3s;
animation-timing-function: ease-in;
background-color: #000;
}
div:active{
animation-name: anim;
}
@keyframes anim {
0% {transform: scale(1);}
10%, 40% {transform: scale(0.7) rotate(-1.5deg);}
100% {transform: scale(1) rotate(0);}
}
.neutral {
background-color: #888;
}
.danger {
background-color: #d66;
}
.success {
background-color: #3a6;
}
</style>
<div class="${this.status}"><slot></slot></div>
`;
}
}
window.customElements.define('boton-status', BotonStatus);
Ahora, en cualquier página donde se pretenda usar lo tenemos que incluir como script:
<script src="boton-status.js"></script>
Y luego utilizar la etiqueta del componente, con los status que deseemos:
<boton-status>Haz clic aquí</boton-status>
<boton-status status="danger">No hagas clic aquí</boton-status>
<boton-status status="success">Haz clic aquí para tener éxito!</boton-status>
Eso es todo, hemos podido crear un componente nativo, completamente basado en las características de Web Components V1 y sin necesidad de usar ninguna librería Javascript.
Recuerda que puedes aprender mucho más en el Manual de Web Components.
Miguel Angel Alvarez
Fundador de DesarrolloWeb.com y la plataforma de formación online EscuelaIT. Com...