A lo largo de los últimos cuatro artículos de esta serie de cinco partes, hemos analizado en profundidad las tecnologías que conforman los estándares de componentes web. En primer lugar, analizamos cómo crear plantillas HTML que podrían consumirse en un momento posterior. En segundo lugar, nos sumergimos en la creación de nuestro propio elemento personalizado. Después de eso, encapsulamos los estilos y selectores de nuestro elemento en el DOM de sombra, de modo que nuestro elemento sea completamente autónomo.
Hemos explorado cuán poderosas pueden ser estas herramientas al crear nuestro propio diálogo modal personalizado, un elemento que se puede usar en la mayoría de los contextos de aplicaciones modernas, independientemente del marco o biblioteca subyacente. En este artículo, veremos cómo consumir nuestro elemento en los diversos marcos y veremos algunas herramientas avanzadas para mejorar realmente sus habilidades de componentes web.
Serie de artículos:
- Introducción a los componentes web
- Creación de plantillas HTML reutilizables
- Crear un elemento personalizado desde cero
- Encapsulación de estilo y estructura con Shadow DOM
- Herramientas avanzadas para componentes web (esta publicación)
Agnóstico del marco
Nuestro componente de diálogo funciona muy bien en casi cualquier marco o incluso sin uno. (Por supuesto, si JavaScript está desactivado, todo es en vano). Angular y Vue tratan a los componentes web como ciudadanos de primera clase: los marcos se han diseñado teniendo en cuenta los estándares web. React es un poco más obstinado, pero no imposible de integrar.
Angular
Primero, echemos un vistazo a cómo Angular maneja los elementos personalizados. De forma predeterminada, Angular arrojará un error de plantilla cada vez que encuentre un elemento que no reconoce (es decir, los elementos predeterminados del navegador o cualquiera de los componentes definidos por Angular). Este comportamiento se puede cambiar incluyendo el CUSTOM_ELEMENTS_SCHEMA
.
… Permite que un NgModule contenga lo siguiente:
- Elementos no angulares nombrados con mayúsculas y minúsculas (
-
). - Propiedades del elemento nombradas con mayúsculas y minúsculas (
-
). Dash case es la convención de nomenclatura para elementos personalizados.
– Documentación angular
Consumir este esquema es tan simple como agregarlo a un módulo:
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
@NgModule({
/** Omitted */
schemas: [ CUSTOM_ELEMENTS_SCHEMA ]
})
export class MyModuleAllowsCustomElements {}
Eso es. Después de esto, Angular nos permitirá usar nuestro elemento personalizado donde queramos con la propiedad estándar y los enlaces de eventos:
<one-dialog [open]="isDialogOpen" (dialog-closed)="dialogClosed($event)">
<span slot="heading">Heading text</span>
<div>
<p>Body copy</p>
</div>
</one-dialog>
Vue
La compatibilidad de Vue con los componentes web es incluso mejor que la de Angular, ya que no requiere ninguna configuración especial. Una vez que se registra un elemento, se puede usar con la sintaxis de plantillas predeterminada de Vue:
<one-dialog v-bind:open="isDialogOpen" v-on:dialog-closed="dialogClosed">
<span slot="heading">Heading text</span>
<div>
<p>Body copy</p>
</div>
</one-dialog>
Sin embargo, una advertencia con Angular y Vue son sus controles de formulario predeterminados. Si deseamos usar algo como formas reactivas o [(ng-model)]
en Angular o v-model
en Vue en un elemento personalizado con un control de formulario, necesitaremos configurar esa plomería para la cual está más allá del alcance de este artículo.
Reaccionar
React es un poco más complicado que Angular. El DOM virtual de React toma efectivamente un árbol JSX y lo representa como un objeto grande. Entonces, en lugar de modificar directamente los atributos en elementos HTML como Angular o Vue, React usa una sintaxis de objeto para rastrear los cambios que deben realizarse en el DOM y los actualiza de forma masiva. Esto funciona bien en la mayoría de los casos. El atributo de apertura de nuestro diálogo está vinculado a su propiedad y responderá perfectamente bien a los cambios de accesorios.
El problema viene cuando empezamos a mirar el CustomEvent
enviado cuando se cierra nuestro diálogo. React implementa una serie de detectores de eventos nativos para nosotros con su sistema de eventos sintéticos. Desafortunadamente, eso significa que controles como onDialogClosed
en realidad no adjuntará detectores de eventos a nuestro componente, por lo que tenemos que encontrar otra forma.
El medio más obvio de agregar detectores de eventos personalizados en React es mediante el uso de referencias DOM. En este modelo, podemos hacer referencia a nuestro nodo HTML directamente. La sintaxis es un poco detallada, pero funciona muy bien:
import React, { Component, createRef } from 'react';
export default class MyComponent extends Component {
constructor(props) {
super(props);
// Create the ref
this.dialog = createRef();
// Bind our method to the instance
this.onDialogClosed = this.onDialogClosed.bind(this);
this.state = {
open: false
};
}
componentDidMount() {
// Once the component mounds, add the event listener
this.dialog.current.addEventListener('dialog-closed', this.onDialogClosed);
}
componentWillUnmount() {
// When the component unmounts, remove the listener
this.dialog.current.removeEventListener('dialog-closed', this.onDialogClosed);
}
onDialogClosed(event) { /** Omitted **/ }
render() {
return <div>
<one-dialog open={this.state.open} ref={this.dialog}>
<span slot="heading">Heading text</span>
<div>
<p>Body copy</p>
</div>
</one-dialog>
</div>
}
}
O podemos utilizar componentes funcionales sin estado y ganchos:
import React, { useState, useEffect, useRef } from 'react';
export default function MyComponent(props) {
const [ dialogOpen, setDialogOpen ] = useState(false);
const oneDialog = useRef(null);
const onDialogClosed = event => console.log(event);
useEffect(() => {
oneDialog.current.addEventListener('dialog-closed', onDialogClosed);
return () => oneDialog.current.removeEventListener('dialog-closed', onDialogClosed)
});
return <div>
<button onClick={() => setDialogOpen(true)}>Open dialog</button>
<one-dialog ref={oneDialog} open={dialogOpen}>
<span slot="heading">Heading text</span>
<div>
<p>Body copy</p>
</div>
</one-dialog>
</div>
}
Eso no está mal, pero puede ver cómo reutilizar este componente puede volverse engorroso rápidamente. Afortunadamente, podemos exportar un componente React predeterminado que envuelve nuestro elemento personalizado usando las mismas herramientas.
import React, { Component, createRef } from 'react';
import PropTypes from 'prop-types';
export default class OneDialog extends Component {
constructor(props) {
super(props);
// Create the ref
this.dialog = createRef();
// Bind our method to the instance
this.onDialogClosed = this.onDialogClosed.bind(this);
}
componentDidMount() {
// Once the component mounds, add the event listener
this.dialog.current.addEventListener('dialog-closed', this.onDialogClosed);
}
componentWillUnmount() {
// When the component unmounts, remove the listener
this.dialog.current.removeEventListener('dialog-closed', this.onDialogClosed);
}
onDialogClosed(event) {
// Check to make sure the prop is present before calling it
if (this.props.onDialogClosed) {
this.props.onDialogClosed(event);
}
}
render() {
const { children, onDialogClosed, ...props } = this.props;
return <one-dialog {...props} ref={this.dialog}>
{children}
</one-dialog>
}
}
OneDialog.propTypes = {
children: children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]).isRequired,
onDialogClosed: PropTypes.func
};
… o de nuevo como un componente funcional sin estado:
import React, { useRef, useEffect } from 'react';
import PropTypes from 'prop-types';
export default function OneDialog(props) {
const { children, onDialogClosed, ...restProps } = props;
const oneDialog = useRef(null);
useEffect(() => {
onDialogClosed ? oneDialog.current.addEventListener('dialog-closed', onDialogClosed) : null;
return () => {
onDialogClosed ? oneDialog.current.removeEventListener('dialog-closed', onDialogClosed) : null;
};
});
return <one-dialog ref={oneDialog} {...restProps}>{children}</one-dialog>
}
Ahora podemos usar nuestro diálogo de forma nativa en React, pero seguir manteniendo la misma API en todas nuestras aplicaciones (y aún descartar clases, si eso es lo tuyo).
import React, { useState } from 'react';
import OneDialog from './OneDialog';
export default function MyComponent(props) {
const [open, setOpen] = useState(false);
return <div>
<button onClick={() => setOpen(true)}>Open dialog</button>
<OneDialog open={open} onDialogClosed={() => setOpen(false)}>
<span slot="heading">Heading text</span>
<div>
<p>Body copy</p>
</div>
</OneDialog>
</div>
}
Herramientas avanzadas
Hay una serie de excelentes herramientas para crear sus propios elementos personalizados. La búsqueda a través de npm revela una multitud de herramientas para crear elementos personalizados altamente reactivos (incluido mi propio proyecto favorito), pero el más popular hoy en día es lit-html del equipo de Polymer y, más específicamente para los componentes web, LitElement.
LitElement es una clase base de elementos personalizados que proporciona una serie de API para hacer todas las cosas que hemos recorrido hasta ahora. Se puede ejecutar en un navegador sin un paso de compilación, pero si le gusta usar herramientas de cara al futuro como decoradores, también existen utilidades para eso.
Antes de sumergirse en cómo usar lit o LitElement, tómese un minuto para familiarizarse con los literales de plantilla etiquetados, que son un tipo especial de función llamada en cadenas de literales de plantilla en JavaScript. Estas funciones toman una matriz de cadenas y una colección de valores interpolados y pueden devolver cualquier cosa que desee.
function tag(strings, ...values) {
console.log({ strings, values });
return true;
}
const who = 'world';
tag`hello ${who}`;
/** would log out { strings: ['hello ', ''], values: ['world'] } and return true **/
Lo que nos da LitElement es una actualización dinámica y en vivo de todo lo que se pasa a esa matriz de valores, por lo que, a medida que se actualiza una propiedad, se llamaría a la función de renderización del elemento y se volvería a renderizar el DOM resultante.
import { LitElement, html } from 'lit-element';
class SomeComponent {
static get properties() {
return {
now: { type: String }
};
}
connectedCallback() {
// Be sure to call the super
super.connectedCallback();
this.interval = window.setInterval(() => {
this.now = Date.now();
});
}
disconnectedCallback() {
super.disconnectedCallback();
window.clearInterval(this.interval);
}
render() {
return html`<h1>It is ${this.now}</h1>`;
}
}
customElements.define('some-component', SomeComponent);
Ver la pluma
LitElement ahora ejemplo de Caleb Williams (@calebdwilliams)
en CodePen.
Lo que notará es que tenemos que definir cualquier propiedad que queremos que LitElement observe usando el static properties
adquiridor. El uso de esa API le dice a la clase base que llame a render cada vez que se realice un cambio en las propiedades del componente. render
, a su vez, actualizará solo los nodos que necesiten cambiar.
Entonces, para nuestro ejemplo de diálogo, se vería así usando LitElement:
Ver la pluma
Ejemplo de diálogo usando LitElement por Caleb Williams (@calebdwilliams)
en CodePen.
Hay varias variantes de lit-html disponibles, incluida Haunted, una biblioteca de React hooks-style para componentes web que también puede hacer uso de componentes virtuales usando lit-html como base.
Al final del día, la mayoría de las herramientas modernas de componentes web son varios tipos de lo que LitElement
es: una clase base que abstrae la lógica común de nuestros componentes. Entre los otros sabores se encuentran Stencil, SkateJS, Angular Elements y Polymer.
Que sigue
Los estándares de los componentes web continúan evolucionando y se están discutiendo y agregando nuevas características a los navegadores de manera continua. Pronto, los autores de componentes web tendrán API para interactuar con formularios web a un alto nivel (incluidos otros elementos internos que están más allá del alcance de estos artículos introductorios), como importaciones de módulos HTML y CSS nativos, creación de instancias de plantillas nativas y controles de actualización, y muchos más que se puede rastrear en el tablero de problemas de W3C / componentes web en GitHub.
Estos estándares están listos para adoptar en nuestros proyectos hoy con los polyfills adecuados para navegadores heredados y Edge. Y aunque es posible que no reemplacen el marco de su elección, se pueden utilizar junto con ellos para aumentar los flujos de trabajo de su organización y usted.