Como encapsular funcionalidad de componentes visuales utilizando Web Components

16 de noviembre de 2022

by Christina Victoria Craft
Photo by Christina Victoria Craft on Unsplash

Escribir componentes reutilizables es una de las tantas prácticas que nos ayudan a escribir código más limpio y fácil de mantener en nuestros proyectos.

Es muy usual que en nuestros proyectos nos vemos con la necesidad de tener un listado de componentes visuales que se van a utilizar en diferentes partes de la aplicación, por ejemplo, un componente de botón, un componente de label, un componente de input, entre otros.

En muchos casos, utilizamos librerías y frameworks como React para crear los components visuales y encapsular esta funcionalidad de forma que podamos reutilizarla en diferentes partes de la aplicación. Esto hace mucho sentido cuando utilizamos React también en nuestra aplicación, pero muchas veces no es el caso.

Para ello, existe un estandar conocido como Web Components, que que es una serie de diferentes tecnologías que nos permiten crear componentes reutilizables, encapsulados e interoperables que se pueden utilizar en cualquier aplicación web.

#.Web Components

Los Web Components son bloques de código encapsulan una estructura central de HTML, incluyendo sus estilos y sus funcionalidades de JavaScript.

El uso de ellos persigue dos objetivos principales:

  1. Encapsular funcionalidad de componentes visuales, al ser componentes totalmente aislados entre ellos. Esto permite protegerlos de efectos que no deseamos al implementarlos junto a otros componentes.
  2. Interoperar entre diferentes componentes, como compartir información o eventos entre ellos.

El estándar se compone de tres tecnologías principales:

  1. Custom Elements: Es una API que nos permite definir nuestros propios elementos HTML personalizados.
  2. Shadow DOM: Es una API que nos permite crear un DOM virtual y encapsulado dentro de un elemento HTML.
  3. HTML Templates: Es una API que nos permite crear plantillas HTML que podemos reutilizar en diferentes partes de nuestra aplicación.

Analicémoslos uno a uno y con un poco más de detalle.

#.Custom Elments

Los Custom Elements (elementos personalizados) son una forma de crear etiquetas HTML personalizadas que nos permiten encapsular funcionalidad de componentes visuales, entre ellos el HTML o el estilo de CSS.

Un ejemplo de Custom Element es el siguiente:

<my-button>Click me!</my-button>

❗ Uno de los requerimientos de ellos es que deben llevar un guión en el nombre de la etiqueta, por ejemplo, my-button, my-label, my-input, etc.

#.Shadow DOM

El Shadow DOM (DOM en la sombra) es una API que nos permite crear un DOM virtual y encapsulado dentro de un elemento HTML. El objetivo es crear una estructura aislada, independiente y privada dentro de un elemento HTML, sin que afecte el resto del documento (o del DOM).

🤔 ¿Por qué esto es útil? Porque de esta manera podemos crear estructuras con estilos y funcionalidades que no afecten a otros elementos HTML, ya que son locales para el elemento

#.HTML Template

Los HTML Templates son una forma de crear plantillas HTML que podemos reutilizar en diferentes partes de nuestra aplicación. Este contenido es inerte y no se renderiza hasta que no es necesario su funcionamiento.

#.Resolviendo un problema de uniformidad

Un uso muy común para los Web Components es cuando tenemos una aplicación que utiliza distintos frameworks o librerías para la creación de las interfaces visuales. Digamos que parte de nuestro proyecto utiliza React y otra parte utiliza Vue, por ejemplo.

¿Cómo mantenemos uniformidad entre ambas partes? ¿Cómo encapsulamos la funcionalidad de los componentes visuales para que sean reutilizables en cualquier parte de la aplicación, incluso si no utilizamos el mismo framework en todas estas partes?

Bueno, primero esto es un caso donde se hace obvio que debemos crear un Design System para nuestra aplicación, el tema es cómo escribirlo para que podamos soportar cualquier otra librería donde se implemente:

  1. Podríamos crear componentes planos en HTML y CSS, como lo hace el Gobierno de Reino Unido, donde importamos las hojas de CSS y la funcionalidad de forma independiente (además de seguir los patrones ya definidos).
  2. O, también podríamos crear Web Components que nos permitan encapsular todo lo relacionado a como mostrar esos componentes, permitiéndonos encapsular la funcionalidad, marcado y estilo.

Esto lo podemos hacer en JavaScript/HTML/CSS plano o podemos utilizar una librería, como Svelte, que nos ayuda a crear esta funcionalidad.

#.Creando Web Components con Svelte

Primero, vamos a crear un nuevo proyecto. Para ello, vamos a utilizar Vite y Svelte:

npm init @vitejs/app svelte-web-components --template svelte
cd svelte-web-components
yarn

Este proyecto base tiene algunos componentes y scripts ya creados, por ejemplo podemos ejecutar nuestra aplicación base con:

yarn dev

El comando que utilizaríamos para compilar todo sería:

yarn build

Vamos a ver que esto crea dentro de ./dist/assets/ un archivo index.(...).js y si abrimos el archivo veremos que es un archivo JavaScript que contiene todo el código de nuestra aplicación, pero empaquetado como un módulo.

Vamos a alterar la forma en que nuestros componentes se compilan, para ello vamos a la configuración de vite, en ./vite.config.js y vamos a agregar la siguiente configuración:

export default defineConfig({
  plugins: [
    svelte({
      compilerOptions: {
        customElement: true,
      },
    }),
  ],
})

Esta configuración nos permite crear componentes que se pueden utilizar como Custom Elements. Por defecto, Svelte compila los componentes como módulos de JavaScript, pero con esta configuración podemos crear componentes que se pueden utilizar como Custom Elements. En la configuración inicial, Svelte ha creado ya un archivo llamado Counter.svelte, para indicarle cuál es la etiqueta que vamos a utilizar para este componente, vamos a agregar un atributo tag al componente:

<!-- Counter.svelte -->
<svelte:options tag="my-counter" />

Y hacemos lo mismo en el App.svelte:

<!-- App.svelte -->
<svelte:options tag="my-app" />

Ahora, vamos a crear un nuevo componente, llamado Button.svelte:

<!-- Button.svelte -->
<svelte:options tag="my-button" />

<script>
  export let label = ""
</script>

<button>
  {label}
</button>

Para poder utilizarlos, debemos modificar el main.js para que importe los componentes que vamos a utilizar:

// main.js
// ...
import Button from './lib/Button.svelte'

Y dentro de nuestro componente App.svelte vamos a utilizar el componente Button, pero vamos a utilizarlo como un Custom Element:

<!-- ... -->

<my-button label="My Button" />

Esto nos generará algo como lo siguiente:

img1

El componente que se muestra como “My Button” es, de hecho, un Custom Element, que se ha creado a partir de nuestro componente Button.svelte.

#.Estilizando el botón

Para estilizar el botón, vamos a editar el componente Button.svelte y le agregaremos una hoja de estilo al mismo:

<svelte:options tag="my-button" />

<script>
  export let label = ""
</script>

<style>  .my-button {    background-color: rgb(249 115 22);    border: none;    border-radius: 0.25em;    color: white;    cursor: pointer;    font-size: 1.25em;    font-weight: bold;    padding: 0.5em 1em;  }  .my-button:hover {    background-color: rgb(154 52 18);  }</style>
<button class="my-button">  {label}
</button>

Si notamos los cambios, podemos ver que el <button> ahora se le aplica la clase .my-button, a la cual le creamos un estilo por defecto. Lo más interesante sobre esto es que si agregamos un <button></button> en el App.svelte, este no tendrá el estilo que le hemos dado al componente Button.svelte, ya que este estilo solo se aplica a los componentes que se crean a partir de Button.svelte:

<my-button label="My Button" />
<button class="my-button">Something</button>

El resultado sería el siguiente:

img2

El primer componente es nuestro Custom Element, estilizado con nuestros estilos propios para ese elemento (y no afecta a otros elementos <button>), mientras que el segundo elemento <button> no tiene estilos, ya que no se ha creado a partir de nuestro componente Button.svelte.

#.Probando nuestro Custom Element fuera de Svelte

Vamos a hacer una prueba extremadamente simple, pero que explica el concepto. Para ello, vamos primero a compilar nuestro proyecto:

yarn build

Esto nos generará un directorio ./dist/ con todos los archivos necesarios para poder utilizar nuestra aplicación. Entre estos archivos vamos a ver un archivo JavaScript, llamado index.(...).js, que contiene todo el código de nuestra aplicación, incluyendo nuestro nuevo componente de botón.

En un directorio aparte, vamos a crear un archivo index.html y vamos a mover el archivo compilado de nuestro proyecto. Igualmente vamos a copiar el archivo compilado index.(...).js a este directorio y lo vamos a llamar index.js, para que sea más fácil de referenciar:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>My Custom Element</title>
  </head>
  <body>
    <my-button label="My Button"></my-button>    <script src="./index.js"></script>  </body>
</html>

Si abrimos este nuevo archivo index.html en nuestro explorador, vamos a ver que el botón se muestra correctamente:

img3

Todos los estilos y propiedades de la etiqueta ya vienen configurados para nosotros y lo mejor es que vienen encapsulados en la etiqueta para mejor control.

Imaginemos ahora que, en vez de ser un archivo plano index.html lo estuviéramos llamando desde alguna otra librería como React o Vue. Al ser un Custom Element, podemos utilizarlo sin ningún problema, ya que es una etiqueta HTML que se puede utilizar en cualquier lugar. Lo único es que tendríamos que requerir o importar el archivo index.js para poder utilizarlo.

Si estamos desarrollando un Design System Library, podríamos tener un monorepo con distintos paquetes internamente que se referencien entre ellos:

  • @my-design-system/web: Contiene los componentes básicos de la librería, como botones, inputs, etc., pero en forma de Web Components
  • @my-design-system/react: Contiene los componentes básicos de la librería, pero en forma de React Components, que internamente referencian y utilizan los Web Components de @my-design-system/web.
  • @my-design-system/vue: Lo mismo que @my-design-system/react, pero para Vue.

Profile picture

Escrito por Demóstenes García G. (@demogar) Ingeniero de Software y desarrollador web y móvil, basado en Ciudad de Panamá.