UD5 - 2. Componentes¶
Introducción¶
Los componentes web (WebComponents) se pueden usar y replicar en cualquier parte de la aplicación. Si creamos nuestra propia librería de componentes, podemos reutilizarlos en cualquier proyecto.
Incorporan lógica (js), estructura (html) y estilos (css) en un único paquete.
Los componentes se representan en HTML con etiquetas personalizadas, que se pueden usar en cualquier parte de la aplicación.
Referencia MDN: Web Components
El nombre de los componentes debe ser único, para evitar conflictos y deben contener al menos un guion (-).
Principales ventajas:
- Reutilización de código.
- Facilita el mantenimiento.
- Facilita la colaboración entre desarrolladores.
- Diseño consistente.
Veremos cómo usar componentes de terceros y cómo crear nuestros propios componentes.
Uso de librerías de componentes¶
Vamos a ver cómo usar componentes de terceros en nuestra aplicación. En este caso, vamos a usar la librería material-web que implementa WebComponents basados en Material Design. Material Design es un sistema de diseño creado por Google ampliamente utilizado en todo tipo de aplicaciones.
Este apartado servirá como introducción y profundizaremos en detalle cuando veamos Angular Material en la siguiente unidad.
Instalación¶
Para utilizar la librería necesitaremos es muy recomendable el uso de un bundler, como webpack o vite, que facilitará la importación y uso de los módulos de la librería.
Recuerda
Para crear una estructura base para un proyecto con vite utilizamos:
Crea un directorio con el nombre del proyecto, por lo que si quieres que se cree en el directorio actual, debes ejecutar el comando desde el directorio padre.
Una vez creado el proyecto, debemos instalar las dependencias con npm install (o npm i).
Para instalar la librería, ejecutamos el siguiente comando:
Es necesario consultar la documentación oficial, disponible en https://material-web.dev/about/intro/, para ver cómo se usan los componentes.
Ejemplo de uso¶
Siguiendo los ejemplos de la documentación, podemos crear un componente de prueba:
Se definen las etiquetas <md-> que se pueden usar en cualquier parte de la aplicación.
Pero es importante que se carguen los ficheros js de la librería, para que se puedan interpretar las etiquetas personalizadas. Para ello, añadiremos el siguiente código a src/main.js:
Si hacemos un uso intensivo de la librería podemos importar directamente todos los componentes:
Cómo habitualmente el comportamiento de los diálogos es que se muestren bajo demanda, por ejemplo, cuando ocurre un evento, se añade un evento click al botón, que llama al método show() del diálogo. También existe el método close() si necesitamos cerrarlo desde el código.
ACTIVIDAD 1: 📂 UD5/act01/
Sigue la documentación de material-web y construye una pequeña aplicación que dado un input de tipo texto y un botón, al pulsar el botón mostrará un diálogo con el texto escrito en el input.
Por ejemplo, puedes usar los siguiente componentes:
md-filled-text-fieldpara el input.elevated-buttonpara el botón.md-dialogpara el diálogo.
Haz que el diálogo se cierre a los 3 segundos de mostrarse.
Recordatorio de cómo se define un timeout:
Con esto hemos visto una pequeña introducción a material-web, y siguiendo la documentación no deberíamos tener problemas para usarlo en nuestros proyectos. Cómo ya se ha comentado, profundizaremos en detalle cuando veamos Angular Material en la siguiente unidad.
Componentes personalizados (WebComponents nativos)¶
Por otra parte, también podemos crear nuestros propios componentes con el aspecto y comportamiento que necesitemos.
Veamos un ejemplo de creación de un componente simple mediante javascript.
Como la principal ventaja que buscamos es la encapsulación, lo primero que debemos hacer es crear un nuevo archivo, por ejemplo, src/HelloWorld.js, que contendrá toda la estructura, lógica y estilo del componente:
| src/HelloWorld.js | |
|---|---|
- En la primera línea, se define una clase que hereda de
HTMLElement, que es la clase base de todos los elementos HTML. - En la función
connectedCallbackse define el contenido del componente. Esta función se ejecuta cuando el componente se añade al DOM. - Se recomienda el uso de template literals para definir el contenido, dado que no suele ser estático, pueden utilizar variables, eventos, comportamiento, estilos, etc.
- En la última línea, se registra el componente con el nombre
<hello-world>y se asocia con la claseHelloWorldpara que el navegador sepa interpretarlo.
Para poder usarlo en nuestra aplicación, debemos importarlo en el fichero index.html y luego escribir la etiqueta:
De esta forma podemos llamar a esta pieza de código en cualquier parte de nuestra aplicación y tantas veces como haga falta.
Pasar atributos al componente¶
Pero por lo general, los componentes suelen tener un comportamiento algo más complejo. Vamos a pasarle un atributo para que se muestre un saludo personalizado:
| src/HelloWorld.js | |
|---|---|
- En el constructor, se obtiene el valor del atributo
namey se guarda en una propiedad del componente. - En la función
connectedCallbackse usa la propiedadnamepara mostrar el saludo.
Ahora en nuestro html podemos usar el componente de la siguiente forma:
| index.html | |
|---|---|
Shadow DOM, controlando el contenido del componente¶
Puede que hayas notado que el componente define una etiqueta de apertura y otra de cierre, pero no hay contenido entre ellas, de hecho, si añadimos contenido, no se mostrará. Esto es porque el contenido que se añade en el html se sustituye por el contenido que se define en la función connectedCallback.
Para controlar poder controlar el contenido un componente debemos utilizar Shadow DOM, que es una característica de los componentes que permite encapsular el contenido, definiendo un nuevo árbol DOM independiente del DOM principal.
Para poder añadir contenido al componente, declarar debemos usar la etiqueta <slot>
| src/HelloWorld.js | |
|---|---|
Ahora, si añadimos contenido entre las etiquetas <hello-world> y </hello-world> se mostrará en el componente:
Se puede añadir cualquier contenido, incluso otros componentes.
Añadir más de un slot¶
Si necesitamos crear componentes más complejos, podemos usar varios slot, para ello deberemos darle un nombre que los identifique en el componente con la etiqueta name, por ejemplo <slot name="saludo"></slot>. Y en el archivo html, añadir el atributo slot con el mismo nombre que hemos definido en el componente, por ejemplo <div slot="saludo">Buenos días</div>.
Y desde el html podemos usarlos de la siguiente forma, añadiendo el atributo slot a las etiquetas que queremos que se muestren en ese slot:
| index.html | |
|---|---|
Si comprobamos el código anterior, podremos observar que el orden de los <slot> se define en el componente, y es independiente del orden en el que se escriban en el html.
Añadir eventos y estilos¶
Para añadir eventos a los componentes, podemos usar el método addEventListener, igual que lo haríamos con cualquier otro elemento del DOM.
Además, podemos añadir estilos al componente, que se aplicarán al componente y a su contenido, pero no afectarán al resto de la aplicación.
Es interesante utilizar el selector :host que hace referencia al estilo del componente que envuelve al shadow DOM. Es recomendable usarlo para definir propiedades que definan la maquetación global del componente, con propiedades como: display, width, height, margin, padding, border, background, etc.
ACTIVIDAD 2: 📂 UD5/act02/
Crea los siguientes componentes personalizados:
-
mi-card: que muestre un título, un contenido y un pie.- Se le pasará el atributo
titlepara el título. - Tendrá dos slots:
contentyfooter.
Ejemplo:
- Se le pasará el atributo
-
mi-counter: que muestre que muestre un número y dos botones para incrementar y decrementar el número, tendrá los siguientes atributos:initque indicará el valor inicial del contador. Por defecto será 0.minque indicará el valor mínimo del contador. Por defecto será 0.maxque indicará el valor máximo del contador. Por defecto será 10.stepque indicará el incremento/decremento del contador. Por defecto será 1.- Los atributos pueden omitirse, y en ese caso se usarán los valores por defecto.
Ejemplo:
-
Crea dos componentes tipo loader (animación de carga), escoge los que quieras de https://cssloaders.github.io/ y crea un componente para cada uno de ellos.
Cada componente tendrá un atributo
colorque defina el color que por defecto será#fffY otro atributo
speedpara la velocidad de la animación que por defecto será1s.Ponles el nombre que creas más apropiado.
Ejemplo:
En un archivo html, añade los componentes creados con diferentes configuraciones, uno de ellos debe incluir <mi-counter> y un loader dentro de <mi-card>.
Comunicación entre componentes, eventos personalizados¶
En los ejemplos anteriores, hemos creado componentes personalizados, pero pero aún no pueden comunicarse con el exterior.
Para ello, podemos usar eventos personalizados, (unidad 3.1). Vamos a hacer un pequeño repaso.
CustomEvents¶
Para crear un evento personalizado, debemos crear una instancia de la clase CustomEvent y pasarle como parámetro el nombre del evento. Por ejemplo:
Nombre del evento¶
Es una buena práctica es elegir una buena convención de nombres para los eventos, que sea "autoexplicativo" en cuanto la acción que vamos a realizar y a la vez sea coherente y fácil de recordar.
Aunque no hay una forma universal de hacerlo, algunos consejos:
- Los eventos son case sensitive, por lo que es preferible usar todo en minúsculas.
- Evita camelCase, que suele inducir a dudas. Si hemos elegido minúsculas, mejor optar por kebab-case.
- Usar namespaces y elegir un separador: Por ejemplo,
user:data-messageouser.data-message.
Opciones del evento¶
El segundo parámetro, options, del CustomEvent es un objeto donde podremos especificar varios detalles en relación al comportamiento o contenido del evento.
A continuación, se muestra una lista de las propiedades que pueden contener estas opciones:
| Valor | Default | Descripción |
|---|---|---|
detail |
null | Objeto que contiene la información que queremos transmitir. |
bubbles |
false | Indica si el evento debe burbujear en el DOM "hacia la superficie" |
composed |
false | Indica si la propagación puede atravesar Shadow DOM |
cancelable |
false | Indica si el comportamiento se puede cancelar con .preventDefault() |
Ejemplo de uso de eventos personalizados¶
Para enviar un evento personalizado, debemos crear una instancia de la clase CustomEvent y pasarle como parámetro el nombre del evento junto con las opciones, y después enviarlo con dispatchEvent.
const customEvent = new CustomEvent("user:data-message", {
detail: {
from: "Paco",
message: "Hola, ¿qué tal?",
},
bubbles: true,
composed: true
});
dispatchEvent(customEvent);
Para escuchar un evento personalizado, registra addEventListener directamente sobre el elemento que lo va a recibir, o sobre el documento, para capturar todos los eventos personalizados.
document.addEventListener("user:data-message", (event) => {
console.log("Nuevo mensaje de " + event.detail.from);
console.log("Mensaje: " + event.detail.message);
});
El método "mágico" handleEvent¶
Al registrar eventos, ya sean personalizados o no, dentro de una clase, tendremos problemas para acceder al objeto this de la clase, pues el this dentro de la función addEventListener hace referencia al elemento que recibe el evento.
Para solucionar este problema, podemos usar el método handleEvent, que nos permite registrar todos los eventos en un único lugar, y que se encargará de llamar al método correspondiente.
constructor() {
document.getElementById("form").addEventListener("submit", this);
document.addEventListener("user:data-message", this);
}
handleEvent = (event) => {
switch (event.type) {
case "user:data-message":
console.log("Nuevo mensaje de " + event.detail.from);
console.log("Mensaje: " + event.detail.message);
break;
case "submit":
// código para el evento submit
break;
default:
console.warn("evento no manejado", event);
}
};
Ejemplo completo: Componentes para un chat con eventos personalizados¶
Vamos a ilustrar todo lo aprendido hasta el momento con un ejemplo, un chat que tendrá dos componentes:
<chat-input>: Componente que permitirá escribir y enviar un mensaje.- Atributo
user: indicará el nombre del usuario que envía el mensaje. - Atributo
positionque indicará la posición del mensaje, que puede serleftoright. - Enviará un evento personalizado
chat:send-messagecon la información del mensaje.
- Atributo
<chat-messages>: Componente encargado de mostrar los mensajes.- Tendrá un listener para escuchar el evento
chat:send-message, que tomará la información del mensaje y lo guardará en un array. - Cuando se renderice el componente, recorrerá el array de mensajes y mostrará todos los mensajes.
- Tendrá un listener para escuchar el evento
Empecemos por la creación del componente <chat-input>
Ahora vamos a definir el componente que recogerá y mostrará los mensajes:
Y por último, en el archivo html podemos usar los componentes de la siguiente forma:
ACTIVIDAD 3: 📂 UD5/act03/
A partir de los componentes creados en la actividad 2; <mi-card>, <mi-counter> y dos <mi-loader>.
-
Partiendo de
<mi-counter>crea un nuevo componente<mi-counter-emitter>que enviará un evento personalizadocounter:changecada vez que se modifique el valor del contador.Dentro del evento, se enviará el valor actualizado del contador.
-
Partiendo de uno de los loaders, crea un nuevo componente
<mi-loader-handled>que escuchará el eventocounter:changey dependiendo del valor recibido, ajustará la velocidad de la animación.Recuerda que creamos los loaders a partir de https://cssloaders.github.io/, y la velocidad de la animación se define con la propiedad css
animationoanimation-duration, y el valor debe ser un número decimal seguido de la unidads(segundos) oms(milisegundos).Por ejemplo, si el contador es
0, la velocidad será0, si el contador es1, la velocidad será0.1s, si el contador es20, la velocidad será2s, etc. (no se pueden usar valores negativos) -
Muestra un componente
<mi-card>que contenga un<mi-counter-emitter>y un<mi-loader-handled>.
Haz pruebas. Ya sea dentro o fuera de <mi-card>:
- ¿Qué ocurre si añades dos
<mi-loader-handled>? - ¿Y si añades dos
<mi-counter-emitter>?
Librería de creación de componentes Lit¶
Lit es una librería de Google que facilita la creación de WebComponents nativos, reduciendo y simplificando la cantidad de código que hay que escribir, sin sacrificar rendimiento y con una sintaxis muy cercana a a la que acabamos de ver.
No tiene dependencias de terceros y el objetivo es mejorar la experiencia de desarrollador simplificando la creación de WebComponents, lo que permite acelerar la productividad de forma parecida a como lo hacen los frameworks de Javascript, pero enfocándose en el estándar.
AVISO
No vamos a profundizar en el uso esta librería, pero es necesario conocerla, ya que se ha convertido en una de las librerías más populares para crear WebComponents y seguramente la encuentres en algún proyecto.
Con la base que hemos visto en esta unidad, no resultará difícil entender la documentación oficial y empezar a usar Lit.
Si te resulta interesante y quieres saber más, puedes:
- consultar la web lit.dev
- documentación oficial
- realizar el tutorial de introducción.
Características¶
-
Elementos personalizados
Los componentes Lit utilizan la clase
LitElementque extiende la funcionalidad de la claseHTMLElement, por lo que el navegador los trata como elementos nativos. -
Estilos
Aplica sus estilos de forma predeterminada, utilizando Shadow DOM. Esto mantiene los selectores de CSS simples y garantiza que los estilos del componente no afecten (ni se vean afectados por) ningún otro estilo de la página.
-
Propiedades reactivas
Las propiedades reactivas permiten modelar la API y el estado interno de su componente. Un componente Lit se vuelve a renderizar de manera eficiente cada vez que cambia una propiedad reactiva (o el atributo HTML correspondiente).
-
Plantillas declarativas
Utiliza plantillas basadas en template strings, son simples, expresivas y rápidas, y permiten marcado HTML junto expresiones Javascript nativas. No es necesario aprender ninguna sintaxis ni se requiere compilación.