Cree componentes web interoperables que funcionen trucos CSS
Aquellos de nosotros que hemos sido desarrolladores web durante más de unos pocos años, probablemente hemos escrito código utilizando más de un marco de JavaScript. Con todas las opciones: React, Svelte, Vue, Angular, Solid, todo es inevitable. Una de las cosas más frustrantes con las que tenemos que lidiar cuando trabajamos en diferentes contextos es recrear todos esos componentes de interfaz de usuario de bajo nivel: botones, pestañas, menús desplegables, etc. Lo que es particularmente decepcionante es que generalmente los definiremos en un marco, digamos React, pero luego tenemos que reescribirlos si queremos construir algo en Svelte. O Vue. O Sólido. Y así.
¿No sería mejor si pudiéramos definir estos componentes de interfaz de usuario de bajo nivel una vez, de forma independiente del marco, y luego reutilizarlos entre marcos? ¡Por supuesto que lo harías! Y podemos; los componentes web son el camino. Esta publicación le mostrará cómo.
Por ahora, falta un poco el historial de SSR para los componentes web. La sombra declarativa DOM (DSD) es la forma en que el servidor muestra el componente web, pero en el momento de escribir este artículo no está integrado con sus marcos de aplicaciones favoritos, como Next, Remix o SvelteKit. Si este es un requisito para usted, asegúrese de verificar el estado DSD más reciente. Pero de lo contrario, si SSR no es algo que usa, siga leyendo.
Primero, un poco de contexto
Los componentes web son esencialmente elementos HTML que usted mismo define, como <yummy-pizza>
o lo que sea, desde el principio. Están cubiertos aquí en CSS-Tricks (incluida una extensa serie de Caleb Williams y una de John Rea), pero repasaremos el proceso brevemente. Esencialmente, define una clase de JavaScript, la hereda de HTMLElement
y luego defina cualquier propiedad, atributo y estilo que tenga el componente web y, por supuesto, el etiquetado que finalmente proporcionará a sus usuarios.
La capacidad de definir elementos HTML personalizados que no están vinculados a ningún componente en particular es emocionante. Pero esta libertad es también una limitación. La existencia independientemente de cualquier marco de JavaScript significa que realmente no puede interactuar con estos marcos de JavaScript. Piense en un componente React que recupera algunos datos y luego muestra algunos otros Reaccionar componente mediante la transmisión de datos Esto realmente no funcionará como un componente web porque el componente web no sabe cómo mostrar un componente React.
Los componentes web se distinguen especialmente como componentes de la hoja. componentes de la hoja son lo último que se muestra en el árbol de componentes. Estos son los componentes que reciben algunos accesorios y representan algunos interfaz de usuario. Estos son no los componentes ubicados en el medio de su árbol de componentes transmiten datos, establecen contexto, etc. - simplemente limpie partes de interfaz de usuario esto se verá igual sin importar qué marco de JavaScript impulse el resto de la aplicación.
El componente web que construimos
En lugar de crear algo aburrido (y común), como un botón, construyamos algo un poco diferente. En mi última publicación, analizamos el uso de vistas previas de imágenes borrosas para evitar la reconversión de contenido y proporcionar una interfaz de usuario decente para los usuarios mientras se cargan nuestras imágenes. Analizamos base64, que codifica versiones borrosas y degradadas de nuestras imágenes y las muestra en nuestra interfaz de usuario mientras se carga la imagen real. También buscamos generar visualizaciones increíblemente compactas y borrosas usando una herramienta llamada Borrón.
Esta publicación le mostró cómo generar estas visualizaciones y usarlas en un proyecto de React. Esta publicación le mostrará cómo usar estas vistas previas de componentes web para que puedan ser utilizadas por todos los tipos Marco JavaScript.
Pero tenemos que hacerlo antes de que podamos ejecutarlo, así que primero pasaremos por algo trivial y estúpido para ver exactamente cómo funcionan los componentes web.
Todo en esta publicación construirá componentes web estándar sin ninguna herramienta. Esto significa que el código tendrá una plantilla pequeña, pero debería ser relativamente fácil de seguir. Herramientas como Iluminado. o Modelo están diseñados para crear componentes web y se pueden usar para eliminar gran parte de esta plantilla. ¡Os animo a echarles un vistazo! Pero para este post, preferiría un poco más de plantilla a cambio de no tener que introducir y enseñar otra dependencia.
Un componente de contador simple
Construyamos el clásico "Hello World" a partir de componentes de JavaScript: contador. Mostraremos un valor y un botón que aumenta este valor. Simple y aburrido, pero nos permitirá mirar el componente web más simple posible.
Para construir un componente web, el primer paso es crear una clase de JavaScript que herede de HTMLElement
:
class Counter extends HTMLElement {}
El último paso es registrar el componente web, pero solo si aún no lo hemos hecho. registrado por:
if (!customElements.get("counter-wc")) {
customElements.define("counter-wc", Counter);
}
Y, por supuesto, retratarlo:
<counter-wc></counter-wc>
Y todo lo demás es que hacemos que el componente web haga lo que queremos. Un método común del ciclo de vida es connectedCallback
que se activa cuando nuestro componente web se agrega al DOM. Podemos usar este método para mostrar cualquier contenido que queramos. Recuerde que esta es una clase JS heredada de HTMLElement
que significa nuestro this
value es el elemento mismo del componente web, con todos los métodos normales de manipulación de DOM que ya conoce y ama.
Simplemente, podemos hacer esto:
class Counter extends HTMLElement {
connectedCallback() {
this.innerHTML = "<div style="color: green">Hey</div>";
}
}
if (!customElements.get("counter-wc")) {
customElements.define("counter-wc", Counter);
}
... que funcionará bien.
Agregar contenido real
Agreguemos contenido útil e interactivo. Necesitamos una <span>
para conservar el valor numérico actual y un <button>
para aumentar el contador. Por ahora, crearemos este contenido en nuestro constructor y lo agregaremos cuando el componente web esté realmente en el DOM:
constructor() {
super();
const container = document.createElement('div');
this.valSpan = document.createElement('span');
const increment = document.createElement('button');
increment.innerText="Increment";
increment.addEventListener('click', () => {
this.#value = this.#currentValue + 1;
});
container.appendChild(this.valSpan);
container.appendChild(document.createElement('br'));
container.appendChild(increment);
this.container = container;
}
connectedCallback() {
this.appendChild(this.container);
this.update();
}
Si está realmente preocupado por la creación manual de un DOM, recuerde que puede establecer innerHTML
o incluso cree un elemento de plantilla una vez como una propiedad estática de la clase de su componente web, clónelo e inserte los contenidos para nuevas instancias del componente web. Probablemente hay algunas otras opciones que no puedo pensar, o siempre puede usar un marco de componente web como Iluminado. o ModeloPero para esta publicación, continuaremos manteniéndolo simple.
Para continuar, necesitamos una propiedad personalizada de una clase de JavaScript llamada value
#currentValue = 0;
set #value(val) {
this.#currentValue = val;
this.update();
}
Esta es solo una propiedad estándar de la clase establecida, junto con una segunda propiedad para mantener el valor. Un giro divertido es que utilizo la sintaxis de las propiedades de la clase privada de JavaScript para estos valores. Esto significa que nadie fuera de nuestro componente web puede tocar sus valores. Este es JavaScript estándar que es compatible con todos los navegadores modernosasí que no tengas miedo de usarlo.
O no dudes en llamarlo _value
si tu prefieres. Finalmente, el nuestro update
método:
update() {
this.valSpan.innerText = this.#currentValue;
}
¡Funciona!
Obviamente, este no es un código que le gustaría mantener a escala. aqui esta lleno ejemplo de trabajo si quieres echar un vistazo más de cerca. Como dije, herramientas como Lit y Stencil están diseñadas para hacer esto más fácil.
Añadir más funcionalidad
Esta publicación no es una inmersión profunda en los componentes web. No cubriremos todas las API y ciclos de vida; ni siquiera taparemos las raíces a la sombra o ranuras. Hay un sinfín de contenidos sobre estos temas. Mi objetivo aquí es proporcionar una introducción lo suficientemente decente para despertar cierto interés, junto con algunas pautas útiles para hacerlo. usando componentes web con marcos JavaScript populares que ya conoce y ama.
Con este fin, mejoremos un poco nuestro componente web contador. Supongamos un color
atributo para controlar el color del valor que se muestra. Y aceptemos también una increment
propiedad para que los usuarios de este componente web puedan aumentarlo en 2, 3, 4 a la vez. Y para administrar estos cambios en el estado, usemos nuestro nuevo contador en Sandbox Svelte: llegaremos a Reaccionar en un momento.
Comenzaremos con el mismo componente web que antes y agregaremos un atributo de color. Para configurar nuestro componente web para aceptar y hacer coincidir un atributo, agregamos uno estático observedAttributes
una propiedad que devuelve los atributos en los que escucha nuestro componente web.
static observedAttributes = ["color"];
Con esto en su lugar, podemos agregar un attributeChangedCallback
ciclo de vida que se ejecutará cada vez que cualquiera de los atributos enumerados en observedAttributes
están configurados o actualizados.
attributeChangedCallback(name, oldValue, newValue) {
if (name === "color") {
this.update();
}
}
Ahora estamos actualizando el nuestro. update
método para usarlo realmente:
update() {
this.valSpan.innerText = this._currentValue;
this.valSpan.style.color = this.getAttribute("color") || "black";
}
Finalmente, agreguemos el nuestro. increment
Propiedad:
increment = 1;
Sencillo y modesto.
Usando el componente de contador en Svelte
Usemos lo que acabamos de hacer. Ingresaremos a nuestro componente de la aplicación Svelte y agregaremos algo similar:
<script>
let color = "red";
</script>
<style>
main {
text-align: center;
}
</style>
<main>
<select bind:value={color}>
<option value="red">Red</option>
<option value="green">Green</option>
<option value="blue">Blue</option>
</select>
<counter-wc color={color}></counter-wc>
</main>
¡Y funciona! Nuestro contador muestra, se acerca y el menú desplegable actualiza el color. Como puede ver, mostramos el atributo de color en nuestra plantilla Svelte, y cuando el valor cambia, Svelte hace el trabajo de llamar setAttribute
de nuestra instancia principal del componente web. No tiene nada de especial: es lo mismo que ya hace con los atributos de todos los tipos elemento HTML.
Las cosas se ponen un poco interesantes con increment
apoyo Esto es no atributo de nuestro componente web; esta es la columna vertebral de la clase de componente web. Esto significa que debe establecerse en la instancia del componente web. No lo dudes, ya que las cosas se volverán mucho más simples en poco tiempo.
Primero, agregaremos algunas variables a nuestro componente Svelte:
let increment = 1;
let wcInstance;
Nuestra central eléctrica de contraparte te permitirá aumentar en 1 o 2:
<button on:click={() => increment = 1}>Increment 1</button>
<button on:click={() => increment = 2}>Increment 2</button>
Pero, En teorianecesitamos obtener la instancia real de nuestro componente web. Esto es lo mismo que hacemos cada vez que agregamos ref
con Reaccionar. Con Svelte es simple bind:this
directiva:
<counter-wc bind:this={wcInstance} color={color}></counter-wc>
Ahora, en nuestra plantilla Svelte, escuchamos cambios en la variable para aumentar nuestro componente y establecemos la propiedad principal del componente web.
$: {
if (wcInstance) {
wcInstance.increment = increment;
}
}
Puedes probarlo en esta demostración en vivo.
Obviamente, no queremos hacer esto para cada componente web o accesorio que necesitemos administrar. ¿No sería bueno si pudiéramos preguntar increment
directamente en nuestro componente web, en el marcado, como solemos hacer para los accesorios de componentes, y lo tenemos, ya sabes, solo trabajoEn otras palabras, sería bueno si pudiéramos eliminar todos los usos de wcInstance
y use este código más simple en su lugar:
<counter-wc increment={increment} color={color}></counter-wc>
Resulta que podemos. Este código funciona; Svelte hace todo este trabajo por nosotros. Véalo en esta demostración. Este es un comportamiento estándar para casi todos los marcos de JavaScript.
Entonces, ¿por qué les mostré la forma manual de ajustar el soporte del componente web? Dos razones: es útil entender cómo funcionan estas cosas, y acabo de decir que funciona para "casi" todos los marcos de JavaScript. Pero hay un framework que, locamente, no soporta la configuración del componente web, como acabamos de ver.
React es una bestia diferente
Reaccionar. El marco de JavaScript más popular del planeta no admite la interacción básica con los componentes web. Este es un problema bien conocido que es exclusivo de React. Curiosamente, esto se corrigió en la rama experimental de React, pero por alguna razón no se fusionó en la versión 18. Dicho esto, aún podemos seguir su progreso. Y puedes probar esto tú mismo con un demo en vivo.
La solución, por supuesto, es utilizar un ref
tome la instancia del componente web y configúrelo manualmente increment
cuando este valor cambia. Se parece a eso:
import React, { useState, useRef, useEffect } from 'react';
import './counter-wc';
export default function App() {
const [increment, setIncrement] = useState(1);
const [color, setColor] = useState('red');
const wcRef = useRef(null);
useEffect(() => {
wcRef.current.increment = increment;
}, [increment]);
return (
<div>
<div className="increment-container">
<button onClick={() => setIncrement(1)}>Increment by 1</button>
<button onClick={() => setIncrement(2)}>Increment by 2</button>
</div>
<select value={color} onChange={(e) => setColor(e.target.value)}>
<option value="red">Red</option>
<option value="green">Green</option>
<option value="blue">Blue</option>
</select>
<counter-wc ref={wcRef} increment={increment} color={color}></counter-wc>
</div>
);
}
Como comentamos, codificar esto manualmente para cada propiedad de un componente web simplemente no es escalable, pero no todo está perdido porque tenemos varias opciones.
Opción 1: usar atributos en todas partes
Tenemos atributos. Si hace clic en la demostración de React anterior, increment
prop no funcionó, pero el color cambió correctamente. ¿No podemos codificar todo con atributos? Lamentablemente no. Los valores de los atributos solo pueden ser cadenas. Eso es lo suficientemente bueno aquí y podríamos ir un poco más allá con este enfoque. increment
se puede convertir a y desde cadenas. Incluso podemos secuenciar JSON/comprender objetos. Pero al final tendremos que pasar una función a un componente web y llegados a este punto no nos quedaremos sin opciones.
Opción 2: Envuélvelo
Hay un viejo dicho que dice que puedes resolver cualquier problema en informática agregando un nivel de indireccionalidad (excepto por el problema con demasiados niveles de indireccionalidad). El código para configurar estos accesorios es bastante predecible y simple. ¿Y si lo escondemos en la Gente Inteligente detrás de Lit. hay una soluciónEsta biblioteca crea un nuevo componente React para usted después de que le proporcione un componente web y enumere las propiedades que necesita. Aunque inteligente, no soy un fanático de este enfoque.
En lugar de tener un mapeo uno a uno de componentes web con componentes React hechos a mano, prefiero simplemente una El componente React que pasamos a nuestro componente web. Nombre de etiqueta a (counter-wc
en nuestro caso) - junto con todos los atributos y propiedades - y para que este componente represente nuestro componente web, agregue ref
luego descubra qué es un accesorio y qué es un atributo. Esta es la solución perfecta en mi opinión. No conozco una biblioteca que haga esto, pero debería ser fácil de crear. ¡Intentemos!
Está usar estamos buscando:
<WcWrapper wcTag="counter-wc" increment={increment} color={color} />
wcTag
es el nombre de la etiqueta del componente web; los otros son las propiedades y atributos que queremos transmitir.
Así es como se ve mi conversión:
import React, { createElement, useRef, useLayoutEffect, memo } from 'react';
const _WcWrapper = (props) => {
const { wcTag, children, ...restProps } = props;
const wcRef = useRef(null);
useLayoutEffect(() => {
const wc = wcRef.current;
for (const [key, value] of Object.entries(restProps)) {
if (key in wc) {
if (wc[key] !== value) {
wc[key] = value;
}
} else {
if (wc.getAttribute(key) !== value) {
wc.setAttribute(key, value);
}
}
}
});
return createElement(wcTag, { ref: wcRef });
};
export const WcWrapper = memo(_WcWrapper);
La línea más interesante está al final:
return createElement(wcTag, { ref: wcRef });
Aquí se explica cómo crear un elemento en React con un nombre dinámico. De hecho, aquí es donde React suele traducir JSX. Todos nuestros divs se convierten a createElement("div")
Por lo general, no necesitamos llamar a esta API directamente, pero está ahí cuando la necesitamos.
Además, queremos realizar un efecto de diseño y revisar cada accesorio que hemos pasado a nuestro componente. Los revisamos todos y verificamos si se trata de una propiedad con in
asegúrese de que comprueba la instancia del objeto del componente web, así como su cadena de prototipo, que capturará todos los getters/setters que están envueltos en el prototipo de clase. Si tal propiedad no existe, se considera un atributo. En ambos casos, establecemos solo si el valor realmente ha cambiado.
Si se pregunta por qué usamos useLayoutEffect
en cambio useEffect
esto se debe a que queremos ejecutar estas actualizaciones inmediatamente antes de que se muestre nuestro contenido. También tenga en cuenta que no tenemos una matriz de dependencias por nuestra cuenta. useLayoutEffect
; esto significa que queremos ejecutar esta actualización cada renderEsto puede ser arriesgado, ya que React es propenso a volver a generar imágenes. muchoMejoro esto envolviendo todo React.memo
Esta es esencialmente la versión moderna de React.PureComponent
lo que significa que el componente se representará solo si se ha cambiado uno de sus detalles reales, y verifica que esto haya sucedido mediante una simple verificación de igualdad.
El único riesgo aquí es que si pasa un soporte de objeto que muta directamente sin reasignación, entonces no verá las actualizaciones. Pero es muy desalentador, especialmente en la comunidad de React, así que no me preocuparía por eso.
Antes de continuar, me gustaría señalar una cosa más. Es posible que no esté satisfecho con la forma en que se ve el uso. Nuevamente, este componente se usa de la siguiente manera:
<WcWrapper wcTag="counter-wc" increment={increment} color={color} />
En particular, es posible que no le guste pasar el nombre de la etiqueta del componente web a <WcWrapper>
componente y prefiero en su lugar @lit-labs/react
paquete anterior, que crea un nuevo componente React individual para cada componente web. Esto es perfectamente justo y te animo a usar lo que sea más conveniente para ti. Pero para mí una ventaja de este enfoque es que es fácil de BorrarSi por algún milagro React fusiona la correcta gestión de componentes web desde su rama experimental en main
mañana podrá cambiar el código anterior de este:
<WcWrapper wcTag="counter-wc" increment={increment} color={color} />
… A esto:
<counter-wc ref={wcRef} increment={increment} color={color} />
Probablemente podría incluso escribir un modo de código para hacer esto en cualquier lugar y luego eliminar <WcWrapper>
De hecho, elimine esto: la búsqueda global y el reemplazo con RegEx probablemente funcionarán.
La aplicación
Lo sé, parece haber hecho un viaje aquí. Si recuerda, nuestro objetivo original era tomar el código de vista previa de la imagen que vimos en mi última publicación y moverlo a un componente web para que pueda usarse en cualquier marco de JavaScript. La falta de interacción React adecuada agregó muchos detalles a la mezcla. Pero ahora que tenemos una descripción general decente de cómo crear un componente web y usarlo, la implementación casi no tendrá éxito.
Publicaré todo el componente web aquí y mencionaré algunas de las partes interesantes. Si quieres verlo en acción, aquí demostración de trabajo. Cambiará entre mis tres libros favoritos en mis tres lenguajes de programación favoritos. La URL de cada libro será única cada vez, por lo que puede ver la vista previa, aunque probablemente desee reducir las cosas en la pestaña Red de DevTools para ver realmente cómo van las cosas.
Ver todo el código
class BookCover extends HTMLElement {
static observedAttributes = ['url'];
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'url') {
this.createMainImage(newValue);
}
}
set preview(val) {
this.previewEl = this.createPreview(val);
this.render();
}
createPreview(val) {
if (typeof val === 'string') {
return base64Preview(val);
} else {
return blurHashPreview(val);
}
}
createMainImage(url) {
this.loaded = false;
const img = document.createElement('img');
img.alt="Book cover";
img.addEventListener('load', () => {
if (img === this.imageEl) {
this.loaded = true;
this.render();
}
});
img.src = url;
this.imageEl = img;
}
connectedCallback() {
this.render();
}
render() {
const elementMaybe = this.loaded ? this.imageEl : this.previewEl;
syncSingleChild(this, elementMaybe);
}
}
Primero, registramos el atributo que nos interesa y reaccionamos cuando cambia:
static observedAttributes = ['url'];
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'url') {
this.createMainImage(newValue);
}
}
Esto crea nuestro componente de imagen, que solo se mostrará al cargar:
createMainImage(url) {
this.loaded = false;
const img = document.createElement('img');
img.alt="Book cover";
img.addEventListener('load', () => {
if (img === this.imageEl) {
this.loaded = true;
this.render();
}
});
img.src = url;
this.imageEl = img;
}
Luego tenemos nuestra propiedad de vista previa, que puede ser nuestra cadena de vista previa base64 o nuestra propia blurhash
paquete:
set preview(val) {
this.previewEl = this.createPreview(val);
this.render();
}
createPreview(val) {
if (typeof val === 'string') {
return base64Preview(val);
} else {
return blurHashPreview(val);
}
}
Esto se aplica a la función auxiliar que necesitamos:
function base64Preview(val) {
const img = document.createElement('img');
img.src = val;
return img;
}
function blurHashPreview(preview) {
const canvasEl = document.createElement('canvas');
const { w: width, h: height } = preview;
canvasEl.width = width;
canvasEl.height = height;
const pixels = decode(preview.blurhash, width, height);
const ctx = canvasEl.getContext('2d');
const imageData = ctx.createImageData(width, height);
imageData.data.set(pixels);
ctx.putImageData(imageData, 0, 0);
return canvasEl;
}
Finalmente, el nuestro render
método:
connectedCallback() {
this.render();
}
render() {
const elementMaybe = this.loaded ? this.imageEl : this.previewEl;
syncSingleChild(this, elementMaybe);
}
Y algunos métodos útiles para ponerlo todo junto:
export function syncSingleChild(container, child) {
const currentChild = container.firstElementChild;
if (currentChild !== child) {
clearContainer(container);
if (child) {
container.appendChild(child);
}
}
}
export function clearContainer(el) {
let child;
while ((child = el.firstElementChild)) {
el.removeChild(child);
}
}
Esto es un poco más complicado de lo que necesitaríamos si lo construyéramos en un marco, pero la ventaja es que podemos reutilizarlo en cualquier marco que queramos, aunque React necesitará un envoltorio por ahora, como comentamos.
Retazos
Ya mencioné el paquete React de Lit. Pero si descubre que está usando Stencil, en realidad es compatible tubo de salida separado solo para ReactY la buena gente de Microsoft también lo ha hecho. creó algo como el caparazón de Litadjunto a la biblioteca para componentes web rápidos.
Como mencioné, todos los marcos sin el nombre de React se encargarán de configurar las propiedades de los componentes web por usted. Solo tenga en cuenta que algunos tienen algunos matices especiales de sintaxis. Por ejemplo, con Solid.js, <your-wc value={12}>
siempre sugiere esto value
es una propiedad que puedes reemplazar con attr
prefijo, como <your-wc attr:value={12}>
.
resumiendo
Los componentes web son una parte interesante, a menudo infrautilizada, del panorama del desarrollo web. Pueden ayudarlo a reducir su dependencia de cualquier marco de JavaScript único al administrar su interfaz de usuario o componentes de "hoja". Mientras los cree como componentes web, a diferencia de los componentes Svelte o React, no serán tan ergonómicos, la ventaja es que pueden usarse ampliamente repetidamente.
Deja una respuesta