¿Qué es Lit?
Lit es un conjunto de bibliotecas de código abierto de Google que ayuda a los desarrolladores a compilar componentes rápidos y livianos que funcionen en cualquier framework. Con Lit, puedes compilar y compartir componentes, aplicaciones, sistemas de diseño y mucho más.
Qué aprenderás
Cómo traducir varios conceptos de React a Lit, como los siguientes:
- JSX y creación de plantillas
- Componentes y props
- Estado y ciclo de vida
- Hooks
- Hijos
- Refs
- Mediación del estado
Qué compilarás
Al finalizar este codelab, podrás convertir los conceptos de los componentes de React en los equivalentes de Lit.
Qué necesitarás
- La versión más reciente de Chrome, Safari, Firefox o Edge.
- Conocimiento de HTML, CSS, JavaScript y Herramientas para desarrolladores de Chrome.
- Conocimiento de React
- (Avanzado) Si quieres obtener la mejor experiencia de desarrollo, descarga VS Code. También necesitarás lit-plugin para VS Code y NPM.
Los conceptos y capacidades básicas de Lit son similares a los de React en muchos aspectos, pero Lit también tiene algunas diferencias y factores diferenciadores clave:
Es pequeño.
Lit es pequeño (su tamaño reducido y comprimido en gzip es de alrededor de 5 KB) en comparación con el tamaño mayor a 40 KB de React + ReactDOM.
Es rápido.
En las comparativas públicas que comparan el sistema de plantillas de Lit, lit-html, con el de VDOM de React, el primero es entre un 8% y 10% más rápido que React en el peor de los casos y un 50% más rápido en los casos de uso más comunes.
LitElement (la clase básica de componentes de Lit) agrega una sobrecarga mínima al lit-html, pero supera el rendimiento de React en un 16% a 30% cuando se comparan funciones de los componentes como el uso de memoria, la interacción y los tiempos de inicio.
No necesita compilación
Gracias a las funciones del navegador nuevas, como módulos ES y literales de plantillas etiquetados, Lit no necesita compilación para ejecutarse. Esto significa que los entornos de desarrollo se pueden configurar con una etiqueta de secuencia de comandos + un navegador + un servidor para poder comenzar a trabajar.
Con los módulos ES y las CDN modernas, como Skypack o UNPKG, es posible que no necesites NPM para comenzar.
Sin embargo, si lo deseas, puedes compilar y optimizar el código de Lit. La reciente integración para desarrolladores en torno a módulos ES nativos resultó positiva para Lit: Lit es solo JavaScript normal, y no es necesario contar con CLI específicos del framework ni manejar compilaciones.
Independiente del framework
Los componentes de Lit se basan en un conjunto de estándares web llamados Web Components. Por lo tanto, compilar un componente en Lit funcionará en frameworks actuales y futuros. Si es compatible con elementos HTML, es compatible con Web Components.
Los únicos problemas relacionados con la interoperabilidad del framework se presentan cuando los frameworks tienen una compatibilidad restrictiva para el DOM. React es uno de esos frameworks, pero sí ofrece rutas de escape mediante Refs, los cuales no representan una buena experiencia para los desarrolladores en React.
El equipo de Lit está trabajando en un proyecto experimental llamado @lit-labs/react
, que analizará automáticamente los componentes de Lit y generará un wrapper de React para que no tengas que usar Refs.
Además, puedes buscar en Custom Elements Everywhere los frameworks y bibliotecas que funcionan bien con los elementos personalizados.
Compatibilidad de primera clase con TypeScript
Aunque es posible escribir todo el código de Lit en JavaScript, Lit está escrito en TypeScript, por lo que el equipo de Lit recomienda que los desarrolladores también usen este lenguaje.
El equipo de Lit estuvo trabajando con la comunidad de esta biblioteca para ayudar a mantener los proyectos que incorporan la comprobación de tipos de TypeScript y el uso de intellisense en plantillas de Lit tanto en el desarrollo como en la compilación con lit-analyzer
y lit-plugin
.
Las herramientas para desarrolladores están integradas en el navegador
Los componentes de Lit son solo elementos HTML en el DOM. Esto significa que, para inspeccionar tus componentes, no es necesario que instales herramientas ni extensiones en el navegador.
Simplemente puedes abrir una herramienta para desarrolladores, seleccionar un elemento y explorar sus propiedades o su estado.
, <mwc-textfield>, $0.value devuelve "hello world", $0.outlined devuelve "true", y {$0} muestra la expansión de la propiedad." class="l10n-relative-url-src" l10n-attrs-original-order="alt,src,class" src="https://codelabs.developers.google.com/codelabs/lit-2-for-react-devs/./img/browser-tools.png" />
Se diseñó teniendo en cuenta la renderización del servidor (SSR)
Lit 2 se desarrolló teniendo en cuenta la compatibilidad con la SSR. En el momento en que se redactó este codelab, el equipo de Lit todavía no había lanzado las herramientas de SSR de forma estable, pero ya estuvo implementando componentes renderizados del servidor en los productos de Google. El equipo de Lit espera lanzar estas herramientas de forma externa en GitHub pronto.
Mientras tanto, puedes seguir el progreso del equipo de Lit aquí.
Es fácil de adoptar
Lit no requiere un compromiso significativo para su uso. Puedes compilar componentes en Lit y agregarlos a tu proyecto existente. Si no te gustan, no tienes que convertir toda la app de una vez, ya que los componentes web funcionan en otros frameworks.
¿Compilaste una aplicación completa en Lit pero quieres pasarte a otra opción? Luego, puedes colocar tu aplicación de Lit actual dentro de tu nuevo framework y migrar lo que desees a sus componentes.
Además, muchos frameworks modernos son compatibles con los datos de salida de componentes web, lo que significa que, por lo general, se pueden integrar en un elemento de Lit.
Hay dos maneras de hacer este codelab:
- Puedes hacerlo todo en línea en el navegador.
- (Avanzado) Puedes hacerlo en tu máquina local con VS Code.
Accede al código
A lo largo del codelab, habrá vínculos a la zona de pruebas de Lit, como este:
La zona de pruebas es un entorno que se ejecuta por completo en el navegador. Puede compilar y ejecutar archivos de TypeScript y JavaScript, y también puede solucionar de manera automática las importaciones a módulos de nodos, p. ej.:
// before
import './my-file.js';
import 'lit';
// after
import './my-file.js';
import 'https://cdn.skypack.dev/lit';
Puedes completar todo el instructivo en la zona de pruebas de Lit y usar estos puntos de control como puntos de partida. Si usas VS Code, puedes usar estos puntos de control para descargar el código inicial de cualquier paso y usarlos para verificar tu trabajo.
Explora la IU de la zona de pruebas de Lit
La captura de pantalla de la IU de la zona de pruebas de Lit destaca las secciones que usarás en este codelab.
- Selector de archivos Presta atención al botón del signo más…
- Editor de archivos
- Vista previa del código
- Botón para volver a cargar
- Botón para descargar
Configuración de VS Code (avanzado)
Estos son los beneficios de usar esta configuración de VS Code:
- Comprobación del tipo de plantilla
- Intellisense y autocompletado de plantillas
Si tienes NPM y VS Code (con el complemento lit-plugin) ya instalados y sabes cómo usar ese entorno, puedes descargar e iniciar estos proyectos de la siguiente manera:
- Presiona el botón de descarga.
- Extrae el contenido del archivo tar en un directorio.
- Si usas TS, realiza una configuración rápida de tsconfig que genere módulos ES y ES2015+.
- Instala un servidor de desarrollo que pueda resolver especificadores de módulos básicos (el equipo de Lit recomienda @web/dev-server).
- Ejecuta el servidor de desarrollo y abre el navegador (si usas @web/dev-server, puedes recurrir a
web-dev-server --node-resolve --watch --open
).
En esta sección, aprenderás lo básico sobre las plantillas de Lit.
Plantillas de JSX y Lit
JSX es una extensión de sintaxis de JavaScript que permite a los usuarios de React crear plantillas con facilidad en su código de JavaScript. Las plantillas de Lit tienen un propósito similar: expresar la IU de un componente como una función de su estado.
Sintaxis básica
En React, tendrías que renderizar un Hello World de JSX de esta forma:
import 'react';
import ReactDOM from 'react-dom';
const name = 'Josh Perez';
const element = (
<>
<h1>Hello, {name}</h1>
<div>How are you?</div>
</>
);
ReactDOM.render(
element,
mountNode
);
En el ejemplo anterior, hay dos elementos y una variable "name" incluida. En Lit, harías lo siguiente:
import {html, render} from 'lit';
const name = 'Josh Perez';
const element = html`
<h1>Hello, ${name}</h1>
<div>How are you?</div>`;
render(
element,
mountNode
);
Ten en cuenta que las plantillas de Lit no necesitan un fragmento de React para agrupar varios elementos en ellas.
Como su nombre lo indica, en Lit, las plantillas se unen con un LITeral de plantilla etiquetado de html
.
Valores de plantillas
Las plantillas de Lit pueden aceptar otras plantillas de la misma biblioteca, conocidas como TemplateResult
. Por ejemplo, encierra name
con etiquetas de cursiva (<i>
) y únelas con un literal de plantilla etiquetado N.B. Asegúrate de usar un carácter de acento grave (`
) en lugar del carácter de comillas simples ('
).
import {html, render} from 'lit';
const name = html`<i>Josh Perez</i>`;
const element = html`
<h1>Hello, ${name}</h1>
<div>How are you?</div>`;
render(
element,
mountNode
);
Los TemplateResult
de Lit pueden aceptar arrays, strings y otros TemplateResult
, así como directivas.
Como ejercicio, intenta convertir el siguiente código de React en Lit:
const itemsToBuy = [
<li>Bananas</li>,
<li>oranges</li>,
<li>apples</li>,
<li>grapes</li>
];
const element = (
<>
<h1>Things to buy:</h1>
<ol>
{itemsToBuy}
</ol>
</>);
ReactDOM.render(
element,
mountNode
);
Respuesta:
import {html, render} from 'lit';
const itemsToBuy = [
html`<li>Bananas</li>`,
html`<li>oranges</li>`,
html`<li>apples</li>`,
html`<li>grapes</li>`
];
const element = html`
<h1>Things to buy:</h1>
<ol>
${itemsToBuy}
</ol>`;
render(
element,
mountNode
);
Cómo pasar y configurar props
Una de las diferencias más importantes entre las sintaxis de JSX y Lit es la sintaxis de vinculación de datos. Por ejemplo, mira esta entrada de React con vinculaciones:
const disabled = false;
const label = 'my label';
const myClass = 'my-class';
const value = 'my value';
const element =
<input
disabled={disabled}
className={`static-class ${myClass}`}
defaultValue={value}/>;
ReactDOM.render(
element,
mountNode
);
En el ejemplo anterior, se define una entrada que hace lo siguiente:
- Asigna una variable definida a disabled (falso en este caso).
- Configura la clase como
static-class
más una variable (en este caso,"static-class my-class"
). - Configura un valor predeterminado.
En Lit, harías lo siguiente:
import {html, render} from 'lit';
const disabled = false;
const label = 'my label';
const myClass = 'my-class';
const value = 'my value';
const element = html`
<input
?disabled=${disabled}
class="static-class ${myClass}"
.value=${value}>`;
render(
element,
mountNode
);
En el ejemplo de Lit, se agrega una vinculación booleana para activar o desactivar el atributo disabled
.
A continuación, hay una vinculación directa con el atributo class
, en lugar de className
. Se pueden agregar varias vinculaciones al atributo class
, a menos que uses la directiva classMap
, que es un auxiliar para alternar entre clases.
Finalmente, se establece la propiedad value
en la entrada. A diferencia de React, esto no establecerá que el elemento de entrada sea de solo lectura, ya que sigue la implementación nativa y el comportamiento de la entrada.
Sintaxis de la vinculación de propiedades de Lit
html`<my-element ?attribute-name=${booleanVar}>`;
- El prefijo
?
es la sintaxis de vinculación para activar o desactivar un atributo en un elemento. - Equivale a
inputRef.toggleAttribute('attribute-name', booleanVar)
. - Es útil para los elementos que usan
disabled
debido a que el DOM aún leedisabled="false"
como verdadero porqueinputElement.hasAttribute('disabled') === true
.
html`<my-element .property-name=${anyVar}>`;
- El prefijo
.
es la sintaxis de vinculación para configurar una propiedad de un elemento. - Equivale a
inputRef.propertyName = anyVar
. - Es ideal para pasar datos complejos como objetos, arrays o clases.
html`<my-element attribute-name=${stringVar}>`;
- Se vincula al atributo de un elemento.
- Equivale a
inputRef.setAttribute('attribute-name', stringVar)
. - Es ideal para valores básicos, selectores de normas de estilo y querySelectors.
Cómo pasar controladores
const disabled = false;
const label = 'my label';
const myClass = 'my-class';
const value = 'my value';
const element =
<input
onClick={() => console.log('click')}
onChange={e => console.log(e.target.value)} />;
ReactDOM.render(
element,
mountNode
);
En el ejemplo anterior, se define una entrada que hace lo siguiente:
- Registra la palabra "clic" cuando se hace clic en el campo.
- Registra el valor del campo cuando el usuario escribe un carácter.
En Lit, harías lo siguiente:
import {html, render} from 'lit';
const disabled = false;
const label = 'my label';
const myClass = 'my-class';
const value = 'my value';
const element = html`
<input
@click=${() => console.log('click')}
@input=${e => console.log(e.target.value)}>`;
render(
element,
mountNode
);
En el ejemplo de Lit, se agregó un objeto de escucha al evento click
con @click
.
A continuación, en lugar de usar onChange
, hay una vinculación al evento input
nativo de <input>
dado que el evento change
nativo solo se activa en blur
(React sintetiza estos eventos).
Sintaxis del controlador de eventos de Lit
html`<my-element @event-name=${() => {...}}></my-element>`;
- El prefijo
@
es la sintaxis de vinculación para un objeto de escucha de eventos. - Equivale a
inputRef.addEventListener('event-name', ...)
. - Usa nombres de eventos nativos del DOM.
En esta sección, aprenderás sobre los componentes y las funciones de clase de Lit. En las secciones posteriores, se explican el estado y los hooks con más detalle.
Componentes de clase y LitElement
El equivalente en Lit de un componente de clase de React es LitElement, y el concepto de "propiedades reactivas" de Lit es una combinación de props y estado de React. Por ejemplo:
import React from 'react';
import ReactDOM from 'react-dom';
class Welcome extends React.Component {
constructor(props) {
super(props);
this.state = {name: ''};
}
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
const element = <Welcome name="Elliott"/>
ReactDOM.render(
element,
mountNode
);
En el ejemplo anterior, hay un componente React que hace lo siguiente:
- Procesa un
name
. - Establece el valor predeterminado de
name
en una string vacía (""
). - Reasigna
name
a"Elliott"
.
Así es como lo harías en LitElement:
En TypeScript:
import {LitElement, html} from 'lit';
import {customElement, property} from 'lit/decorators.js';
@customElement('welcome-banner')
class WelcomeBanner extends LitElement {
@property({type: String})
name = '';
render() {
return html`<h1>Hello, ${this.name}</h1>`
}
}
En JavaScript:
import {LitElement, html} from 'lit';
class WelcomeBanner extends LitElement {
static get properties() {
return {
name: {type: String}
}
}
constructor() {
super();
this.name = '';
}
render() {
return html`<h1>Hello, ${this.name}</h1>`
}
}
customElements.define('welcome-banner', WelcomeBanner);
En el archivo HTML:
<!-- index.html -->
<head>
<script type="module" src="./index.js"></script>
</head>
<body>
<welcome-banner name="Elliott"></welcome-banner>
</body>
Una revisión de lo que sucede en el ejemplo anterior:
@property({type: String})
name = '';
- Define una propiedad reactiva pública, que forma parte de la API pública de tu componente.
- Expone un atributo (de forma predeterminada) y una propiedad de tu componente.
- Define cómo traducir el atributo del componente (que son strings) a un valor.
static get properties() {
return {
name: {type: String}
}
}
- Esto cumple la misma función que el decorador de TS
@property
, pero se ejecuta de forma nativa en JavaScript.
render() {
return html`<h1>Hello, ${this.name}</h1>`
}
- Se llama a este método cuando se modifica cualquier propiedad reactiva.
@customElement('welcome-banner')
class WelcomeBanner extends LitElement {
...
}
- Este proceso asocia un nombre de etiqueta de elemento HTML con una definición de clase.
- Debido al estándar de Custom Elements, el nombre de la etiqueta debe incluir un guion (-).
this
en un LitElement hace referencia a la instancia del elemento personalizado (<welcome-banner>
en este caso).
customElements.define('welcome-banner', WelcomeBanner);
- Este es el equivalente de JavaScript del decorador de TS
@customElement
.
<head>
<script type="module" src="./index.js"></script>
</head>
- Importa la definición del elemento personalizado.
<body>
<welcome-banner name="Elliott"></welcome-banner>
</body>
- Agrega el elemento personalizado a la página.
- Configura la propiedad de
name
como'Elliott'
.
Componentes de una función
Lit no tiene una interpretación 1:1 de un componente de una función, ya que no usa JSX ni un preprocesador. Sin embargo, es bastante simple componer una función que tome propiedades y renderice el DOM según esas propiedades. Por ejemplo:
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
const element = <Welcome name="Elliott"/>
ReactDOM.render(
element,
mountNode
);
En Lit sería de esta manera:
import {html, render} from 'lit';
function Welcome(props) {
return html`<h1>Hello, ${props.name}</h1>`;
}
render(
Welcome({name: 'Elliott'}),
document.body.querySelector('#root')
);
En esta sección, aprenderás sobre el estado y el ciclo de vida de Lit.
Estado
El concepto de Lit de "propiedades reactivas" es una mezcla del estado y las props de React. Las propiedades reactivas, cuando se modifican, pueden activar el ciclo de vida del componente. Las propiedades reactivas se dividen en dos variantes:
Propiedades reactivas públicas
// React
import React from 'react';
class MyEl extends React.Component {
constructor(props) {
super(props)
this.state = {name: 'there'}
}
componentWillReceiveProps(nextProps) {
if (this.props.name !== nextProps.name) {
this.setState({name: nextProps.name})
}
}
}
// Lit (TS)
import {LitElement} from 'lit';
import {property} from 'lit/decorators.js';
class MyEl extends LitElement {
@property() name = 'there';
}
- Se definen según
@property
. - Son similares a las props y el estado de React, pero pueden cambiar.
- Es una API pública a la que acceden los consumidores del componente y que estos pueden configurar.
Estado reactivo interno
// React
import React from 'react';
class MyEl extends React.Component {
constructor(props) {
super(props)
this.state = {name: 'there'}
}
}
// Lit (TS)
import {LitElement} from 'lit';
import {state} from 'lit/decorators.js';
class MyEl extends LitElement {
@state() name = 'there';
}
- Se define según
@state
. - Es similar al estado de React, pero puede cambiar.
- Es un estado privado interno al que por lo general se accede desde el componente o las subclases.
Ciclo de vida
El ciclo de vida de Lit es bastante similar al de React, pero existen algunas diferencias significativas.
constructor
// React (js)
import React from 'react';
class MyEl extends React.Component {
constructor(props) {
super(props);
this.state = { counter: 0 };
this._privateProp = 'private';
}
}
// Lit (ts)
class MyEl extends LitElement {
@property({type: Number}) counter = 0;
private _privateProp = 'private';
}
// Lit (js)
class MyEl extends LitElement {
static get properties() {
return { counter: {type: Number} }
}
constructor() {
this.counter = 0;
this._privateProp = 'private';
}
}
- El equivalente de Lit también es
constructor
. - No es necesario pasar nada a la llamada super.
- Lo invoca (no de forma completamente inclusiva):
document.createElement
document.innerHTML
new ComponentClass()
- Si se incluye un nombre de etiqueta no actualizado en la página y la definición se carga y se registra con
@customElement
ocustomElements.define
.
- Es una función similar a
constructor
de React.
render
// React
render() {
return <div>Hello World</div>
}
// Lit
render() {
return html`<div>Hello World</div>`;
}
- El equivalente de Lit también es
render
. - Puede mostrar cualquier resultado que se pueda renderizar, como
TemplateResult
ostring
, entre otros. - Como en React,
render()
debe ser una función pura. - Se renderizará al nodo que muestre
createRenderRoot()
(ShadowRoot
de forma predeterminada).
componentDidMount
componentDidMount
es similar a una combinación de las devoluciones de llamada del ciclo de vida firstUpdated
y connectedCallback
de Lit.
firstUpdated
import Chart from 'chart.js';
// React
componentDidMount() {
this._chart = new Chart(this.chartElRef.current, {...});
}
// Lit
firstUpdated() {
this._chart = new Chart(this.chartEl, {...});
}
- Se llama la primera vez que la plantilla del componente se renderiza en la raíz del componente.
- Solo se llamará si el elemento está conectado; p. ej., no se llama a través de
document.createElement('my-component')
hasta que el nodo se agregue al árbol del DOM. - Este es un buen lugar para realizar la configuración de componentes que requiere el DOM renderizado por el componente.
- A diferencia de los cambios de
componentDidMount
de React en las propiedades reactivas enfirstUpdated
, se realizará una nueva renderización, aunque el navegador generalmente agrupará los cambios en el mismo marco. Si esos cambios no requieren acceso al DOM de la raíz, por lo general deberían incluirse enwillUpdate
.
connectedCallback
// React
componentDidMount() {
this.window.addEventListener('resize', this.boundOnResize);
}
// Lit
connectedCallback() {
super.connectedCallback();
this.window.addEventListener('resize', this.boundOnResize);
}
- Se llama cada vez que se inserta el elemento personalizado en el árbol del DOM.
- A diferencia de los componentes de React, cuando los elementos personalizados se separan del DOM, no se destruyen y, por lo tanto, se pueden "conectar" múltiples veces.
- Es útil para volver a inicializar el DOM o volver a adjuntar objetos de escucha de eventos que se borraron al desconectarse.
- Nota: Se puede llamar a
connectedCallback
antes defirstUpdated
; por lo que en la primera llamada, es posible que el DOM no esté disponible.
componentDidUpdate
// React
componentDidUpdate(prevProps) {
if (this.props.title !== prevProps.title) {
this._chart.setTitle(this.props.title);
}
}
// Lit (ts)
updated(prevProps: PropertyValues<this>) {
if (prevProps.has('title')) {
this._chart.setTitle(this.title);
}
}
- El equivalente de Lit es
updated
(con el tiempo pasado en inglés de "update"). - A diferencia de React, también se llama a
updated
en la renderización inicial. - Es una función similar a
componentDidUpdate
de React.
componentWillUnmount
// React
componentWillUnmount() {
this.window.removeEventListener('resize', this.boundOnResize);
}
// Lit
disconnectedCallback() {
super.disconnectedCallback();
this.window.removeEventListener('resize', this.boundOnResize);
}
- El equivalente de Lit es similar a
disconnectedCallback
. - A diferencia de los componentes de React, el componente no se destruye cuando los elementos personalizados se separan del DOM.
- A diferencia de
componentWillUnmount
, se llama adisconnectedCallback
después de que se quita el elemento del árbol. - El DOM de la raíz aún está conectado al subárbol de la raíz.
- Es útil para borrar los objetos de escucha de eventos y referencias con fugas para que el navegador pueda recolectar el componente no utilizado.
Ejercicio
import React from 'react';
import ReactDOM from 'react-dom';
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
componentWillUnmount() {
clearInterval(this.timerID);
}
tick() {
this.setState({
date: new Date()
});
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
En el ejemplo anterior, hay un reloj simple que hace lo siguiente:
- Renderiza "Hello World! It is" y, luego, muestra la hora.
- El reloj se actualiza cada segundo.
- Cuando se desmonta, se borra el intervalo que llama el tick.
Primero, comienza con la declaración de la clase del componente:
// Lit (TS)
// some imports here are imported in advance
import {LitElement, html} from 'lit';
import {customElement, state} from 'lit/decorators.js';
@customElement('lit-clock')
class LitClock extends LitElement {
}
// Lit (JS)
// `html` is imported in advance
import {LitElement, html} from 'lit';
class LitClock extends LitElement {
}
customElements.define('lit-clock', LitClock);
A continuación, inicializa date
y declara que sea una propiedad interna reactiva con @state
, ya que los usuarios del componente no configurarán date
de manera directa.
// Lit (TS)
import {LitElement, html} from 'lit';
import {customElement, state} from 'lit/decorators.js';
@customElement('lit-clock')
class LitClock extends LitElement {
@state() // declares internal reactive prop
private date = new Date(); // initialization
}
// Lit (JS)
import {LitElement, html} from 'lit';
class LitClock extends LitElement {
static get properties() {
return {
// declares internal reactive prop
date: {state: true}
}
}
constructor() {
super();
// initialization
this.date = new Date();
}
}
customElements.define('lit-clock', LitClock);
A continuación, renderiza la plantilla.
// Lit (JS & TS)
render() {
return html`
<div>
<h1>Hello, World!</h1>
<h2>It is ${this.date.toLocaleTimeString()}.</h2>
</div>
`;
}
Ahora, implementa el método tick.
tick() {
this.date = new Date();
}
Luego sigue la implementación de componentDidMount
. Una vez más, el análogo de Lit es una combinación de firstUpdated
y connectedCallback
. En el caso de este componente, llamar a tick
con setInterval
no requiere acceso al DOM dentro de la raíz. Además, el intervalo se borra cuando se quita el elemento del árbol de documentos; por lo tanto, si se vuelve a conectar, el intervalo debe comenzar de nuevo. Entonces, connectedCallback
es una mejor opción en este caso.
// Lit (TS)
@customElement('lit-clock')
class LitClock extends LitElement {
@state()
private date = new Date();
private timerId = -1; // initialize timerId for TS
connectedCallback() {
super.connectedCallback();
this.timerId = setInterval(
() => this.tick(),
1000
);
}
...
}
// Lit (JS)
constructor() {
super();
// initialization
this.date = new Date();
this.timerId = -1; // initialize timerId for JS
}
connectedCallback() {
super.connectedCallback();
this.timerId = setInterval(
() => this.tick(),
1000
);
}
Por último, borra el intervalo para que no ejecute el tick después de que el elemento se desconecte del árbol de documentos.
// Lit (TS & JS)
disconnectedCallback() {
super.disconnectedCallback();
clearInterval(this.timerId);
}
Al juntar todo, debería verse de la siguiente manera:
// Lit (TS)
import {LitElement, html} from 'lit';
import {customElement, state} from 'lit/decorators.js';
@customElement('lit-clock')
class LitClock extends LitElement {
@state()
private date = new Date();
private timerId = -1;
connectedCallback() {
super.connectedCallback();
this.timerId = setInterval(
() => this.tick(),
1000
);
}
tick() {
this.date = new Date();
}
render() {
return html`
<div>
<h1>Hello, World!</h1>
<h2>It is ${this.date.toLocaleTimeString()}.</h2>
</div>
`;
}
disconnectedCallback() {
super.disconnectedCallback();
clearInterval(this.timerId);
}
}
// Lit (JS)
import {LitElement, html} from 'lit';
class LitClock extends LitElement {
static get properties() {
return {
date: {state: true}
}
}
constructor() {
super();
this.date = new Date();
}
connectedCallback() {
super.connectedCallback();
this.timerId = setInterval(
() => this.tick(),
1000
);
}
tick() {
this.date = new Date();
}
render() {
return html`
<div>
<h1>Hello, World!</h1>
<h2>It is ${this.date.toLocaleTimeString()}.</h2>
</div>
`;
}
disconnectedCallback() {
super.disconnectedCallback();
clearInterval(this.timerId);
}
}
customElements.define('lit-clock', LitClock);
En esta sección, aprenderás a traducir los conceptos hook de React a Lit.
Los conceptos de hooks de React
Los hooks de React ofrecen una manera para que los componentes de las funciones se "enganchen" en el estado. Esto tiene varios beneficios.
- Simplifican la reutilización de la lógica con estado.
- Ayudan a dividir un componente en funciones más pequeñas.
Además, el enfoque en los componentes basados en funciones aborda ciertos problemas con la sintaxis basada en clases de React, como los siguientes:
- Tener que pasar
props
deconstructor
asuper
- La inicialización desordenada de propiedades en
constructor
- Ese fue uno de los motivos que indicó el equipo de React en ese momento, pero lo resolvió en ES2019
- Los problemas que causa
this
ya no hacen referencia al componente
Conceptos de hooks de React en Lit
Como se mencionó en la sección Componentes y props, Lit no ofrece una manera de crear elementos personalizados a partir de una función, pero LitElement soluciona la mayoría de los problemas principales de los componentes de la clase de React. Por ejemplo:
// React (at the time of making hooks)
import React from 'react';
import ReactDOM from 'react-dom';
class MyEl extends React.Component {
constructor(props) {
super(props); // Leaky implementation
this.state = {count: 0};
this._chart = null; // Deemed messy
}
render() {
return (
<>
<div>Num times clicked {count}</div>
<button onClick={this.clickCallback}>click me</button>
</>
);
}
clickCallback() {
// Errors because `this` no longer refers to the component
this.setState({count: this.count + 1});
}
}
// Lit (ts)
class MyEl extends LitElement {
@property({type: Number}) count = 0; // No need for constructor to set state
private _chart = null; // Public class fields introduced to JS in 2019
render() {
return html`
<div>Num times clicked ${count}</div>
<button @click=${this.clickCallback}>click me</button>`;
}
private clickCallback() {
// No error because `this` refers to component
this.count++;
}
}
¿Cómo aborda Lit estos problemas?
constructor
no acepta argumentos.- Todas las vinculaciones de
@event
se vinculan de forma automática athis
. this
en la gran mayoría de los casos se refiere a la referencia del elemento personalizado.- Ahora se pueden crear instancias de las propiedades de clases como miembros de clases. Esto borra las implementaciones basadas en constructor.
Controladores reactivos
Los conceptos principales detrás de los hooks existen en Lit como controladores reactivos. Los patrones del controlador reactivo permiten compartir lógica con estado, dividir los componentes en bits más pequeños y modulares, y también conectarse al ciclo de vida de actualización de un elemento.
Un controlador reactivo es una interfaz de objetos que se puede conectar al ciclo de vida de actualización de un host de controlador, como LitElement.
El ciclo de vida de un ReactiveController
y un reactiveControllerHost
es de la siguiente manera:
interface ReactiveController {
hostConnected(): void;
hostUpdate(): void;
hostUpdated(): void;
hostDisconnected(): void;
}
interface ReactiveControllerHost {
addController(controller: ReactiveController): void;
removeController(controller: ReactiveController): void;
requestUpdate(): void;
readonly updateComplete: Promise<boolean>;
}
Si construyes un controlador reactivo y lo conectas a un host con addController
, se llamará al ciclo de vida del controlador junto con la del host. Por ejemplo, recuerda el caso del reloj de la sección Estado y ciclo de vida:
import React from 'react';
import ReactDOM from 'react-dom';
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
componentWillUnmount() {
clearInterval(this.timerID);
}
tick() {
this.setState({
date: new Date()
});
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
En el ejemplo anterior, hay un reloj sencillo que hace lo siguiente:
- Renderiza "Hello World! It is" y, luego, muestra la hora.
- El reloj se actualiza cada segundo.
- Cuando se desmonta, se borra el intervalo que llama el tick.
Compilación de la estructura del componente
Primero, comienza con la declaración de la clase del componente y agrega la función render
.
// Lit (TS) - index.ts
import {LitElement, html} from 'lit';
import {customElement} from 'lit/decorators.js';
@customElement('my-element')
class MyElement extends LitElement {
render() {
return html`
<div>
<h1>Hello, world!</h1>
<h2>It is ${'time to get Lit'}.</h2>
</div>
`;
}
}
// Lit (JS) - index.js
import {LitElement, html} from 'lit';
class MyElement extends LitElement {
render() {
return html`
<div>
<h1>Hello, world!</h1>
<h2>It is ${'time to get Lit'}.</h2>
</div>
`;
}
}
customElements.define('my-element', MyElement);
Compilación del controlador
Ahora, cambia a clock.ts
, crea una clase para el ClockController
y configura el constructor
:
// Lit (TS) - clock.ts
import {ReactiveController, ReactiveControllerHost} from 'lit';
export class ClockController implements ReactiveController {
private readonly host: ReactiveControllerHost;
constructor(host: ReactiveControllerHost) {
this.host = host;
host.addController(this);
}
hostConnected() {
}
private tick() {
}
hostDisconnected() {
}
// Will not be used but needed for TS compilation
hostUpdate() {};
hostUpdated() {};
}
// Lit (JS) - clock.js
export class ClockController {
constructor(host) {
this.host = host;
host.addController(this);
}
hostConnected() {
}
tick() {
}
hostDisconnected() {
}
}
Un controlador reactivo se puede compilar de cualquier manera, siempre y cuando comparta la interfaz de ReactiveController
; sin embargo, el equipo de Lit prefiere usar en la mayoría de los casos básicos una clase con un constructor
que pueda incorporar una interfaz de ReactiveControllerHost
y cualquier otra propiedad que se necesite para inicializar el controlador.
Ahora debes traducir las devoluciones de llamada del ciclo de vida de React a las devoluciones de llamada del controlador. En resumen:
componentDidMount
- Con el
connectedCallback
de LitElement - Con el
hostConnected
del controlador
- Con el
ComponentWillUnmount
- Del
disconnectedCallback
de LitElement - Del
hostDisconnected
del controlador
- Del
Para obtener más información sobre cómo traducir el ciclo de vida de React al de Lit, consulta la sección Estado y ciclo de vida.
A continuación, implementa la devolución de llamada hostConnected
y los métodos tick
, y borra el intervalo de hostDisconnected
como se hace en el ejemplo de la sección Estado y ciclo de vida.
// Lit (TS) - clock.ts
export class ClockController implements ReactiveController {
private readonly host: ReactiveControllerHost;
private interval = 0;
date = new Date();
constructor(host: ReactiveControllerHost) {
this.host = host;
host.addController(this);
}
hostConnected() {
this.interval = setInterval(() => this.tick(), 1000);
}
private tick() {
this.date = new Date();
}
hostDisconnected() {
clearInterval(this.interval);
}
hostUpdate() {};
hostUpdated() {};
}
// Lit (JS) - clock.js
export class ClockController {
interval = 0;
host;
date = new Date();
constructor(host) {
this.host = host;
host.addController(this);
}
hostConnected() {
this.interval = setInterval(() => this.tick(), 1000);
}
tick() {
this.date = new Date();
}
hostDisconnected() {
clearInterval(this.interval);
}
}
Uso del controlador
Para usar el controlador del reloj, importa el controlador y actualiza el componente en index.ts
o index.js
.
// Lit (TS) - index.ts
import {LitElement, html, ReactiveController, ReactiveControllerHost} from 'lit';
import {customElement} from 'lit/decorators.js';
import {ClockController} from './clock.js';
@customElement('my-element')
class MyElement extends LitElement {
private readonly clock = new ClockController(this); // Instantiate
render() {
// Use controller
return html`
<div>
<h1>Hello, world!</h1>
<h2>It is ${this.clock.date.toLocaleTimeString()}.</h2>
</div>
`;
}
}
// Lit (JS) - index.js
import {LitElement, html} from 'lit';
import {ClockController} from './clock.js';
class MyElement extends LitElement {
clock = new ClockController(this); // Instantiate
render() {
// Use controller
return html`
<div>
<h1>Hello, world!</h1>
<h2>It is ${this.clock.date.toLocaleTimeString()}.</h2>
</div>
`;
}
}
customElements.define('my-element', MyElement);
Si quieres usar el controlador, debes crear una instancia de este. Para ello, pasa una referencia al host del controlador (que es el componente <my-element>
) y, luego, usa el controlador en el método render
.
Activación de repeticiones de renderizaciones en el controlador
Ten en cuenta que se mostrará la hora, pero la hora no se actualiza. Esto se debe a que el control establece la fecha cada segundo, pero el host no se actualiza. El motivo es que date
está cambiando en la clase ClockController
y ya no el componente. Esto significa que, después de que date
esté configurado en el controlador, se le debe solicitar al host que ejecute el ciclo de vida de actualización con host.requestUpdate()
.
// Lit (TS & JS) - clock.ts / clock.js
private tick() {
this.date = new Date();
this.host.requestUpdate();
}
Ahora el reloj debería funcionar.
Para una comparación más detallada de casos de uso habituales con hooks, consulta la sección Temas avanzados: hooks.
En esta sección, aprenderás a usar slots para administrar hijos en Lit.
Hijos y slots
Los slots te permiten anidar componentes para realizar una composición.
En React, los hijos se heredan mediante props. El slot predeterminado es props.children
, y la función render
define dónde se posiciona el slot predeterminado. Por ejemplo:
const MyArticle = (props) => {
return <article>{props.children}</article>;
};
Ten en cuenta que props.children
son componentes de React y no elementos HTML.
En Lit, los hijos se componen en la función de renderización con elementos de slot. Ten en cuenta que los hijos no se heredan de la misma manera que en React. En Lit, los hijos son HTMLElements conectados a slots. Esta conexión se denomina proyección.
@customElement("my-article")
export class MyArticle extends LitElement {
render() {
return html`
<article>
<slot></slot>
</article>
`;
}
}
Slots múltiples
En React, agregar múltiples slots es básicamente lo mismo que heredar más props.
const MyArticle = (props) => {
return (
<article>
<header>
{props.headerChildren}
</header>
<section>
{props.sectionChildren}
</section>
</article>
);
};
Del mismo modo, agregar más elementos <slot>
crea más slots en Lit. Los slots múltiples se definen con el atributo name
: <slot name="slot-name">
. Esto permite que los hijos declaren qué slot se les asignará.
@customElement("my-article")
export class MyArticle extends LitElement {
render() {
return html`
<article>
<header>
<slot name="headerChildren"></slot>
</header>
<section>
<slot name="sectionChildren"></slot>
</section>
</article>
`;
}
}
Contenido de slot predeterminado
Los slots mostrarán su subárbol cuando no haya nodos proyectados para ese slot. Cuando se proyectan nodos en un slot, este no mostrará el subárbol y, en su lugar, se mostrarán los nodos proyectados.
@customElement("my-element")
export class MyElement extends LitElement {
render() {
return html`
<section>
<div>
<slot name="slotWithDefault">
<p>
This message will not be rendered when children are attached to this slot!
<p>
</slot>
</div>
</section>
`;
}
}
Cómo asignar hijos a los slots
En React, los hijos se asignan a los slots a través de las propiedades de un componente. En el siguiente ejemplo, los elementos de React se pasan a los props headerChildren
y sectionChildren
.
const MyNewsArticle = () => {
return (
<MyArticle
headerChildren={<h3>Extry, Extry! Read all about it!</h3>}
sectionChildren={<p>Children are props in React!</p>}
/>
);
};
En Lit, los hijos se asignan a los slots mediante el atributo slot
.
@customElement("my-news-article")
export class MyNewsArticle extends LitElement {
render() {
return html`
<my-article>
<h3 slot="headerChildren">
Extry, Extry! Read all about it!
</h3>
<p slot="sectionChildren">
Children are composed with slots in Lit!
</p>
</my-article>
`;
}
}
Si no hay un slot predeterminado (p. ej., <slot>
) y no hay un slot que tenga un atributo name
(p. ej., <slot name="foo">
) que coincida con el atributo slot
de los hijos del elemento personalizado (p. ej., <div slot="foo">
), ese nodo no se proyectara y no se mostrará.
En ocasiones, es posible que un desarrollador necesite acceder a la API de un HTMLElement.
En esta sección, aprenderás a adquirir referencias de elementos en Lit.
Referencias de React
Un componente React se transpila en una serie de llamadas a funciones que crean un DOM virtual cuando se lo invoca. ReactDOM interpreta este DOM virtual y renderiza HTMLElements.
En React, las refs son espacio en la memoria que contienen un HTMLElement generado.
const RefsExample = (props) => {
const inputRef = React.useRef(null);
const onButtonClick = React.useCallback(() => {
inputRef.current?.focus();
}, [inputRef]);
return (
<div>
<input type={"text"} ref={inputRef} />
<br />
<button onClick={onButtonClick}>
Click to focus on the input above!
</button>
</div>
);
};
En el ejemplo anterior, el componente de React hará lo siguiente:
- Renderizar una entrada de texto vacía y un botón con texto.
- Enfocar la entrada cuando se haga clic en el botón.
Después de la renderización inicial, React establecerá inputRef.current
en el HTMLInputElement
generado mediante el atributo ref
.
"Referencias" de Lit con @query
Lit se ejecuta en un nivel cercano al del navegador y crea una abstracción muy delgada sobre las funciones nativas del navegador.
El equivalente de React para refs
en Lit es el HTMLElement que muestran los decoradores @query
y @queryAll
.
@customElement("my-element")
export class MyElement extends LitElement {
@query('input') // Define the query
inputEl!: HTMLInputElement; // Declare the prop
// Declare the click event listener
onButtonClick() {
// Use the query to focus
this.inputEl.focus();
}
render() {
return html`
<input type="text"></input>
<br />
<!-- Bind the click listener -->
<button @click=${this.onButtonClick}>
Click to focus on the input above!
</button>
`;
}
}
En el ejemplo anterior, el componente de Lit realiza las siguientes acciones:
- Define una propiedad en
MyElement
con el decorador@query
(lo que crea un método get para unHTMLInputElement
). - Declara y conecta una devolución de llamada de evento de clic denominada
onButtonClick
. - Selecciona la entrada al hacer clic en el botón.
En JavaScript, los decoradores @query
y @queryAll
realizan querySelector
y querySelectorAll
, respectivamente. Este es el equivalente de JavaScript de @query('input') inputEl!: HTMLInputElement;
get inputEl() {
return this.renderRoot.querySelector('input');
}
Después de que el componente de Lit confirme la plantilla del método render
a la raíz de my-element
, el decorador @query
ahora permitirá que inputEl
muestre el primer elemento input
que se encuentre en la raíz de renderización. Mostrará null
si @query
no puede encontrar el elemento especificado.
Si había varios elementos input
en la raíz de renderización, @queryAll
mostrará una lista de nodos.
En esta sección, aprenderás a mediar el estado entre componentes de Lit.
Componentes reutilizables
React imita las canalizaciones funcionales de renderización con el flujo de datos de arriba hacia abajo. Los padres les proporcionan estado a los hijos mediante props. Los hijos se comunican con los padres mediante devoluciones de llamada que se encuentran en las props.
const CounterButton = (props) => {
const label = props.step < 0
? `- ${-1 * props.step}`
: `+ ${props.step}`;
return (
<button
onClick={() =>
props.addToCounter(props.step)}>{label}</button>
);
};
En el ejemplo anterior, un componente de React hace lo siguiente:
- Crea una etiqueta basada en el valor
props.step
. - Renderiza un botón con la etiqueta +step o -step.
- Actualiza el componente padre llamando a
props.addToCounter
conprops.step
como un argumento cuando se hace clic.
Aunque es posible pasar devoluciones de llamada en Lit, los patrones convencionales son diferentes. El componente de React del ejemplo anterior se podría escribir como un componente de Lit en el siguiente ejemplo:
@customElement('counter-button')
export class CounterButton extends LitElement {
@property({type: Number}) step: number = 0;
onClick() {
const event = new CustomEvent('update-counter', {
bubbles: true,
detail: {
step: this.step,
}
});
this.dispatchEvent(event);
}
render() {
const label = this.step < 0
? `- ${-1 * this.step}` // "- 1"
: `+ ${this.step}`; // "+ 1"
return html`
<button @click=${this.onClick}>${label}</button>
`;
}
}
En el ejemplo anterior, un componente de Lit hará lo siguiente:
- Crear la propiedad reactiva
step
- Enviar un evento personalizado llamado
update-counter
que contiene el valorstep
del elemento cuando se hace clic en él
Los eventos del navegador pasan de los elementos hijos a los elementos padres. Los eventos permiten que los hijos transmitan eventos de interacción y cambios de estado. React pasa el estado básicamente en la dirección opuesta, de modo que es poco común ver que los componentes de React envíen y escuchen los eventos de la misma manera que los de Lit.
Componentes con estado
En React, es común usar hooks para administrar el estado. Para crear un componente MyCounter
, se puede reutilizar el componente CounterButton
. Observa cómo se pasa addToCounter
a ambas instancias de CounterButton
.
const MyCounter = (props) => {
const [counterSum, setCounterSum] = React.useState(0);
const addToCounter = useCallback(
(step) => {
setCounterSum(counterSum + step);
},
[counterSum, setCounterSum]
);
return (
<div>
<h3>Σ: {counterSum}</h3>
<CounterButton
step={-1}
addToCounter={addToCounter} />
<CounterButton
step={1}
addToCounter={addToCounter} />
</div>
);
};
En el ejemplo anterior, se hace lo siguiente:
- Se crea un estado
count
. - Se crea una devolución de llamada que agregue un número a un estado
count
. CounterButton
utilizaaddToCounter
para actualizarcount
mediantestep
con cada clic.
Se puede lograr una implementación similar de MyCounter
en Lit. Observa cómo addToCounter
no se pasa a counter-button
. En su lugar, la devolución de llamada está vinculada como objeto de escucha de eventos al evento @update-counter
en un elemento padre.
@customElement("my-counter")
export class MyCounter extends LitElement {
@property({type: Number}) count = 0;
addToCounter(e: CustomEvent<{step: number}>) {
// Get step from detail of event or via @query
this.count += e.detail.step;
}
render() {
return html`
<div @update-counter="${this.addToCounter}">
<h3>Σ ${this.count}</h3>
<counter-button step="-1"></counter-button>
<counter-button step="1"></counter-button>
</div>
`;
}
}
En el ejemplo anterior, se hace lo siguiente:
- Se crea una propiedad reactiva llamada
count
que actualizará el componente cuando se modifique el valor. - Se vincula la devolución de llamada
addToCounter
con el objeto de escucha de eventos@update-counter
. - Se actualiza
count
agregando el valor que se encuentra en eldetail.step
del eventoupdate-counter
. - Se establece el valor
step
decounter-button
mediante el atributostep
.
Es más común usar propiedades reactivas en Lit para transmitir cambios de padres a hijos. Del mismo modo, se recomienda utilizar el sistema de eventos del navegador para pasar los detalles de abajo hacia arriba.
Este enfoque sigue las prácticas recomendadas y cumple con el objetivo de Lit de proporcionar compatibilidad multiplataforma para componentes web.
En esta sección, aprenderás cómo aplicar estilos en Lit.
Aplica estilos
Lit ofrece varias formas de aplicar estilos a los elementos como una solución integrada.
Estilos intercalados
Lit es compatible con estilos intercalados y permite vincularlos.
import {LitElement, html, css} from 'lit';
import {customElement} from 'lit/decorators.js';
@customElement('my-element')
class MyElement extends LitElement {
render() {
return html`
<div>
<h1 style="color:orange;">This text is orange</h1>
<h1 style="color:rebeccapurple;">This text is rebeccapurple</h1>
</div>
`;
}
}
En el ejemplo anterior, hay 2 encabezados, cada uno con un estilo intercalado.
Ahora intenta vincular un border: 1px solid black
al texto naranja:
<h1 style="color:orange;${'border: 1px solid black;'}">This text is orange</h1>
Es posible que calcular el string del estilo en cada ocasión sea un poco molesto, por lo que Lit ofrece una directiva para ayudarte con esto.
styleMap
La directiva styleMap
facilita el uso de JavaScript para configurar estilos en línea. Por ejemplo:
import {LitElement, html, css} from 'lit';
import {customElement, property} from 'lit/decorators.js';
import {styleMap} from 'lit/directives/style-map';
@customElement('my-element')
class MyElement extends LitElement {
@property({type: String})
color = '#000'
render() {
// Define the styleMap
const headerStyle = styleMap({
'border-color': this.color,
});
return html`
<div>
<h1
style="border-style:solid;
<!-- Use the styleMap -->
border-width:2px;${headerStyle}">
This div has a border color of ${this.color}
</h1>
<input
type="color"
@input=${e => (this.color = e.target.value)}
value="#000">
</div>
`;
}
}
En el ejemplo anterior, se hace lo siguiente:
- Se muestra un
h1
con un borde y un selector de color. - Se cambia el
border-color
al valor del selector de color.
Además, existe styleMap
, que se usa para establecer los estilos de h1
. styleMap
sigue una sintaxis similar a la sintaxis de vinculación de atributos de style
de React.
CSSResult
La forma recomendada de aplicar estilos a los componentes es usar el literal de plantilla etiquetado css
.
import {LitElement, html, css} from 'lit';
import {customElement} from 'lit/decorators.js';
const ORANGE = css`orange`;
@customElement('my-element')
class MyElement extends LitElement {
static styles = [
css`
#orange {
color: ${ORANGE};
}
#purple {
color: rebeccapurple;
}
`
];
render() {
return html`
<div>
<h1 id="orange">This text is orange</h1>
<h1 id="purple">This text is rebeccapurple</h1>
</div>
`;
}
}
En el ejemplo anterior, se hace lo siguiente:
- Se declara un literal de plantilla etiquetado de CSS con una vinculación.
- Se establecen los colores de dos
h1
con ID.
Beneficios de usar la etiqueta de plantilla css
:
- Se analiza una vez por clase en lugar de una vez por instancia.
- Se implementa teniendo en cuenta la capacidad de reutilización del módulo.
- Puede separar los estilos en sus propios archivos con facilidad.
- Es compatible con el polyfill de propiedades personalizadas de CSS.
Además, analiza la etiqueta <style>
en index.html
:
<!-- index.html -->
<style>
h1 {
color: red !important;
}
</style>
Lit encapsulará los estilos de tus componentes en sus raíces. Esto significa que los estilos no se filtrarán ni fugarán. Para pasar estilos a los componentes, el equipo de Lit recomienda usar propiedades personalizadas de CSS, ya que pueden penetrar la encapsulación de estilos de Lit.
Etiquetas de estilo
También es posible incluir etiquetas <style>
en línea en las plantillas. El navegador anulará la duplicación de estas etiquetas de estilo, pero si las ubica en tus plantillas, se analizarán por instancia de componente en lugar de hacerlo por clase, como sucede con la plantilla etiquetada css
. Además, la anulación de duplicación que realiza el navegador de CSSResult
es mucho más rápida.
Etiquetas de vínculos
El uso de un <link rel="stylesheet">
en tu plantilla también es adecuado para los estilos, pero esto tampoco se recomienda porque podría provocar una carga inicial del contenido sin estilo (FOUC).
JSX y creación de plantillas
Lit y DOM virtual
Lit-html no incluye un DOM virtual convencional que compara cada nodo individual. En cambio, usa funciones de rendimiento intrínsecas a la especificación del literal de plantillas etiquetado de ES2015. Los literales de plantilla etiquetados son strings de literales de plantilla con funciones de etiqueta.
A continuación, se muestra un ejemplo de un literal de plantilla:
const str = 'string';
console.log(`This is a template literal ${str}`);
Este es un ejemplo de un literal de plantilla etiquetado:
const tag = (strings, ...values) => ({strings, values});
const f = (x) => tag`hello ${x} how are you`;
console.log(f('world')); // {strings: ["hello ", " how are you"], values: ["world"]}
console.log(f('world').strings === f(1 + 2).strings); // true
En el ejemplo anterior, la etiqueta es la función tag
y la función f
muestra una invocación de un literal de plantilla etiquetado.
Gran parte de la magia del rendimiento de Lit proviene del hecho de que los arrays de strings que se pasan a la función de etiqueta tienen el mismo puntero (como se muestra en el segundo console.log
). El navegador no vuelve a crear un nuevo array strings
en cada invocación de la función de la etiqueta debido a que usa el mismo literal de plantilla (es decir, en la misma ubicación en el AST). Por lo tanto, la vinculación, el análisis y el almacenamiento en caché de plantillas de Lit pueden aprovechar estas funciones sin demasiada sobrecarga del tiempo de ejecución.
Este comportamiento del navegador incorporado de los literales de plantilla etiquetados le otorga a Lit una ventaja de rendimiento. La mayoría de los DOM virtuales convencionales realizan la mayor parte del trabajo en JavaScript. Sin embargo, los literales de plantilla etiquetados realizan la mayor parte de la comparación en el C++ del navegador.
Si quieres comenzar a usar literales de plantilla con etiquetas HTML con React o Preact, el equipo de Lit recomienda la biblioteca htm
.
Sin embargo, como en el caso del sitio de Google Codelabs y varios editores de código en línea, observarás que destacar la sintaxis del literal de plantilla etiquetado no es muy común. Algunos IDE y editores de texto son compatibles con esta función de forma predeterminada, como el resaltador de bloques de código de Atom y GitHub. El equipo de Lit también trabaja muy estrechamente con la comunidad para mantener proyectos como lit-plugin
, que es un complemento de VS Code que agregará el resaltado de sintaxis, la comprobación de tipos y la función intellisense a los proyectos de Lit.
Lit y JSX + React DOM
JSX no se ejecuta en el navegador, sino que usa un preprocesador para convertir JSX en llamadas a funciones de JavaScript (generalmente mediante Babel).
Por ejemplo, Babel transformará lo siguiente:
const element = <div className="title">Hello World!</div>;
ReactDOM.render(element, mountNode);
en esto:
const element = React.createElement('div', {className: 'title'}, 'Hello World!');
ReactDOM.render(element, mountNode);
Luego, el DOM de React toma la salida de React y la convierte en un DOM real, con propiedades, atributos, objetos de escucha de eventos, entre otros.
Lit-html usa literales de plantilla etiquetados que se pueden ejecutar en el navegador sin transpilación ni un preprocesador. Esto significa que para comenzar con Lit, todo lo que necesitas es un archivo HTML, una secuencia de comandos del módulo ES y un servidor. A continuación, se muestra una secuencia de comandos que se puede ejecutar por completo en el navegador:
<!DOCTYPE html>
<html>
<head>
<script type="module">
import {html, render} from 'https://cdn.skypack.dev/lit';
render(
html`<div>Hello World!</div>`,
document.querySelector('.root')
)
</script>
</head>
<body>
<div class="root"></div>
</body>
</html>
Además, debido a que el sistema de plantillas de Lit, lit-html, no utiliza un DOM virtual convencional, sino que utiliza la API de DOM de manera directa, el tamaño de Lit 2 reducido y comprimido en gzip pesa menos de 5 KB, poco en comparación con los 40 KB reducidos y comprimidos en gzip de React (2.8 KB) + react-dom (39.4 KB).
Eventos
React usa un sistema de eventos sintéticos. Esto significa que react-dom debe definir cada evento que se usará en todos los componentes y proporcionar un objeto de escucha de eventos camelCase equivalente para cada tipo de nodo. Como resultado, JSX no tiene un método para definir un objeto de escucha de eventos para un evento personalizado, por lo que los desarrolladores deben usar ref
y, luego, aplicar un objeto de escucha de manera obligatoria. Esto crea una experiencia para el desarrollador poco satisfactoria al momento de integrar bibliotecas que no se idearon para usarlas con React. Por lo tanto, esto genera la necesidad de codificar un wrapper específico para React.
Lit-html accede directamente al DOM y usa eventos nativos, por lo que agregar objetos de escucha de eventos es tan fácil como @event-name=${eventNameListener}
. Esto significa que se realizan menos análisis del tiempo de ejecución para agregar objetos de escucha de eventos y ejecutar eventos.
Componentes y props
Componentes de React y elementos personalizados
En niveles más profundos, LitElement usa elementos personalizados para empaquetar sus componentes. Los elementos personalizados presentan algunos beneficios entre los componentes de React cuando se trata de la componentización (en la sección Estado y ciclo de vida se explican estos dos temas en mayor detalle).
Estas son algunas de las ventajas que tienen los elementos personalizados por ser un sistema de componentes:
- Son nativos del navegador y no requieren ninguna herramienta.
- Se adaptan a todas las API de navegadores desde
innerHTML
ydocument.createElement
hastaquerySelector
. - Por lo general, se pueden usar en diferentes frameworks.
- Se pueden registrar de forma diferida con
customElements.define
y el DOM "hydrate".
Estas son algunas desventajas de los elementos personalizados en comparación con los componentes de React:
- No pueden crear un elemento personalizado sin definir una clase (por ende, no se pueden crear componentes funcionales similares a JSX).
- Deben contener una etiqueta de cierre.
- Nota: A pesar de que es conveniente para desarrollar, los proveedores de navegadores por lo general evitan la especificación de la etiqueta de cierre automático, por lo que las especificaciones más nuevas no suelen incluir este tipo de etiquetas.
- Introducen un nodo adicional en el árbol del DOM, lo que puede provocar problemas de diseño.
- Se deben registrar mediante JavaScript.
Lit prefirió utilizar elementos personalizados sobre un sistema personalizado de elementos porque están integrados en el navegador, y el equipo de Lit considera que los beneficios entre los frameworks superan los beneficios que brinda una capa de abstracción de componentes. De hecho, los esfuerzos del equipo de Lit en el espacio lit-ssr lograron superar los problemas principales en el registro de JavaScript. Además, algunas empresas, como GitHub, aprovechan el registro diferido de elementos personalizados para mejorar las páginas de forma progresiva con un estilo opcional.
Cómo pasar datos a elementos personalizados
Un concepto erróneo común de los elementos personalizados es que los datos solo se pueden pasar como strings. Es posible que este concepto erróneo se deba al hecho de que los atributos de los elementos solo se pueden codificar como strings. Aunque es cierto que Lit transmitirá atributos de string a los tipos definidos, los elementos personalizados también pueden aceptar datos complejos como propiedades.
Por ejemplo, con la siguiente definición de LitElement:
// data-test.ts
import {LitElement, html} from 'lit';
import {customElement, property} from 'lit/decorators.js';
@customElement('data-test')
class DataTest extends LitElement {
@property({type: Number})
num = 0;
@property({attribute: false})
data = {a: 0, b: null, c: [html`<div>hello</div>`, html`<div>world</div>`]}
render() {
return html`
<div>num + 1 = ${this.num + 1}</div>
<div>data.a = ${this.data.a}</div>
<div>data.b = ${this.data.b}</div>
<div>data.c = ${this.data.c}</div>`;
}
}
Se define una propiedad básica reactiva num
, que convertirá el valor de string de un atributo en un number
. Luego, se introduce la estructura de datos compleja con attribute:false
, que desactiva el manejo de atributos de Lit.
A continuación, se muestra cómo pasar datos a este elemento personalizado:
<head>
<script type="module">
import './data-test.js'; // loads element definition
import {html} from './data-test.js';
const el = document.querySelector('data-test');
el.data = {
a: 5,
b: null,
c: [html`<div>foo</div>`,html`<div>bar</div>`]
};
</script>
</head>
<body>
<data-test num="5"></data-test>
</body>
Estado y ciclo de vida
Otras devoluciones de llamada de ciclo de vida de React
static getDerivedStateFromProps
En Lit, no hay equivalente debido a que las props y el estado son propiedades de la misma clase.
shouldComponentUpdate
- El equivalente en Lit es
shouldUpdate
. - Se llama en la primera renderización, a diferencia de React.
- Es una función similar a
shouldComponentUpdate
de React.
getSnapshotBeforeUpdate
En Lit, getSnapshotBeforeUpdate
es similar a update
y willUpdate
.
willUpdate
- Se llama antes que a
update
. - A diferencia de
getSnapshotBeforeUpdate
, se llama awillUpdate
antes querender
. - Los cambios en las propiedades reactivas de
willUpdate
no vuelven a activar el ciclo de actualización. - Es un buen lugar para calcular los valores de las propiedades que dependen de otras y se usan en el resto del proceso de actualización.
- Se llama a este método en el servidor de SSR, por lo que no se recomienda acceder al DOM aquí.
update
- Se llama después de
willUpdate
. - A diferencia de
getSnapshotBeforeUpdate
, se llama aupdate
antes querender
. - Los cambios en las propiedades reactivas de
update
no vuelven a activar el ciclo de actualización si se cambian antes de llamar asuper.update
. - Es un buen lugar para capturar información del DOM que rodea al componente antes de que la salida renderizada se confirme en el DOM.
- No se llama a este método en el servidor de SSR.
Otras devoluciones de llamada de ciclo de vida de Lit
Hay varias devoluciones de llamada de ciclo de vida que no se mencionaron en la sección anterior porque no hay un análogo en React. Son las siguientes:
attributeChangedCallback
Se invoca cuando cambia uno de los observedAttributes
del elemento. observedAttributes
y attributeChangedCallback
forman parte de las especificaciones de los elementos personalizados que implementa Lit de manera subyacente para proporcionar una API de atributos para elementos de Lit.
adoptedCallback
Se invoca cuando el componente se traslada a un nuevo documento, como p. ej., del documentFragment
de un HTMLTemplateElement
al document
principal. Esta devolución de llamada también forma parte de las especificaciones de los elementos personalizados y solo se debería usar para casos de uso avanzados cuando el componente cambia los documentos.
Otros métodos y propiedades del ciclo de vida
Estos métodos y propiedades son miembros de una clase que puedes llamar, anular o esperar para ayudar a manipular el proceso del ciclo de vida.
updateComplete
Este es un Promise
que se resuelve cuando el elemento termina de actualizarse, ya que los ciclos de vida de la actualización y la renderización son asíncronos. Ejemplo:
async nextButtonClicked() {
this.step++;
// Wait for the next "step" state to render
await this.updateComplete;
this.dispatchEvent(new Event('step-rendered'));
}
getUpdateComplete
Este es un método que se debe anular para personalizar cuando se resuelve updateComplete
. Por ejemplo, esto es común cuando un componente renderiza un componente hijo y los ciclos de renderización deben estar sincronizados, p. ej.:
class MyElement extends LitElement {
...
async getUpdateComplete() {
await super.getUpdateComplete();
await this.myChild.updateComplete;
}
}
performUpdate
Este método es el que llama a las devoluciones de llamada del ciclo de vida de la actualización. Esto en general no es necesario, excepto en casos excepcionales en los que la actualización se debe realizar de forma síncrona o para una programación personalizada.
hasUpdated
Esta propiedad es true
si el componente se actualizó al menos una vez.
isConnected
Como parte de las especificaciones de los elementos personalizados, esta propiedad será true
si el elemento está conectado actualmente al árbol de documentos principal.
Visualización del ciclo de vida de actualización de Lit
El proceso de actualización consta de 3 partes:
- Paso previo a la actualización
- Actualización
- Paso posterior a la actualización
Paso previo a la actualización
Después de requestUpdate
, se espera una actualización programada.
Actualización
Paso posterior a la actualización
Hooks
¿Por qué hooks?
Los hooks se introdujeron en React para los casos de uso de componentes de funciones simples que requerían un estado. En muchos casos simples, los componentes de funciones con hooks suelen ser mucho más sencillos y legibles que sus equivalentes de componentes de clase. Sin embargo, al momento de introducir actualizaciones de estado asíncronas y pasar datos entre hooks o efectos, el patrón de hooks no suele ser suficiente, mientras que una solución basada en clases como los controladores reactivos suele tener éxito.
Hooks y controladores de solicitud a la API
Es común codificar un hook que solicite datos de una API. Por ejemplo, analiza este componente de función de React que realiza lo siguiente:
index.tsx
- Renderiza texto
- Renderiza la respuesta de
useAPI
- ID de usuario + nombre de usuario
- Mensaje de error
- 404 cuando llega al usuario 11 (por diseño)
- Error de anulación si se anula la recuperación de la API
- Cargando mensaje
- Renderiza un botón de acción
- Próximo usuario: recupera la API para el próximo usuario
- Cancelar: anula la recuperación de la API y muestra un error
useApi.tsx
- Define un hook personalizado de
useApi
- Recuperará de forma asíncrona un objeto de usuario desde una API
- Emite:
- Nombre de usuario
- Si se carga la recuperación
- Cualquier mensaje de error
- Una devolución de llamada para anular la recuperación
- Anula las recuperaciones en curso si se desmonta
- Define un hook personalizado de
Esta es la implementación de Lit + el controlador reactivo.
Conclusiones:
- Los controladores reactivos son más parecidos a los hooks personalizados.
- Pasan datos no renderizados entre devoluciones de llamadas y efectos.
- React usa
useRef
para pasar datos entreuseEffect
yuseCallback
. - Lit usa una propiedad de clase privada.
- React básicamente imita el comportamiento de una propiedad de clase privada.
- React usa
Hijos
Slot predeterminado
Cuando no se le asigna un atributo slot
a los elementos HTML, se asignan al slot sin nombre predeterminado. En el siguiente ejemplo, MyApp
se colocará un párrafo en un slot con nombre. El otro párrafo se colocará de manera predeterminada en el slot sin nombre.
@customElement("my-element")
export class MyElement extends LitElement {
render() {
return html`
<section>
<div>
<slot></slot>
</div>
<div>
<slot name="custom-slot"></slot>
</div>
</section>
`;
}
}
@customElement("my-app")
export class MyApp extends LitElement {
render() {
return html`
<my-element>
<p slot="custom-slot">
This paragraph will be placed in the custom-slot!
</p>
<p>
This paragraph will be placed in the unnamed default slot!
</p>
</my-element>
`;
}
}
Actualizaciones de slots
Cuando cambia la estructura de los subordinados de slots, se activa un evento slotchange
. Un componente de Lit puede vincular un objeto de escucha de eventos a un evento slotchange
. En el siguiente ejemplo, el primer slot que se encuentra en shadowRoot
tendrá los assignedNodes registrados en la consola en slotchange
.
@customElement("my-element")
export class MyElement extends LitElement {
onSlotChange(e: Event) {
const slot = this.shadowRoot.querySelector('slot');
console.log(slot.assignedNodes({flatten: true}));
}
render() {
return html`
<section>
<div>
<slot @slotchange="{this.onSlotChange}"></slot>
</div>
</section>
`;
}
}
Refs
Generación de referencias
Lit y React muestran una referencia a un HTMLElement después de que se haya llamado a las funciones render
. Sin embargo, vale la pena revisar cómo React y Lit conforman el DOM que luego se muestra mediante un decorador @query
de Lit o una referencia de React.
React es una canalización funcional que crea componentes de React, no HTMLElements. Como una ref se declara antes de que se renderice un HTMLElement, se asigna un espacio en la memoria. Es por esto que null
se ve como el valor inicial de una ref, dado que aún no se creó (o se renderizó) el elemento DOM real, es decir, useRef(null)
.
Una vez que ReactDOM convierte un componente de React en un HTMLElement, busca un atributo llamado ref
en ReactComponent. Si está disponible, ReactDOM coloca la referencia de HTMLElement en ref.current
.
LitElement usa la función de etiqueta de plantilla html
de lit-html para formular un elemento de plantilla de forma subyacente. LitElement graba el contenido de la plantilla en el shadow DOM de un elemento personalizado después de la renderización. El shadow DOM es un árbol del DOM con alcance limitado y encapsulado por una shadow root. Luego, el decorador @query
crea un método get para la propiedad que, en esencia, realiza un this.shadowRoot.querySelector
en la raíz con alcance limitado.
Consulta de múltiples elementos
En el siguiente ejemplo, el decorador @queryAll
mostrará los dos párrafos en la shadow root como NodeList
.
@customElement("my-element")
export class MyElement extends LitElement {
@queryAll('p')
paragraphs!: NodeList;
render() {
return html`
<p>Hello, world!</p>
<p>How are you?</p>
`;
}
}
Básicamente, @queryAll
crea un método get para paragraphs
que muestra los resultados de this.shadowRoot.querySelectorAll()
. En JavaScript, se puede declarar un método get para cumplir el mismo propósito:
get paragraphs() {
return this.renderRoot.querySelectorAll('p');
}
Consulta de elementos que cambian
El decorador @queryAsync
es más adecuado para controlar un nodo que puede cambiar según el estado de otra propiedad de un elemento.
En el siguiente ejemplo, @queryAsync
encontrará el primer elemento de un párrafo. Sin embargo, el elemento de un párrafo solo se renderizará cuando renderParagraph
genere un número impar de manera aleatoria. La directiva @queryAsync
mostrará una promesa que se resolverá cuando esté disponible el primer párrafo.
@customElement("my-dissappearing-paragraph")
export class MyDisapppearingParagraph extends LitElement {
@queryAsync('p')
paragraph!: Promise<HTMLElement>;
renderParagraph() {
const randomNumber = Math.floor(Math.random() * 10)
if (randomNumber % 2 === 0) {
return "";
}
return html`<p>This checkbox is checked!`
}
render() {
return html`
${this.renderParagraph()}
`;
}
}
Mediación del estado
En React, la costumbre es usar devoluciones de llamada porque React mismo realiza la mediación del estado. React hace todo lo posible para no depender del estado que proporcionan los elementos. El DOM es simplemente un efecto del proceso de renderización.
Estado externo
Es posible usar Redux, MobX o cualquier otra biblioteca de administración de estado junto con Lit.
Los componentes de Lit se crean dentro del alcance del navegador. Por lo tanto, todas las bibliotecas que también existan en el alcance del navegador están disponible para Lit. Muchas bibliotecas asombrosas se compilaron para usar sistemas de administración del estado existentes en Lit.
Esta es una serie de Vaadin que explica cómo aprovechar Redux en un componente de Lit.
Echa un vistazo a lit-mobx de Adobe para descubrir cómo un sitio a gran escala puede aprovechar MobX en Lit.
Además, consulta Apollo Elements para saber cómo los desarrolladores incluyen GraphQL en los componentes web.
Lit opera con funciones nativas del navegador, y la mayoría de las soluciones de administración del estado en el alcance del navegador se pueden usar en un componente de Lit.
Aplica estilos
Shadow DOM
Para encapsular estilos y DOM de forma nativa dentro de un elemento personalizado, Lit usa el Shadow DOM. Las shadow roots generan un shadow tree separado del árbol del documento principal. Esto significa que la mayoría de los estilos se encuentran dentro de este documento. Algunos estilos sí se filtran, como el color y otros estilos relacionados con la fuente.
El Shadow DOM también presenta nuevos conceptos y selectores para la especificación de CSS:
:host, :host(:hover), :host([hover]) {
/* Styles the element in which the shadow root is attached to */
}
slot[name="title"]::slotted(*), slot::slotted(:hover), slot::slotted([hover]) {
/*
* Styles the elements projected into a slot element. NOTE: the spec only allows
* styling the direcly slotted elements. Children of those elements are not stylable.
*/
}
Cómo compartir estilos
Lit facilita el uso compartido de estilos entre componentes en forma de CSSTemplateResults
a través de etiquetas de plantilla css
. Por ejemplo:
// typography.ts
export const body1 = css`
.body1 {
...
}
`;
// my-el.ts
import {body1} from './typography.ts';
@customElement('my-el')
class MyEl Extends {
static get styles = [
body1,
css`/* local styles come after so they will override bod1 */`
]
render() {
return html`<div class="body1">...</div>`
}
}
Temas
Las shadow roots representan un desafío para los temas convencionales, que suelen ser enfoques de etiquetas de estilo de arriba hacia abajo. La manera convencional de abordar los temas con componentes web que usan Shadow DOM es exponer una API de estilo mediante propiedades personalizadas de CSS. Por ejemplo, este es un patrón que usa Material Design:
.mdc-textfield-outline {
border-color: var(--mdc-theme-primary, /* default value */ #...);
}
.mdc-textfield--input {
caret-color: var(--mdc-theme-primary, #...);
}
Luego, el usuario cambia el tema del sitio mediante el uso de valores de propiedad personalizada:
html {
--mdc-theme-primary: #F00;
}
html[dark] {
--mdc-theme-primary: #F88;
}
Si los temas de arriba hacia abajo son obligatorios y no puedes exponer estilos, siempre es posible inhabilitar el Shadow DOM; para ello, se debe anular createRenderRoot
a fin de que se muestre this
, que luego renderizará la plantilla de tus componentes al elemento personalizado en sí en lugar de a una shadow root conectada al elemento personalizado. De esta forma, perderás el encapsulamiento de estilo, el encapsulamiento del DOM y los slots.
Producción
IE 11
Si necesitas compatibilidad con navegadores más antiguos, como IE 11, tendrás que cargar algunos polyfills que pesan otros 33 KB. Puedes obtener más información aquí.
Paquetes condicionales
El equipo de Lit recomienda entregar dos paquetes diferentes: uno para IE 11 y otro para los navegadores actualizados. Esto tiene varios beneficios:
- Entregar ES 6 es más rápido y satisfacerá a la mayoría de tus clientes.
- El ES 5 transpilado aumenta de manera significativa el tamaño del paquete.
- Los paquetes condicionales te ofrecen lo mejor de ambos mundos.
- Son compatibles con IE 11.
- Los navegadores modernos no presentan demoras.
Puedes obtener más información sobre cómo compilar un paquete entregado de forma condicional en nuestro sitio de documentación aquí.