El Santo Grial de los componentes reutilizables: elementos personalizados, Shadow DOM y NPM

Publicado: 2022-03-10
Resumen rápido ↬ Este artículo analiza el aumento de HTML con componentes que tienen funciones y estilos integrados. También aprenderemos cómo hacer que estos elementos personalizados sean reutilizables en proyectos que usan NPM.

Incluso para los componentes más simples, el costo en mano de obra humana puede haber sido significativo. Los equipos de UX realizan pruebas de usabilidad. Una serie de partes interesadas tienen que aprobar el diseño.

Los desarrolladores realizan pruebas AB, auditorías de accesibilidad, pruebas unitarias y comprobaciones entre navegadores. Una vez que haya resuelto un problema, no querrá repetir ese esfuerzo . Al construir una biblioteca de componentes reutilizables (en lugar de construir todo desde cero), podemos utilizar continuamente los esfuerzos anteriores y evitar volver a visitar los desafíos de diseño y desarrollo ya resueltos.

Una captura de pantalla del sitio web de componentes materiales de Google, que muestra varios componentes.
Vista previa grande

Crear un arsenal de componentes es particularmente útil para empresas como Google, que poseen una cartera considerable de sitios web que comparten una marca común. Al codificar su interfaz de usuario en widgets componibles, las empresas más grandes pueden acelerar el tiempo de desarrollo y lograr la consistencia del diseño visual y de interacción del usuario en todos los proyectos. Ha habido un aumento en el interés por las guías de estilo y las bibliotecas de patrones en los últimos años. Dados los múltiples desarrolladores y diseñadores repartidos en varios equipos, las grandes empresas buscan lograr la coherencia. Podemos hacerlo mejor que simples muestras de color. Lo que necesitamos es código fácilmente distribuible .

Compartir y reutilizar código

Copiar y pegar manualmente el código es sencillo. Sin embargo, mantener ese código actualizado es una pesadilla de mantenimiento. Muchos desarrolladores, por lo tanto, confían en un administrador de paquetes para reutilizar el código en todos los proyectos. A pesar de su nombre, Node Package Manager se ha convertido en la plataforma sin rival para la gestión de paquetes front-end . Actualmente hay más de 700 000 paquetes en el registro de NPM y se descargan miles de millones de paquetes cada mes. Cualquier carpeta con un archivo package.json se puede cargar en NPM como un paquete que se puede compartir. Si bien NPM se asocia principalmente con JavaScript, un paquete puede incluir CSS y marcado. NPM facilita la reutilización y, lo que es más importante, la actualización del código. En lugar de tener que modificar el código en innumerables lugares, cambia el código solo en el paquete.

¡Más después del salto! Continúe leyendo a continuación ↓

El problema del marcado

Sass y Javascript son fácilmente portátiles con el uso de declaraciones de importación. Los lenguajes de plantillas le dan a HTML la misma capacidad: las plantillas pueden importar otros fragmentos de HTML en forma de parciales. Puede escribir el marcado para su pie de página, por ejemplo, solo una vez, luego incluirlo en otras plantillas. Decir que existe una multiplicidad de lenguajes de plantillas sería quedarse corto. Atarse a uno solo limita severamente la reutilización potencial de su código. La alternativa es copiar y pegar marcas y usar NPM solo para estilos y javascript.

Este es el enfoque adoptado por el Financial Times con su biblioteca de componentes de Origami . En su charla "¿No puedes simplemente hacerlo más como Bootstrap?" Alice Bartlett concluyó que "no hay una buena manera de permitir que las personas incluyan plantillas en sus proyectos". Hablando sobre su experiencia de mantener una biblioteca de componentes en Lonely Planet, Ian Feather reiteró los problemas con este enfoque:

“Una vez que copian ese código, esencialmente están cortando una versión que debe mantenerse indefinidamente. Cuando copiaron el marcado para un componente de trabajo, tenía un enlace implícito a una instantánea del CSS en ese punto. Si luego actualiza la plantilla o refactoriza el CSS, debe actualizar todas las versiones de la plantilla repartidas por su sitio”.

Una solución: componentes web

Los componentes web resuelven este problema al definir el marcado en JavaScript. El autor de un componente es libre de modificar el marcado, CSS y Javascript. El consumidor del componente puede beneficiarse de estas actualizaciones sin necesidad de rastrear a mano el código de modificación del proyecto. La sincronización con los últimos cambios en todo el proyecto se puede lograr con una breve npm update a través de la terminal. Solo el nombre del componente y su API deben permanecer consistentes.

Instalar un componente web es tan simple como escribir npm install component-name en una terminal. El Javascript se puede incluir con una declaración de importación:

 <script type="module"> import './node_modules/component-name/index.js'; </script>

Luego puede usar el componente en cualquier lugar de su marcado. Aquí hay un componente de ejemplo simple que copia texto al portapapeles.

Vea la demostración del componente web Pen Simple de CSS GRID (@cssgrid) en CodePen.

Vea la demostración del componente web Pen Simple de CSS GRID (@cssgrid) en CodePen.

Un enfoque centrado en componentes para el desarrollo front-end se ha vuelto omnipresente, introducido por el marco React de Facebook. Inevitablemente, dada la omnipresencia de los marcos en los flujos de trabajo front-end modernos, varias empresas han creado bibliotecas de componentes utilizando el marco de su elección. Esos componentes son reutilizables solo dentro de ese marco particular.

Un componente del Carbon Design System de IBM
Un componente del Carbon Design System de IBM. Solo para uso en aplicaciones React. Otros ejemplos significativos de bibliotecas de componentes integradas en React incluyen Atlaskit de Atlassian y Polaris de Shopify. (Vista previa grande)

Es raro que una empresa importante tenga un front-end uniforme y cambiar la plataforma de un marco a otro no es raro. Los marcos van y vienen. Para habilitar la cantidad máxima de reutilización potencial entre proyectos, necesitamos componentes que sean independientes del marco .

Una captura de pantalla de npmjs.com que muestra componentes que hacen lo mismo creados exclusivamente para marcos de JavaScript particulares.
La búsqueda de componentes a través de npmjs.com revela un ecosistema de Javascript fragmentado. (Vista previa grande)
Un gráfico que muestra la popularidad de los marcos a lo largo del tiempo. Ember, Knockout y Backbone han perdido popularidad y han sido reemplazados por ofertas más nuevas.
La popularidad siempre cambiante de los marcos a lo largo del tiempo. (Vista previa grande)
“Desarrollé aplicaciones web usando: Dojo, Mootools, Prototype, jQuery, Backbone, Thorax y React a lo largo de los años... Me encantaría haber podido llevar ese componente Dojo increíble que trabajé conmigo a mi React. aplicación de hoy.”

— Dion Almaer, director de ingeniería, Google

Cuando hablamos de un componente web, estamos hablando de la combinación de un elemento personalizado con shadow DOM. Los elementos personalizados y shadow DOM son parte tanto de la especificación W3C DOM como del estándar WHATWG DOM, lo que significa que los componentes web son un estándar web . Los elementos personalizados y el DOM en la sombra finalmente están configurados para lograr la compatibilidad con todos los navegadores este año. Al utilizar una parte estándar de la plataforma web nativa, nos aseguramos de que nuestros componentes puedan sobrevivir al ciclo acelerado de reestructuración de front-end y replanteamientos arquitectónicos. Los componentes web se pueden usar con cualquier lenguaje de plantillas y cualquier marco de front-end: son verdaderamente compatibles e interoperables. Se pueden usar en todas partes, desde un blog de Wordpress hasta una aplicación de una sola página.

El proyecto Custom Elements Everywhere de Rob Dodson documenta la interoperabilidad de los componentes web con varios marcos Javascript del lado del cliente.
El proyecto Custom Elements Everywhere de Rob Dodson documenta la interoperabilidad de los componentes web con varios marcos Javascript del lado del cliente. React, el valor atípico aquí, con suerte resolverá estos problemas con React 17. (Vista previa grande)

Hacer un componente web

Definición de un elemento personalizado

Siempre ha sido posible inventar nombres de etiquetas y hacer que su contenido aparezca en la página.

 <made-up-tag>Hello World!</made-up-tag>

HTML está diseñado para ser tolerante a fallas. Lo anterior se procesará, aunque no sea un elemento HTML válido. Nunca ha habido una buena razón para hacer esto: desviarse de las etiquetas estandarizadas ha sido tradicionalmente una mala práctica. Sin embargo, al definir una nueva etiqueta usando la API de elementos personalizados, podemos aumentar HTML con elementos reutilizables que tienen una funcionalidad integrada. Crear un elemento personalizado es muy parecido a crear un componente en React, pero aquí estamos extendiendo HTMLElement .

 class ExpandableBox extends HTMLElement { constructor() { super() } }

Una llamada sin parámetros a super() debe ser la primera declaración en el constructor. El constructor debe usarse para configurar el estado inicial y los valores predeterminados y para configurar cualquier detector de eventos. Es necesario definir un nuevo elemento personalizado con un nombre para su etiqueta HTML y la clase correspondiente de los elementos:

 customElements.define('expandable-box', ExpandableBox)

Es una convención poner en mayúscula los nombres de las clases. Sin embargo, la sintaxis de la etiqueta HTML es más que una convención. ¿Qué pasaría si los navegadores quisieran implementar un nuevo elemento HTML y quisieran llamarlo caja expandible? Para evitar colisiones de nombres, ninguna nueva etiqueta HTML estandarizada incluirá un guión. Por el contrario, los nombres de los elementos personalizados deben incluir un guión.

 customElements.define('whatever', Whatever) // invalid customElements.define('what-ever', Whatever) // valid

Ciclo de vida de elementos personalizados

La API ofrece cuatro reacciones de elementos personalizados: funciones que se pueden definir dentro de la clase que se llamarán automáticamente en respuesta a ciertos eventos en el ciclo de vida de un elemento personalizado.

connectedCallback se ejecuta cuando el elemento personalizado se agrega al DOM.

 connectedCallback() { console.log("custom element is on the page!") }

Esto incluye agregar un elemento con Javascript:

 document.body.appendChild(document.createElement("expandable-box")) //“custom element is on the page”

así como simplemente incluir el elemento dentro de la página con una etiqueta HTML:

 <expandable-box></expandable-box> // "custom element is on the page"

Cualquier trabajo que implique obtener recursos o renderizar debe estar aquí.

desconectadoCallback se ejecuta cuando el elemento personalizado se elimina del DOM.

 disconnectedCallback() { console.log("element has been removed") } document.querySelector("expandable-box").remove() //"element has been removed"

adoptedCallback se ejecuta cuando el elemento personalizado se adopta en un nuevo documento. Probablemente no necesite preocuparse por esto con demasiada frecuencia.

attributeChangedCallback se ejecuta cuando se agrega, cambia o elimina un atributo. Se puede usar para escuchar los cambios en los atributos nativos estandarizados como disabled o src , así como los personalizados que creamos. Este es uno de los aspectos más poderosos de los elementos personalizados, ya que permite la creación de una API fácil de usar.

Atributos de elementos personalizados

Hay una gran cantidad de atributos HTML. Para que el navegador no pierda el tiempo llamando a nuestro attributeChangedCallback cuando se cambia cualquier atributo, debemos proporcionar una lista de los cambios de atributos que queremos escuchar. Para este ejemplo, solo estamos interesados ​​en uno.

 static get observedAttributes() { return ['expanded'] }

Así que ahora nuestro attributeChangedCallback solo se llamará cuando cambiemos el valor del atributo expandido en el elemento personalizado, ya que es el único atributo que hemos enumerado.

Los atributos HTML pueden tener valores correspondientes (piense en href, src, alt, value, etc.) mientras que otros son verdaderos o falsos (por ejemplo , deshabilitado, seleccionado, requerido ). Para un atributo con un valor correspondiente, incluiríamos lo siguiente dentro de la definición de clase del elemento personalizado.

 get yourCustomAttributeName() { return this.getAttribute('yourCustomAttributeName'); } set yourCustomAttributeName(newValue) { this.setAttribute('yourCustomAttributeName', newValue); }

Para nuestro elemento de ejemplo, el atributo será verdadero o falso, por lo que definir el captador y el definidor es un poco diferente.

 get expanded() { return this.hasAttribute('expanded') } // the second argument for setAttribute is mandatory, so we'll use an empty string set expanded(val) { if (val) { this.setAttribute('expanded', ''); } else { this.removeAttribute('expanded') } }

Ahora que se ha solucionado el repetitivo, podemos hacer uso de attributeChangedCallback .

 attributeChangedCallback(name, oldval, newval) { console.log(`the ${name} attribute has changed from ${oldval} to ${newval}!!`); // do something every time the attribute changes }

Tradicionalmente, la configuración de un componente de Javascript implicaba pasar argumentos a una función de init . Al utilizar el attributeChangedCallback , es posible crear un elemento personalizado que se pueda configurar solo con el marcado.

Shadow DOM y los elementos personalizados se pueden usar por separado, y puede encontrar elementos personalizados útiles por sí mismos. A diferencia de shadow DOM, se pueden polillenar. Sin embargo, las dos especificaciones funcionan bien en conjunto.

Adjuntar marcas y estilos con Shadow DOM

Hasta ahora, hemos manejado el comportamiento de un elemento personalizado. Sin embargo, en lo que respecta al marcado y los estilos, nuestro elemento personalizado es equivalente a un <span> vacío sin estilo. Para encapsular HTML y CSS como parte del componente, debemos adjuntar un DOM oculto. Es mejor hacer esto dentro de la función constructora.

 class FancyComponent extends HTMLElement { constructor() { super() var shadowRoot = this.attachShadow({mode: 'open'}) shadowRoot.innerHTML = `<h2>hello world!</h2>` }

No se preocupe por comprender lo que significa el modo: debe incluirlo, pero casi siempre querrá open . Este componente de ejemplo simple solo representará el texto "hola mundo". Como la mayoría de los demás elementos HTML, un elemento personalizado puede tener elementos secundarios, pero no de forma predeterminada. Hasta ahora, el elemento personalizado anterior que hemos definido no mostrará ningún elemento secundario en la pantalla. Para mostrar cualquier contenido entre las etiquetas, necesitamos hacer uso de un elemento de slot .

 shadowRoot.innerHTML = ` <h2>hello world!</h2> <slot></slot> `

Podemos usar una etiqueta de estilo para aplicar algo de CSS al componente.

 shadowRoot.innerHTML = `<style> p { color: red; } </style> <h2>hello world!</h2> <slot>some default content</slot>`

Estos estilos solo se aplicarán al componente, por lo que podemos utilizar selectores de elementos sin que los estilos afecten nada más en la página. Esto simplifica la escritura de CSS, haciendo innecesarias las convenciones de nomenclatura como BEM.

Publicación de un componente en NPM

Los paquetes de NPM se publican a través de la línea de comandos. Abra una ventana de terminal y muévase a un directorio que le gustaría convertir en un paquete reutilizable. Luego escriba los siguientes comandos en la terminal:

  1. Si su proyecto aún no tiene un paquete.json, npm init lo guiará para generar uno.
  2. npm adduser vincula su máquina a su cuenta de NPM. Si no tiene una cuenta preexistente, creará una nueva para usted.
  3. npm publish
Los paquetes de NPM se publican a través de la línea de comandos
Vista previa grande

Si todo salió bien, ahora tiene un componente en el registro de NPM, listo para ser instalado y utilizado en sus propios proyectos, y compartido con el mundo.

Un ejemplo de un componente en el registro de NPM, listo para ser instalado y utilizado en sus propios proyectos.
Vista previa grande

La API de componentes web no es perfecta. Actualmente, los elementos personalizados no pueden incluir datos en los envíos de formularios. La historia de la mejora progresiva no es genial. Lidiar con la accesibilidad no es tan fácil como debería ser.

Aunque se anunció originalmente en 2011, la compatibilidad con navegadores todavía no es universal. El soporte de Firefox está previsto para finales de este año. Sin embargo, algunos sitios web de alto perfil (como Youtube) ya los están utilizando. A pesar de sus deficiencias actuales, para los componentes compartibles universalmente son la opción singular y en el futuro podemos esperar interesantes adiciones a lo que tienen para ofrecer.