Conozca los beneficios de utilizar componentes web, cómo funcionan y cómo empezar a utilizarlos.
Los componentes web (en lo sucesivo, componentes) permiten a los desarrolladores crear sus propios elementos HTML. En esta guía, aprenderá todo lo que hay que saber sobre los componentes. Comenzaremos con cuáles son los componentes, cuáles son sus beneficios y de qué están hechos.
Después de eso, comenzaremos a construir los componentes, primero con plantillas HTML y la interfaz DOM de sombra, luego profundizaremos un poco en el tema y veremos cómo crear un elemento incorporado personalizado.
¿Qué son los componentes?
A los desarrolladores les encantan los componentes (aquí nos referimos a la implementación del patrón de diseño "Módulo"). Esta es una excelente manera de definir un bloque de código que se puede usar en cualquier momento y en cualquier lugar. A lo largo de los años, se han realizado varios intentos más o menos exitosos para poner en práctica esta idea.
El lenguaje de vinculación XML de Mozilla y la especificación de componentes HTML de Microsoft para Internet Explorer 5 fueron hace unos 20 años. Desafortunadamente, ambas implementaciones fueron muy complejas y no interesaron a los fabricantes de otros navegadores, por lo que pronto fueron olvidadas. A pesar de ello, fueron ellos quienes sentaron las bases de lo que hoy tenemos en este ámbito.
Los marcos de JavaScript como React, Vue y Angular adoptan un enfoque similar. Una de las principales razones de su éxito es la capacidad de encapsular la lógica general de la aplicación en algunas plantillas que se mueven fácilmente de un formulario a otro.
Si bien estos marcos mejoran la experiencia de desarrollo, todo tiene un precio. Las características del lenguaje como JSX deben compilarse y la mayoría de los marcos utilizan un motor JavaScript para administrar sus abstracciones. ¿Existe otro enfoque para resolver el problema de dividir el código en componentes? La respuesta son los componentes web.
4 pilares de componentes
Los componentes constan de tres API: elementos personalizados, plantillas HTML y DOM de sombra, así como sus módulos JavaScript subyacentes (módulos ES6). Con las herramientas proporcionadas por estas interfaces, puede crear elementos HTML personalizados que se comportan como sus contrapartes nativas.
Los componentes se utilizan de la misma forma que los elementos HTML normales. Se pueden personalizar usando atributos, recuperar usando JavaScript, diseñar usando CSS. Lo principal es notificar al navegador que existen.
Esto permite que los componentes interactúen con otros marcos y bibliotecas. Al utilizar el mismo mecanismo de comunicación que los elementos regulares, pueden ser utilizados por cualquier marco existente, así como por herramientas que aparecerán en el futuro.
También debe tenerse en cuenta que los componentes cumplen con los estándares web. La web se basa en la idea de compatibilidad con versiones anteriores. Esto significa que los componentes creados hoy funcionarán muy bien durante mucho tiempo.
Echemos un vistazo a cada especificación por separado.
1. Elementos personalizados
Características clave:
- Definición del comportamiento del elemento
- Reaccionar a los cambios de atributos
- Ampliación de elementos existentes
A menudo, cuando la gente habla de componentes, se refiere a la interfaz de elementos personalizados.
Esta API le permite extender elementos definiendo su comportamiento cuando se agregan, actualizan y eliminan.
class ExampleElement extends HTMLElement {
static get observedAttributes() {
return [...]
}
attributeChangedCallback(name, oldValue, newValue) {}
connectedCallback() {}
}
customElements.define('example-element', ExampleElement)
Cada elemento personalizado tiene una estructura similar. Amplía la funcionalidad de la clase HTMLElements existente.
Dentro de un elemento personalizado, existen varios métodos llamados reacciones que son responsables de manejar un cambio particular en un elemento. Por ejemplo, se llama a connectedCallback cuando se agrega un elemento a la página. Esto es similar a las etapas del ciclo de vida utilizadas en los marcos (componentDidMount en React, montado en Vue).
Cambiar los atributos de un elemento conlleva un cambio en su comportamiento. Cuando se produce una actualización, se llama al atributoChangedCallback que contiene información sobre el cambio. Esto solo ocurre para los atributos especificados en la matriz devuelta por ObservedAttributes.
El elemento debe definirse antes de que el navegador pueda utilizarlo. El método "define" toma dos argumentos: el nombre de la etiqueta y su clase. Todas las etiquetas deben contener el carácter "-" para evitar conflictos con elementos nativos existentes y futuros.
<example-element>Content</example-element>
El elemento se puede utilizar como una etiqueta HTML normal. Cuando se encuentra un elemento de este tipo, el navegador asocia su comportamiento con la clase especificada. Este proceso se denomina "actualización".
Hay dos tipos de elementos: "autónomos" y "incorporados personalizados". Hasta ahora, hemos analizado elementos independientes. Estos son elementos que no están relacionados con elementos HTML existentes. Como las etiquetas div y span, que no tienen un significado semántico específico.
Los elementos en línea personalizados, como su nombre indica, amplían la funcionalidad de los elementos HTML existentes. Heredan el comportamiento semántico de estos elementos y pueden cambiarlo. Por ejemplo, si el elemento "entrada" se ha personalizado, seguirá siendo un campo de entrada y parte del formulario cuando se envíe.
class CustomInput extends HTMLInputElement {}
customElements.define('custom-input', CustomInput, { extends: 'input' })
La clase de elemento personalizado en línea extiende la clase de elemento personalizado. Al definir un elemento en línea, el elemento expandible se pasa como tercer argumento.
<input is="custom-input" />
El uso de la etiqueta también es ligeramente diferente. En lugar de una nueva etiqueta, se utiliza la existente, especificando el atributo de extensión especial "es". Cuando el navegador encuentra este atributo, sabe que se trata de un elemento personalizado y lo actualiza en consecuencia.
Si bien la mayoría de los navegadores modernos admiten elementos independientes, los elementos en línea personalizados solo son compatibles con Chrome y Firefox. Cuando se usa en un navegador que no los admite, estos últimos se tratarán como elementos HTML normales, por lo que, en general, son seguros de usar incluso en esos navegadores.
2. Plantillas HTML
- Creación de estructuras prefabricadas.
- No se muestran en la página antes de la llamada.
- Contiene HTML, CSS y JS
Históricamente, la creación de plantillas del lado del cliente ha implicado la concatenación de cadenas en JavaScript o el uso de bibliotecas como Handlebars para analizar bloques de marcado personalizado. Recientemente, la especificación tiene una etiqueta de "plantilla" que puede contener lo que queramos usar.
<template id="tweet">
<div class="tweet">
<span class="message"></span>
Written by @
<span class="username"></span>
</div>
</template>
Por sí solo, no afecta la página de ninguna manera, es decir, el motor no lo analiza, no se envían solicitudes de recursos (audio, video). JavaScript no puede acceder a él y para los navegadores es un elemento vacío.
const template = document.getElementById('tweet')
const node = document.importNode(template.content, true)
document.body.append(node)
Primero obtenemos el elemento "plantilla". El método importNode crea una copia de su contenido, el segundo argumento (verdadero) significa copia profunda. Finalmente, lo agregamos a la página como cualquier otro elemento.
Las plantillas pueden contener cualquier cosa que pueda contener HTML normal, incluidos CSS y JavaScript. Cuando se agrega un elemento a la página, se le aplicarán estilos y se lanzarán los scripts. Recuerde que los estilos y los scripts son globales, lo que significa que pueden sobrescribir otros estilos y valores utilizados por los scripts.
Las plantillas no se limitan a esto. Aparecen en todo su esplendor cuando se utilizan con otras partes de los componentes, en particular el DOM de sombra.
3. DOM de sombra
- Evita conflictos de estilo
- Proponer nombres (de clases, por ejemplo) se vuelve más fácil
- Encapsulando la lógica de implementación
El modelo de objetos de documento (DOM) es la forma en que el navegador interpreta la estructura de la página. Al leer el marcado, el navegador determina qué elementos contienen qué contenido y, basándose en esto, toma una decisión sobre lo que se debe mostrar en la página. Cuando se usa document.getElemetById (), por ejemplo, el navegador accede al DOM para encontrar el elemento que necesita.
Para un diseño de página, esto está bien, pero ¿qué pasa con los detalles ocultos dentro del elemento? Por ejemplo, a una página no le debería importar qué interfaz está contenida dentro del elemento "video". Aquí es donde shadow DOM resulta útil.
<div id="shadow-root"></div>
<script>
const host = document.getElementById('shadow-root')
const shadow = host.attachShadow({ mode: 'open' })
</script>
La sombra DOM se crea cuando se aplica a un elemento. Se puede agregar cualquier contenido al DOM de sombra, al igual que un DOM normal ("ligero"). El DOM de sombra no se ve afectado por lo que sucede afuera, es decir, fuera de ella. El DOM simple tampoco puede acceder a la sombra directamente. Esto significa que en el DOM en la sombra podemos usar cualquier nombre de clase, estilo y scripts y no preocuparnos por posibles conflictos.
Los mejores resultados se obtienen utilizando shadow DOM junto con elementos personalizados. Gracias al DOM de sombra, cuando se reutiliza un componente, sus estilos y estructura no afectan a otros elementos de la página de ninguna manera.
Módulos ES y HTML
- Añadiendo según sea necesario
- No se requiere pre-generación
- Todo se almacena en un solo lugar
Si bien las tres especificaciones anteriores han recorrido un largo camino en su desarrollo, la forma en que se empaquetan y reutilizan sigue siendo un tema de intenso debate.
La especificación HTML Imports define cómo se exportan e importan los documentos HTML, así como CSS y JavaScript. Esto permitiría que los elementos personalizados, junto con las plantillas y el DOM de sombra, se ubiquen en otro lugar y se utilicen según sea necesario.
Sin embargo, Firefox se negó a implementar esta especificación en su navegador y ofreció una forma diferente basada en módulos JavaScript.
export class ExampleElement external HTMLElement {}
import { ExampleElement } from 'ExampleElement.js'
Los módulos tienen su propio espacio de nombres por defecto, es decir su contenido no es global. Las variables, funciones y clases exportadas pueden importarse en cualquier lugar y en cualquier momento y utilizarse como recursos locales.
Esto funciona muy bien para los componentes. Los elementos personalizados que contienen la plantilla y el DOM de sombra se pueden exportar desde un archivo y usar en otro.
import { ExampleElement } from 'ExampleElement.html'
Microsoft ha presentado una propuesta para ampliar la especificación del módulo JavaScript con exportación / importación HTML. Esto le permitirá crear componentes utilizando HTML semántico y declarativo. Esta función llegará pronto a Chrome y Edge.
Creando tu propio componente
Si bien hay muchas cosas sobre los componentes que pueden resultarle complejas, crear y usar un componente simple solo requiere unas pocas líneas de código. Consideremos algunos ejemplos.
Los componentes le permiten mostrar los comentarios de los usuarios utilizando plantillas HTML e interfaces DOM de sombra.
Creemos un componente para mostrar los comentarios de los usuarios usando plantillas HTML y DOM de sombra.
1. Crear una plantilla
El componente necesita una plantilla para copiar antes de generar el marcado. La plantilla se puede ubicar en cualquier lugar de la página, la clase de elemento personalizado puede acceder a ella a través del ID.
Agrega el elemento "plantilla" a la página. Cualquier estilo definido en este elemento solo lo afectará.
<template id="user-comment-template">
<style>
...
</style>
</template>
2. Agregar marcado
Además de los estilos, un componente puede contener un diseño (estructura). Para ello se utiliza el elemento "div".
El contenido dinámico se pasa a través de las ranuras. Agregue espacios para el avatar, el nombre y el mensaje de usuario con los atributos de "nombre" apropiados:
<div class="container">
<div class="avatar-container">
<slot name="avatar"></slot>
</div>
<div class="comment">
<slot name="username"></slot>
<slot name="comment"></slot>
</div>
</div>
Contenido de ranura predeterminado
El contenido predeterminado se mostrará cuando no se pase información a la ranura. Los
datos pasados a la ranura sobrescriben los datos de la plantilla. Si no se pasa información a la ranura, se muestra el contenido predeterminado.
En este caso, si el nombre de usuario no se transfirió, se muestra el mensaje "Sin nombre" en su lugar:
<slot name="username">
<span class="unknown">No name</span>
</slot>
3. Crear una clase
La creación de un elemento personalizado comienza extendiendo la clase HTMLElement. Parte del proceso de configuración es crear una raíz de sombra para representar el contenido del elemento. Lo abrimos para acceder en el siguiente paso.
Finalmente, informamos al navegador sobre la nueva clase UserComment.
class UserComment extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: 'open' })
}
}
customElements.define('user-comment', UserComment)
4. Aplicar contenido de sombra
Cuando el navegador encuentra el elemento "user-comment", mira el nodo raíz de la sombra para recuperar su contenido. El segundo argumento le dice al navegador que copie todo el contenido, no solo la primera capa (elementos de nivel superior).
Agregue marcado al nodo raíz de sombra, que actualiza inmediatamente la apariencia del componente.
connectedCallback() {
const template = document.getElementById('user-comment-template')
const node = document.importNode(template.content, true)
this.shadowRoot.append(node)
}
5. Uso del componente
El componente ahora está listo para usarse. Agrega la etiqueta "user-comment" y pasa la información necesaria.
Dado que todas las ranuras tienen nombres, todo lo que se pase fuera de ellas se ignorará. Todo lo que hay dentro de las ranuras se copia exactamente como se pasó, incluido el estilo.
<user-comment>
<img alt="" slot="avatar" src="avatar.png" />
<span slot="username">Matt Crouch</span>
<div slot="comment">This is an example of a comment</div>
</user-comment>
Código de ejemplo extendido:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Web Components Example</title>
<style>
body {
display: grid;
place-items: center;
}
img {
width: 80px;
border-radius: 4px;
}
</style>
</head>
<body>
<template id="user-comment-template">
<div class="container">
<div class="avatar-container">
<slot name="avatar">
<slot class="unknown"></slot>
</slot>
</div>
<div class="comment">
<slot name="username">No name</slot>
<slot name="comment"></slot>
</div>
</div>
<style>
.container {
width: 320px;
clear: both;
margin-bottom: 1rem;
}
.avatar-container {
float: left;
margin-right: 1rem;
}
.comment {
height: 80px;
display: flex;
flex-direction: column;
justify-content: center;
}
.unknown {
display: block;
width: 80px;
height: 80px;
border-radius: 4px;
background: #ccc;
}
</style>
</template>
<user-comment>
<img alt="" slot="avatar" src="avatar1.jpg" />
<span slot="username">Matt Crouch</span>
<div slot="comment">Fisrt comment</div>
</user-comment>
<user-comment>
<img alt="" slot="avatar" src="avatar2.jpg" />
<!-- no username -->
<div slot="comment">Second comment</div>
</user-comment>
<user-comment>
<!-- no avatar -->
<span slot="username">John Smith</span>
<div slot="comment">Second comment</div>
</user-comment>
<script>
class UserComment extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
}
connectedCallback() {
const template = document.getElementById("user-comment-template");
const node = document.importNode(template.content, true);
this.shadowRoot.append(node);
}
}
customElements.define("user-comment", UserComment);
</script>
</body>
</html>
Crear un elemento en línea personalizado
Como se señaló anteriormente, los elementos personalizados pueden ampliar los existentes. Esto ahorra tiempo al mantener el comportamiento predeterminado del elemento proporcionado por la criadora. En esta sección, veremos cómo puede extender el elemento "tiempo".
1. Creando una clase
Los elementos integrados, como los independientes, aparecen cuando se amplía la clase, pero en lugar de la clase general "HTMLElement", amplían una clase específica.
En nuestro caso, esta clase es HTMLTimeElement, la clase utilizada por los elementos "time". Incluye el comportamiento relacionado con el atributo "fecha y hora", incluido el formato de datos.
class RelativeTime extends HTMLTimeElement {}
2. Definición de elementos
El navegador registra el elemento mediante el método "define". Sin embargo, a diferencia de un elemento independiente, cuando se registra un elemento en línea, el método "define" debe pasar un tercer argumento: un objeto con configuraciones.
Nuestro objeto contendrá una clave con el valor del elemento personalizado. Toma el nombre de la etiqueta. En ausencia de dicha clave, se lanzará una excepción.
customElements.define('relative-time', RelativeTime, { extends: 'time' })
3. Establecer la hora
Dado que podemos tener varios componentes en una página, el componente debe proporcionar un método para establecer el valor de un elemento. Dentro de este método, el componente pasa un valor de tiempo a la biblioteca "timeago" y establece el valor de retorno de esa biblioteca como el valor del elemento (perdón por la tautología).
Finalmente, establecemos el atributo title para que el usuario pueda ver el valor establecido al pasar el mouse.
setTime() {
this.innerHTML = timeago().format(this.getAttribute('datetime'))
this.setAttribute('title', this.getAttribute('datetime'))
}
4. Actualización de la conexión
El componente puede utilizar el método inmediatamente después de mostrarse en la página. Dado que los componentes en línea no tienen un DOM de sombra, no necesitan un constructor.
connectedCAllback() {
this.setTime()
}
5. Seguimiento del cambio de atributos
Si actualiza la hora mediante programación, el componente no responderá. No sabe que tiene que estar atento a los cambios en el atributo "fecha y hora".
Una vez definidos los atributos observados, se llamará a attributeChangedCallback siempre que cambien.
static get observedAttributes() {
return ['datetime']
}
attributeChangedCallback() {
this.setTime()
}
6. Agregar a la página
Dado que nuestro elemento es una extensión del elemento nativo, su implementación es ligeramente diferente. Para usarlo, agregue una etiqueta "tiempo" a la página con un atributo especial "es", cuyo valor es el nombre del elemento integrado definido durante el registro. Los navegadores que no admiten componentes mostrarán contenido de respaldo.
<time is="relative-time" datetime="2020-09-20T12:00:00+0000">
20 2020 . 12:00
</time>
Código de ejemplo extendido:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Web Components Another Example</title>
<!-- timeago.js -->
<script
src="https://cdnjs.cloudflare.com/ajax/libs/timeago.js/4.0.2/timeago.min.js"
integrity="sha512-SVDh1zH5N9ChofSlNAK43lcNS7lWze6DTVx1JCXH1Tmno+0/1jMpdbR8YDgDUfcUrPp1xyE53G42GFrcM0CMVg=="
crossorigin="anonymous"
></script>
<style>
body {
display: flex;
flex-direction: column;
align-items: center;
}
input,
button {
margin-bottom: 0.5rem;
}
time {
font-size: 2rem;
}
</style>
</head>
<body>
<input type="text" placeholder="2020-10-20" value="2020-08-19" />
<button>Set Time</button>
<time is="relative-time" datetime="2020-09-19">
19 2020 .
</time>
<script>
class RelativeTime extends HTMLTimeElement {
setTime() {
this.innerHTML = timeago.format(this.getAttribute("datetime"));
this.setAttribute("title", this.getAttribute("datetime"));
}
connectedCallback() {
this.setTime();
}
static get observedAttributes() {
return ["datetime"];
}
attributeChangedCallback() {
this.setTime();
}
}
customElements.define("relative-time", RelativeTime, { extends: "time" });
const button = document.querySelector("button");
const input = document.querySelector("input");
const time = document.querySelector("time");
button.onclick = () => {
const { value } = input;
time.setAttribute("datetime", value);
};
</script>
</body>
</html>
Espero haberlo ayudado a obtener una comprensión básica de qué son los componentes web, para qué sirven y cómo se utilizan.