Lit para desenvolvedores do React

O que é o Lit

O Lit é um conjunto de bibliotecas de código aberto do Google que ajuda os desenvolvedores a criarem componentes rápidos e leves que funcionam em qualquer framework. Você pode criar componentes, aplicativos, sistemas de design e outros recursos compartilháveis usando o Lit.

O que você aprenderá

Como converter vários conceitos do React para o Lit, como:

  • JSX e modelos
  • Componentes e propriedades
  • Estado e ciclo de vida
  • Hooks
  • Filhos
  • Referências
  • Estado de mediação

O que você criará

Ao final deste codelab, você conseguirá converter conceitos de componentes do React para os equivalentes do Lit.

O que é necessário

  • A versão mais recente do Chrome, Safari, Firefox ou Edge
  • Conhecimento sobre HTML, CSS, JavaScript e Chrome DevTools
  • Conhecimento sobre o React
  • Avançado: caso queira ter a melhor experiência de desenvolvimento, faça o download do VS Code. Você também precisará do lit-plugin para VS Code e NPM.

Os principais conceitos e recursos do Lit são parecidos com o React, mas o Lit tem algumas diferenças essenciais:

Ele é pequeno

O Lit é muito pequeno. Ele tem aproximadamente 5 KB quando minificado e compactado (.gzip), comparado aos mais de 40 KB do React + ReactDOM.

Gráfico de barras com o tamanho de pacote minificado e compactado em KB. A barra do Lit tem 5 KB e a do React + React DOM tem 42,2 KB.

Ele é rápido

Em comparativos públicos entre o lit-html, o sistema de modelos do Lit, e o VDOM do React, o lit-html se mostrou de 8 a 10% mais rápido que o React no pior cenário e mais do que 50% mais rápido nos casos de uso mais comuns.

O LitElement, a classe base de componentes do Lit, gera uma sobrecarga mínima para o lit-html, mas apresenta um desempenho de 16 a 30% melhor que o do React, em relação aos recursos de componentes, como uso de memória e tempos de interação e inicialização.

Gráfico de barras agrupadas comparando o desempenho do Lit com o do React em milissegundos (quanto mais baixo, melhor).

Não exige a compilação

Com os novos recursos de navegador, como módulos ES e literais de modelo com tag, o Lit não exige a compilação para ser executado. Isso significa que para configurar os ambientes de desenvolvimento você só precisa de uma tag de script, um navegador e um servidor.

Com os módulos ES e as CDNs modernas, como a Skypack ou a UNPKG, talvez você nem precise de um NPM para começar.

No entanto, se preferir, você ainda pode criar e otimizar o código do Lit. A consolidação do desenvolvedor recente referente aos módulos ES nativos trouxe bons resultados para o Lit. O Lit é composto apenas por JavaScript comum e não requer CLIs específicas de framework ou processamento de build.

É independente do framework

Os componentes do Lit têm como base um conjunto de padrões da Web, conhecidos como componentes da Web. Isso significa que um componente criado no Lit funcionará em frameworks atuais e futuros. Se ele for compatível com elementos HTML, será compatível com os componentes da Web.

Os únicos problemas com a interoperabilidade de framework ocorrem quando os frameworks têm compatibilidade restritiva com o DOM. Um deles é o React, no entanto, ele aceita escapes usando referências, que não oferecem uma boa experiência para desenvolvedores no React.

A equipe do Lit está trabalhando em um projeto experimental chamado @lit-labs/react, que analisará automaticamente os componentes Lit e gerará um wrapper do React para que você não precise usar referências.

Além disso, o Custom Elements Everywhere mostrará quais frameworks e bibliotecas funcionam bem com elementos personalizados.

Compatibilidade com TypeScript de primeira classe

Embora seja possível programar todo o código do Lit em JavaScript, o Lit foi desenvolvido em TypeScript e a equipe recomenda que os desenvolvedores também usem o TypeScript.

A equipe do Lit está trabalhando com a comunidade para ajudar a manter os projetos que fornecem a verificação de tipo e intellisense do TypeScript para modelos do Lit tanto no momento do desenvolvimento quanto no da compilação, usando o lit-analyzer e o lit-plugin.

Captura de tela de um ambiente de desenvolvimento integrado mostrando uma verificação de tipo incorreta para definir o booleano indicado como um número.

Captura de tela de um ambiente de desenvolvimento integrado mostrando sugestões do intellisense.

As ferramentas para desenvolvedores são integradas ao navegador

Os componentes Lit são apenas elementos HTML no DOM. Isso significa que, para inspecionar os componentes, não é necessário instalar ferramentas ou extensões no navegador.

Basta abrir as ferramentas para desenvolvedores, selecionar um elemento e explorar as propriedades ou o estado.

Imagem das ferramentas para desenvolvedores do Chrome mostrando que &quot;$0&quot; retorna &quot;<mwc-textfield>&quot;, &quot;$0.value&quot; retorna &quot;hello world&quot;, &quot;$0.outlined&quot; retorna &quot;true&quot; e &quot;{$0}&quot; exibe uma expansão de propriedade.

Ele foi criado considerando a renderização do lado do servidor (SSR, na sigla em inglês)

O Lit 2 foi criado considerando a compatibilidade com SSR. Até o momento da preparação deste codelab, a equipe do Lit ainda não lançou as ferramentas de SSR em um formato estável, mas já implantou componentes renderizados do lado do servidor em produtos do Google. A equipe do Lit espera lançar essas ferramentas no GitHub em breve.

Enquanto isso, você pode acompanhar o progresso da equipe do Lit neste link.

A adesão é fácil

Você não precisa se dedicar somente ao Lit para conseguir usá-lo. É possível criar componentes no Lit e adicioná-los ao seu projeto existente. Se não gostar deles, não é necessário converter o app inteiro de uma só vez, já que os componentes da Web funcionam em outros frameworks.

Você criou um app inteiro em Lit e agora quer mudar para outro formato? É só mover o aplicativo Lit atual para o novo framework e migrar os elementos que você quiser para os componentes do novo framework.

Além disso, muitos frameworks modernos são compatíveis com saídas de componentes da Web, o que significa que eles geralmente cabem em um elemento do Lit.

Existem duas formas de concluir este codelab:

  • Fazer tudo on-line, no navegador.
  • Avançado: fazer tudo na sua máquina local, usando o VS Code.

Como acessar o código

Ao longo deste codelab, você encontrará links para o playground do Lit, como este:

Checkpoint do código

O playground é um sandbox de código executado totalmente no navegador. Ele consegue compilar e executar arquivos TypeScript e JavaScript, além de resolver automaticamente importações para módulos de nós, como:

// before
import './my-file.js';
import 'lit';

// after
import './my-file.js';
import 'https://cdn.skypack.dev/lit';

Você pode seguir o tutorial completo no playground do Lit, usando esses checkpoints como pontos de partida. Caso esteja usando o VS Code, você poderá usar esses checkpoints para fazer o download do código inicial em qualquer etapa, além de usá-los para verificar seu trabalho.

Conhecendo a IU do playground do Lit

A barra do seletor de arquivos está marcada como &quot;Section 1&quot;, a seção de edição de código como &quot;Section 2&quot;, a visualização da saída como &quot;Section 3&quot; e o botão de recarregamento da visualização como &quot;Section 4&quot;.

A captura de tela da IU do playground do Lit destaca as seções que serão usadas neste codelab.

  1. Seletor de arquivos (observe que há um botão de adição)
  2. Editor de arquivos
  3. Visualização do código
  4. Botão "Reload"
  5. Botão "Download"

Configuração do VS Code (avançado)

Veja os benefícios de usar essa configuração do VS Code:

  • Verificação do tipo de modelo
  • Preenchimento automático e intellisense do modelo

Se você já tem um NPM, o VS Code (com o plug-in lit-plugin) instalado e sabe como usar esse ambiente, basta fazer o download e dar início aos projetos desta forma:

  • Pressione o botão de download.
  • Extraia o conteúdo do arquivo .tar em um diretório.
  • Para TS, defina uma quick tsconfig que gera módulos "es" e "es2015+".
  • Instale um servidor de desenvolvimento que possa resolver especificadores de módulo simples. A equipe do Lit recomenda usar @web/dev-server.
  • Execute o servidor de desenvolvimento e abra o navegador. Se você estiver usando @web/dev-server, utilize web-dev-server --node-resolve --watch --open.

Nesta seção, você aprenderá as noções básicas de modelos no Lit.

JSX e modelos do Lit

JSX é uma extensão de sintaxe para JavaScript que permite que os usuários do React criem modelos no código JavaScript facilmente. Os modelos do Lit têm uma finalidade semelhante: expressar a IU de um componente como uma função do estado correspondente.

Sintaxe básica

Checkpoint do código

No React, um app "Hello World" em JSX seria renderizado assim:

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
);

No exemplo acima, há dois elementos e uma variável "name" incluída. No Lit, isso seria feito desta forma:

import {html, render} from 'lit';

const name = 'Josh Perez';
const element = html`
  <h1>Hello, ${name}</h1>
  <div>How are you?</div>`;

render(
  element,
  mountNode
);

Os modelos do Lit não precisam de um fragmento do React para agrupar vários elementos.

No Lit, os modelos são encapsulados por um LITeral de modelo com tag html, o que explica o nome Lit.

Valores de modelos

Os modelos do Lit podem aceitar outros modelos Lit, conhecidos como TemplateResult. Por exemplo, coloque name entre tags de itálico (<i>) e depois entre literais de modelo com a tag N.B.. Use o sinal de acento grave (`) e não aspas 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
);

Os TemplateResults do Lit podem aceitar matrizes, strings, outros TemplateResults e diretivas.

Checkpoint do código

Como exercício, tente converter o código do React a seguir para 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
);

Resposta:

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
);

Como transmitir e definir propriedades

Checkpoint do código

Uma das maiores diferenças entre as sintaxes de JSX e Lit é a sintaxe de vinculação de dados. Por exemplo, considere esta entrada do React com vinculações:

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
);

No exemplo acima, foi definida uma entrada que:

  • define "disabled" como uma variável definida (no caso, "false");
  • define a classe como static-class mais uma variável (o caso, "static-class my-class");
  • define um valor padrão.

No Lit, isso seria feito desta forma:

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
);

No exemplo do Lit, uma vinculação booleana é adicionada para alternar o atributo disabled.

Em seguida, há uma vinculação direta ao atributo class, em vez de className. Várias vinculações podem ser adicionadas ao atributo class, a menos que você esteja usando a diretiva classMap, que é um auxiliar declarativo para alternar classes.

Por fim, a propriedade value é definida na entrada. Diferente do que ocorre no React, isso não define o elemento de entrada como somente leitura, já que segue a implementação nativa e o comportamento de entrada.

Propriedades de sintaxe de vinculação do Lit

html`<my-element ?attribute-name=${booleanVar}>`;
  • O prefixo ? é a sintaxe de vinculação para alternar um atributo em um elemento.
  • É equivalente a inputRef.toggleAttribute('attribute-name', booleanVar).
  • Ele é útil para elementos que usam disabled como disabled="false" e ainda são lidos como "true" pelo DOM porque inputElement.hasAttribute('disabled') === true.
html`<my-element .property-name=${anyVar}>`;
  • O prefixo . é a sintaxe de vinculação para definir a propriedade de um elemento.
  • É equivalente a inputRef.propertyName = anyVar.
  • Ele é útil para transmitir dados complexos, como objetos, matrizes ou classes.
html`<my-element attribute-name=${stringVar}>`;
  • Ele faz a vinculação ao atributo de um elemento.
  • É equivalente a inputRef.setAttribute('attribute-name', stringVar).
  • Ele é útil para valores básicos, seletores de regras de estilo e querySelectors.

Como transmitir gerenciadores

Checkpoint do código

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
);

No exemplo acima, foi definida uma entrada que:

  • registra a palavra "click" quando a entrada recebe um clique;
  • registra o valor da entrada quando o usuário digita um caractere.

No Lit, isso seria feito desta forma:

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
);

No exemplo do Lit, um listener foi adicionado ao evento click usando @click.

Em seguida, em vez de usar onChange, há uma vinculação do evento input nativo da <input>, já que o evento change nativo só é disparado em blur. O React abstrai esses eventos.

Sintaxe do manipulador de eventos do Lit

html`<my-element @event-name=${() => {...}}></my-element>`;
  • O prefixo @ é a sintaxe de vinculação de um listener de eventos.
  • Ele é equivalente a inputRef.addEventListener('event-name', ...).
  • Ele usa nomes de eventos DOM nativos.

Nesta seção, você aprenderá sobre os componentes e as funções de classe do Lit. O estado e os hooks são abordados de forma mais detalhada nas próximas seções.

Componentes de classe e LitElement

Checkpoint do código (TS) Checkpoint do código (JS)

O LitElement é equivalente a um componente de classe do React no Lit. O conceito de "propriedades reativas" do Lit é uma combinação de propriedades e estado do React. Por exemplo:

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
);

No exemplo acima, há um componente React que:

  • renderiza um name;
  • define o valor padrão de name como uma string vazia ("");
  • reatribui o name a "Elliott".

No LitElement, isso seria feito desta forma:

Em TypeScript:

import {LitElement, html} from 'lit';
import {customElement, property} from 'lit/decorators';

@customElement('welcome-banner')
class WelcomeBanner extends LitElement {
  @property({type: String})
  name = '';

  render() {
    return html`<h1>Hello, ${this.name}</h1>`
  }
}

Em 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);

No arquivo HTML:

<!-- index.html -->
<head>
  <script type="module" src="./index.js"></script>
</head>
<body>
  <welcome-banner name="Elliott"></welcome-banner>
</body>

O exemplo acima:

@property({type: String})
name = '';
  • define uma propriedade reativa pública, que é uma parte da API public do componente;
  • expõe um atributo e uma propriedade no componente por padrão;
  • define como converter os atributos do componente, que são strings, em um valor.
static get properties() {
  return {
    name: {type: String}
  }
}
  • Este exemplo tem a mesma função que o decorador em TS de @property, mas é executado de forma nativa em JavaScript.
render() {
  return html`<h1>Hello, ${this.name}</h1>`
}
  • Ele é chamado sempre que uma propriedade reativa muda.
@customElement('welcome-banner')
class WelcomeBanner extends LitElement {
  ...
}
  • Este código associa um nome de tag de elemento HTML a uma definição de classe.
  • Devido ao padrão dos elementos personalizados, o nome da tag precisa incluir um hífen (-).
  • this em um LitElement se refere à instância do elemento personalizado (no caso, <welcome-banner>).
customElements.define('welcome-banner', WelcomeBanner);
  • Este é o equivalente em JavaScript do decorador em TS de @customElement.
<head>
  <script type="module" src="./index.js"></script>
</head>
  • Ele importa a definição do elemento personalizado.
<body>
  <welcome-banner name="Elliott"></welcome-banner>
</body>
  • Adiciona o elemento personalizado à página.
  • Define a propriedade name como 'Elliott'.

Componentes de funções

Checkpoint do código

O Lit não tem uma interpretação individual de um componente de função, porque não usa JSX ou um pré-processador. No entanto, é muito simples criar uma função que aceite as propriedades e renderize o DOM de acordo com elas. Por exemplo:

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

const element = <Welcome name="Elliott"/>
ReactDOM.render(
  element,
  mountNode
);

No Lit, isso seria:

import {html, render} from 'lit';

function Welcome(props) {
  return html`<h1>Hello, ${props.name}</h1>`;
}

render(
  Welcome({name: 'Elliott'}),
  document.body.querySelector('#root')
);

Nesta seção, você aprenderá sobre o estado e o ciclo de vida do Lit.

Estado

O conceito de "propriedades reativas" no Lit é uma mistura de estado e propriedades do React. Quando propriedades reativas mudam, elas podem acionar o ciclo de vida do componente. As propriedades reativas têm duas variantes:

Propriedades reativas 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';

class MyEl extends LitElement {
  @property() name = 'there';
}
  • São definidas por @property.
  • São semelhantes ao estado e às propriedades do React, mas mutáveis.
  • São uma API pública, que é acessada e definida pelos consumidores do componente.

Estado reativo 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';

class MyEl extends LitElement {
  @state() name = 'there';
}
  • É definido por @state.
  • É semelhante ao estado do React, mas mutável.
  • É o estado interno particular, que normalmente é acessado no componente ou nas subclasses

Ciclo de vida

O ciclo de vida do Lit é bastante parecido com o do React, mas tem algumas diferenças importantes.

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';
  }
}
  • O equivalente no Lit também é um constructor.
  • Não é necessário transmitir nada para a chamada "super".
  • Ele é invocado pelas funções a seguir, entre outras:
    • document.createElement
    • document.innerHTML
    • new ComponentClass()
    • Se um nome de tag não atualizado estiver na página e a definição for carregada e registrada com @customElement ou customElements.define.
  • Tem função semelhante ao constructor do React.

render

// React
render() {
  return <div>Hello World</div>
}

// Lit
render() {
  return html`<div>Hello World</div>`;
}
  • O equivalente no Lit também é render.
  • Ele pode retornar qualquer resultado renderizável, como TemplateResult, string, entre outros.
  • De modo semelhante ao React, render() precisa ser uma função pura.
  • Renderizará para qualquer nó que o método createRenderRoot() retornar (por padrão, ShadowRoot).

componentDidMount

O componentDidMount é semelhante a uma combinação de callbacks do ciclo de vida firstUpdated e connectedCallback do Lit.

firstUpdated

import Chart from 'chart.js';

// React
componentDidMount() {
  this._chart = new Chart(this.chartElRef.current, {...});
}

// Lit
firstUpdated() {
  this._chart = new Chart(this.chartEl, {...});
}
  • Ele é chamado na primeira vez que o modelo do componente é renderizado na raiz do componente.
  • Só será chamado se o elemento estiver conectado. Por exemplo, não será chamado por document.createElement('my-component') até que esse nó seja anexado à árvore do DOM.
  • É ideal para fazer a configuração de componentes que exigem que o DOM seja renderizado pelo componente.
  • Diferente do componentDidMount do React, mudanças nas propriedades reativas em firstUpdated causarão uma nova renderização, embora o navegador normalmente agrupe as mudanças no mesmo frame. Se essas mudanças não exigirem acesso ao DOM da raiz, elas geralmente serão incluídas na função willUpdate.

connectedCallback

// React
componentDidMount() {
  this.window.addEventListener('resize', this.boundOnResize);
}

// Lit
connectedCallback() {
  super.connectedCallback();
  this.window.addEventListener('resize', this.boundOnResize);
}
  • Ele é chamado sempre que o elemento personalizado é inserido na árvore do DOM.
  • Diferente dos componentes do React, quando os elementos personalizados são removidos do DOM, eles não são destruídos e, portanto, podem ser "conectados" várias vezes.
  • É útil para reiniciar o DOM ou reanexar listeners de eventos que foram apagados ao desconectar.
  • Observação: a função connectedCallback pode ser chamada antes da firstUpdated. Portanto, o DOM pode não estar disponível na primeira chamada.

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);
  }
}
  • O equivalente no Lit é updated, que é o passado de "update" em inglês.
  • Diferente do React, updated também é chamado na renderização inicial.
  • Ele tem função semelhante ao componentDidUpdate do React.

componentWillUnmount

// React
componentWillUnmount() {
  this.window.removeEventListener('resize', this.boundOnResize);
}

// Lit
disconnectedCallback() {
  super.disconnectedCallback();
  this.window.removeEventListener('resize', this.boundOnResize);
}
  • O equivalente no Lit é semelhante ao disconnectedCallback.
  • Diferente dos componentes do React, quando os elementos personalizados são removidos do DOM, o componente não é destruído.
  • Ao contrário de componentWillUnmount, o disconnectedCallback é chamado depois que o elemento é removido da árvore.
  • O DOM dentro da raiz ainda está anexado à subárvore da raiz.
  • Ele é útil para excluir listeners de eventos e referências com vazamentos de memória para que o navegador possa fazer a coleta de lixo do componente.

Exercícios

Checkpoint do código (TS) Checkpoint do código (JS)

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')
);

O exemplo acima contém um relógio simples que:

  • renderiza "Hello World! It is" e exibe o horário;
  • atualiza o relógio a cada segundo;
  • quando desativado, limpa o intervalo que chama o elemento "tick".

Comece com a declaração de classe do componente:

// Lit (TS)
// some imports here are imported in advance
import {LitElement, html} from 'lit';
import {customElement, state} from 'lit/decorators';

@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);

Em seguida, inicialize a date e declare-a como uma propriedade reativa interna usando @state, já que os usuários do componente não definirão a date diretamente.

// Lit (TS)
import {LitElement, html} from 'lit';
import {customElement, state} from 'lit/decorators';

@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);

Em seguida, renderize o modelo.

// Lit (JS & TS)
render() {
  return html`
    <div>
      <h1>Hello, World!</h1>
      <h2>It is ${this.date.toLocaleTimeString()}.</h2>
    </div>
  `;
}

Agora, implemente o método "tick".

tick() {
  this.date = new Date();
}

Em seguida, implementaremos componentDidMount; Mais uma vez, o análogo no Lit é uma mistura de firstUpdated e connectedCallback. No caso desse componente, chamar tick usando setInterval não requer acesso ao DOM da raiz. Além disso, o intervalo será limpo quando o elemento for removido da árvore do documento. Portanto, caso o elemento fosse reanexado, o intervalo precisaria ser reiniciado. Sendo assim, connectedCallback é a melhor opção.

// 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 fim, limpe o intervalo para que ele não execute o "tick" depois que o elemento for desconectado da árvore do documento.

// Lit (TS & JS)
disconnectedCallback() {
  super.disconnectedCallback();
  clearInterval(this.timerId);
}

Juntando tudo, o resultado ficará assim:

// Lit (TS)
import {LitElement, html} from 'lit';
import {customElement, state} from 'lit/decorators';

@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);

Nesta seção, você aprenderá a converter os conceitos de hooks do React para o Lit.

Os conceitos de hooks do React

Os hooks do React oferecem uma forma de conectar os componentes da função ao estado. Isso traz muitos benefícios.

  • Eles simplificam a reutilização de lógica com estado.
  • Ajudam a dividir um componente em funções menores.

Além disso, o foco nos componentes baseados em funções resolve alguns problemas com a sintaxe baseada em classe do React, como:

  • A necessidade de transmitir props do constructor para super
  • A inicialização desorganizada de propriedades no constructor
    • Esse era um motivo apontado pela equipe do React na época, que foi resolvido pelo ES2019
  • Problemas causados pelo fato de this não se referir mais ao componente

Conceitos de hooks do React no Lit

Como mencionado na seção Componentes e propriedades, o Lit não oferece uma forma de criar elementos personalizados de uma função, mas o LitElement resolve a maioria dos principais problemas com componentes de classe do React. Por exemplo:

// 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++;
  }
}

Como o Lit resolve esses problemas?

  • O constructor não aceita argumentos.
  • Todas as vinculações de @event são vinculadas automaticamente a this.
  • Na grande maioria dos casos, this remete à referência do elemento personalizado.
  • As propriedades de classe agora podem ser instanciadas como membros da classe. Isso limpa as implementações baseadas no construtor.

Controladores reativos

Checkpoint do código (TS) Checkpoint do código (JS)

Os principais conceitos por trás dos Hooks existem no Lit como controladores reativos. Os padrões dos controles reativos permitem compartilhar lógica com estado, dividir componentes em bits menores e mais modulares, além de conectá-los ao ciclo de vida de atualização de um elemento.

Um controlador reativo é uma interface de objeto que pode ser conectada ao ciclo de vida de atualização de um host do controlador, como o LitElement.

O ciclo de vida de um ReactiveController e um reactiveControllerHost é:

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>;
}

Ao construir um controlador reativo e anexá-lo a um host usando addController, o ciclo de vida do controlador será chamado junto com o do host. Considere o exemplo do relógio na seção Estado e ciclo de vida, por exemplo:

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')
);

O exemplo acima contém um relógio simples que:

  • renderiza "Hello World! It is" e exibe o horário;
  • atualiza o relógio a cada segundo;
  • quando desativado, limpa o intervalo que chama o elemento "tick".

Como criar a base do componente

Comece com a declaração de classe do componente e adicione a função render.

// Lit (TS) - index.ts
import {LitElement, html} from 'lit';
import {customElement} from 'lit/decorators';

@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);

Como criar o controlador

Agora, vá para clock.ts, crie uma classe para o ClockController e configure o 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() {
  }
}

Um controlador reativo pode ser criado de qualquer forma, desde que compartilhe a interface ReactiveController. Mas, para a maioria dos casos básicos, a equipe do Lit prefere usar uma classe com um constructor que aceite uma interface ReactiveControllerHost e qualquer outra propriedade necessária para inicializar o controlador.

Agora é necessário converter os callbacks do ciclo de vida do React para callbacks do controlador. Resumindo:

  • componentDidMount
    • Para o connectedCallback do LitElement
    • Para o hostConnected do controlador
  • ComponentWillUnmount
    • Para o disconnectedCallback do LitElement
    • Para o hostDisconnected do controlador

Para ver mais informações sobre como converter o ciclo de vida do React para o ciclo de vida do Lit, consulte a seção Estado e ciclo de vida.

Em seguida, implemente o callback hostConnected e os métodos tick e limpe o intervalo em hostDisconnected, como mostrado no exemplo da seção Estado e 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);
  }

  _ick() {
    this.date = new Date();
  }

  hostDisconnected() {
    clearInterval(this.interval);
  }
}

Como usar o controlador

Para usar o controlador do relógio, importe o controlador e atualize o componente em index.ts ou index.js.

// Lit (TS) - index.ts
import {LitElement, html, ReactiveController, ReactiveControllerHost} from 'lit';
import {customElement} from 'lit/decorators';
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);

Para usar o controlador, é necessário instanciá-lo transmitindo uma referência ao host do controlador, que é o componente <my-element>, e depois usar o controlador no método render.

Como acionar novas renderizações no controlador

O relógio exibe a hora, mas o horário não está sendo atualizado. Isso ocorre porque o controlador está definindo a data a cada segundo, mas o host não está sendo atualizado, porque a date muda na classe ClockController, e não mais no componente. Isso significa que depois que a date é definida no controlador, é necessário solicitar que o host execute o ciclo de vida de atualização usando host.requestUpdate().

// Lit (TS & JS) - clock.ts / clock.js
private tick() {
  this.date = new Date();
  this.host.requestUpdate();
}

Agora, o relógio vai funcionar.

Para ver uma comparação mais detalhada dos casos de uso comuns com hooks, consulte a seção Tópicos avançados: hooks.

Nesta seção, você aprenderá a usar slots para gerenciar filhos no Lit.

Slots e filhos

Checkpoint do código

Os slots viabilizam a composição permitindo aninhar componentes.

No React, os filhos são herdados usando propriedades. O slot padrão é props.children e a função render define o local em que o slot padrão será posicionado. Por exemplo:

const MyArticle = (props) => {
 return <article>{props.children}</article>;
};

props.children são componentes do React, não elementos HTML.

No Lit, os filhos são compostos na função de renderização com elementos de slot. Eles são herdados de forma diferente que no React. No Lit, os filhos são elementos HTML anexados a slots. Essa ligação é conhecida como Projeção.

@customElement("my-article")
export class MyArticle extends LitElement {
  render() {
    return html`
      <article>
        <slot></slot>
      </article>
   `;
  }
}

Vários slots

Checkpoint do código

No React, adicionar vários slots é basicamente o mesmo que herdar mais propriedades.

const MyArticle = (props) => {
  return (
    <article>
      <header>
        {props.headerChildren}
      </header>
      <section>
        {props.sectionChildren}
      </section>
    </article>
  );
};

Da mesma forma, adicionar mais elementos <slot> cria mais slots no Lit. Vários slots são definidos com o atributo name: <slot name="slot-name">. Isso permite que os filhos declarem o slot que será atribuído a eles.

@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>
   `;
  }
}

Conteúdo padrão do slot

Um slot exibirá uma subárvore quando não existirem nós projetados para esse slot. Caso haja nós projetados para um slot, eles serão exibidos em vez da subárvore do slot.

@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>
   `;
  }
}

Atribuir filhos a slots

Checkpoint do código

No React, os filhos são atribuídos a slots usando as propriedades de um componente. No exemplo abaixo, os elementos do React são transmitidos para as propriedades headerChildren e sectionChildren.

const MyNewsArticle = () => {
 return (
   <MyArticle
     headerChildren={<h3>Extry, Extry! Read all about it!</h3>}
     sectionChildren={<p>Children are props in React!</p>}
   />
 );
};

No Lit, os filhos são atribuídos aos slots usando o 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>
   `;
  }
}

Caso não haja um slot padrão (por exemplo, <slot>) e nenhum slot tenha um atributo name (por exemplo, <slot name="foo">) que corresponda ao atributo slot dos filhos do elemento personalizado (por exemplo, <div slot="foo">), esse nó não será projetado e não será exibido.

Algumas vezes, o desenvolvedor pode precisar acessar a API de um HTMLElement.

Nesta seção, você aprenderá a usar referências de elementos no Lit.

Referências do React

Checkpoint do código (TS) Checkpoint do código (JS)

Um componente React é convertido em uma série de chamadas de função que criam um DOM virtual quando invocadas. Esse DOM virtual é interpretado pelo ReactDOM e renderiza os HTMLElements.

No React, as referências são espaços na memória para receber o HTMLElement gerado.

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>
 );
};

No exemplo acima, o componente do React:

  • renderiza uma entrada de texto vazia e um botão com texto;
  • move o foco para a entrada quando um usuário clica no botão.

Após a renderização inicial, o React definirá inputRef.current para o HTMLInputElement gerado usando o atributo ref.

"Referências" do Lit com @query

O Lit funciona bem com o navegador e cria uma abstração muito fina dos recursos do nativos do navegador.

O equivalente de refs do React no Lit é o HTMLElement retornado pelos decoradores @query e @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>
   `;
  }
}

No exemplo acima, o componente Lit:

  • define uma propriedade em MyElement usando o decorador @query, criando um getter para um HTMLInputElement;
  • declara e anexa um callback de evento de clique com o nome onButtonClick;
  • direciona o foco para a entrada quando o botão recebe um clique.

Em JavaScript, os decoradores @query e @queryAll executam querySelector e querySelectorAll, respectivamente. Esse é o equivalente em JavaScript de @query('input') inputEl!: HTMLInputElement;

get inputEl() {
  return this.renderRoot.querySelector('input');
}

Depois que o componente Lit confirma o modelo do método render na raiz do my-element, o decorador @query permite que o inputEl retorne o primeiro elemento input encontrado na raiz de renderização. Ele retornará null caso a @query não consiga encontrar o elemento especificado.

Caso houvesse vários elementos input na raiz de renderização, @queryAll retornaria uma lista de nós.

Nesta seção, você aprenderá a mediar os estados entre componentes no Lit.

Componentes reutilizáveis

Checkpoint do código

O React imita os pipelines de renderização funcionais com fluxo de dados de cima para baixo. Os pais atribuem um estado para os filhos usando as propriedades e os filhos se comunicam com os pais usando callbacks encontrados nas propriedades.

const CounterButton = (props) => {
  const label = props.step < 0
    ? `- ${-1 * props.step}`
    : `+ ${props.step}`;

  return (
    <button
      onClick={() =>
        props.addToCounter(props.step)}>{label}</button>
  );
};

No exemplo acima, o componente do React:

  • cria um marcador com base no valor props.step;
  • renderiza um botão com "+step" ou "-step" como o marcador;
  • atualiza o componente pai chamando props.addToCounter com props.step como um argumento quando um clique ocorre.

Embora seja possível transmitir callbacks no Lit, os padrões convencionais são diferentes. O componente do React do exemplo acima poderia ser programado como um componente Lit no exemplo abaixo:

@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>
    `;
  }
}

No exemplo acima, o componente Lit:

  • cria a propriedade reativa step;
  • envia um evento personalizado com o nome update-counter e o valor step do elemento ao receber um clique.

Os eventos do navegador fluem dos elementos filhos para os pais. Os eventos permitem que os filhos transmitam eventos de interação e mudanças de estado. Basicamente, o React transmite o estado na direção oposta. Por isso, não é comum ver os componentes do React enviarem e detectarem eventos da mesma forma que os componentes do Lit.

Componentes com estado

Checkpoint do código

No React, é comum usar os hooks para gerenciar o estado. Um componente MyCounter pode ser criado reutilizando o componente CounterButton. addToCounter é transmitido para as duas instâncias do CounterButton.

const MyCounter = (props) => {
 const [counterSum, setCounterSum] = React.useState(0);
 const addToCounter = useCallback(
   (step) => {
     setCounterSum(counterSum + step);
   },
   [counterSum, setCounterSum]
 );

 return (
   <div>
     <h3>&Sigma;: {counterSum}</h3>
     <CounterButton
       step={-1}
       addToCounter={addToCounter} />
     <CounterButton
       step={1}
       addToCounter={addToCounter} />
   </div>
 );
};

No exemplo acima:

  • um estado count é criado;
  • um callback que adiciona um número a um estado count é criado;
  • o CounterButton usa addToCounter para atualizar a count em step a cada clique.

É possível usar uma implementação semelhante do MyCounter no Lit. addToCounter não é transmitido para o counter-button e, em vez disso, o callback é vinculado como um listener de eventos ao evento @update-counter em um elemento pai.

@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>&Sigma; ${this.count}</h3>
        <counter-button step="-1"></counter-button>
        <counter-button step="1"></counter-button>
      </div>
    `;
  }
}

O exemplo acima:

  • cria uma propriedade reativa com o nome count, que atualizará o componente quando o valor mudar;
  • vincula o callback addToCounter ao listener de eventos @update-counter;
  • atualiza a count adicionando o valor de detail.step do evento update-counter;
  • define o valor step do counter-button usando o atributo step.

A abordagem mais comum é usar propriedades reativas do Lit para transmitir mudanças de pais para filhos. Também é recomendável usar o sistema de eventos do navegador para estabelecer o fluxo de informações de baixo para cima.

Esse procedimento segue as práticas recomendadas e está de acordo com o objetivo do Lit de oferecer compatibilidade multiplataforma com componentes da Web.

Nesta seção, você aprenderá sobre como definir estilos no Lit.

Estilo

O Lit oferece várias formas de definir os estilos de elementos e também uma solução integrada.

Estilos in-line

Checkpoint do código

O Lit é compatível com estilos in-line e com a vinculação deles.

import {LitElement, html, css} from 'lit';
import {customElement} from 'lit/decorators';

@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>
    `;
  }
}

O exemplo acima contém dois títulos, cada um com um estilo in-line.

Agora, tente vincular uma border: 1px solid black ao texto em laranja:

<h1 style="color:orange;${'border: 1px solid black;'}">This text is orange</h1>

Pode ser inconveniente ter que calcular a string de estilo toda as vezes. Por isso, o Lit oferece uma diretiva para facilitar esse processo.

styleMap

A diretiva styleMap facilita o uso do JavaScript ao definir estilos in-line. Por exemplo:

Checkpoint do código

import {LitElement, html, css} from 'lit';
import {customElement, property} from 'lit/decorators';
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>
    `;
  }
}

O exemplo acima:

  • exibe um h1 com uma borda e um seletor de cores;
  • muda a border-color para o valor do seletor de cores.

Ele também inclui o styleMap, que é usado para definir os estilos de h1. O styleMap segue uma sintaxe parecida com a de vinculação de atributos style do React.

CSSResult

Checkpoint do código

A forma recomendada de definir o estilo de componentes é usar o literal de modelo com a tag css.

import {LitElement, html, css} from 'lit';
import {customElement} from 'lit/decorators';

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>
    `;
  }
}

O exemplo acima:

  • declara um literal de modelo com a tag CSS como uma vinculação;
  • define as cores de dois h1s com IDs.

Vantagens de usar a tag de modelo css:

  • Ela é analisada uma vez por classe, e não por instância.
  • É implementada considerando a reutilização do módulo.
  • Consegue separar estilos nos próprios arquivos facilmente.
  • É compatível com o polyfill das propriedades personalizadas de CSS.

Além disso, veja a tag <style> em index.html:

<!-- index.html -->
<style>
  h1 {
    color: red !important;
  }
</style>

O Lit definirá o escopo do estilo dos componentes como as raízes deles. Isso significa que os estilos não vazarão para dentro ou para fora. Para transmitir estilos entre componentes, a equipe do Lit recomenda usar propriedades personalizadas de CSS, porque elas conseguem transpassar o escopo de estilo do Lit.

Tags de estilo

Também é possível simplesmente colocar tags <style> in-line nos modelos. O navegador elimina a duplicação dessas tags de estilo, mas quando colocadas nos modelos, elas serão analisadas por instância de componente, e não por classe, como é o caso do modelo com a tag css. Além disso, a eliminação da duplicação de CSSResults do navegador é muito mais rápida.

Também é possível usar <link rel="stylesheet"> no modelo para definir os estilos. Contudo, isso não é recomendado porque pode causar uma renderização inicial de conteúdo sem estilo (FOUC, na sigla em inglês).

JSX e modelos

Lit e DOM virtual

O lit-html não inclui um DOM virtual convencional que diferencia cada nó separadamente. Em vez disso, ele usa os recursos de desempenho intrínsecos da especificação de literal de modelo com tag do ES2015. Literais de modelo com tag são strings literais de modelo com funções de tag anexadas.

Este é um exemplo de literal de modelo:

const str = 'string';
console.log(`This is a template literal ${str}`);

Este é um exemplo de literal de modelo com tag:

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

No exemplo acima, a tag é a função tag e a função f retorna uma invocação de um literal de modelo com tag.

Boa parte da magia do bom desempenho do Lit é devida às matrizes de strings transmitidas para a função de tag terem o mesmo ponteiro, conforme mostrado no segundo console.log. O navegador não recria uma nova matriz de strings em cada invocação de função da tag, porque ele usa o mesmo literal de modelo, ou seja, o mesmo local na AST. Portanto, a vinculação, a análise e o armazenamento em cache de modelos do Lit podem aproveitar esses recursos sem que haja sobrecarga de comparação durante a execução.

O comportamento de literais de modelo com tag integrado ao navegador proporciona uma boa vantagem de desempenho para o Lit. A maioria dos DOMs virtuais convencionais executa a maior parte do trabalho em JavaScript. No entanto, os literais de modelo com tag fazem a maior parte das comparações no C++ do navegador.

A equipe do Lit recomenda usar a biblioteca htm caso você queira começar a usar literais de modelo com tag HTML com o React ou Preact.

No entanto, como é o caso do site do Google Codelabs e de vários editores de código on-line, o destaque de sintaxe com literais de modelo com tag não é muito comum. Alguns ambientes de desenvolvimento integrado e editores de texto oferecem a compatibilidade por padrão, como os destaques de bloco de código do Atom e do GitHub. A equipe do Lit também trabalha com a comunidade para a manutenção de projetos, como o lit-plugin, um plug-in do VS Code que adiciona o destaque de sintaxe, a verificação de tipo e o intellisense aos projetos do Lit.

Lit e JSX + DOM do React

Como o JSX não é executado no navegador, um pré-processador é usado para converter o JSX em chamadas de função JavaScript, geralmente com o Babel.

Por exemplo, o Babel transformará isto:

const element = <div className="title">Hello World!</div>;
ReactDOM.render(element, mountNode);

nisto:

const element = React.createElement('div', {className: 'title'}, 'Hello World!');
ReactDOM.render(element, mountNode);

Em seguida, o DOM do React usará a saída do React e o converterá em propriedades reais do DOM, como atributos, listeners de eventos e outras.

O lit-html usa literais de modelo com tag que podem ser executados no navegador sem precisar de conversão ou de um pré-processador. Isso significa que para começar a usar o Lit, basta ter um arquivo HTML, um script de módulo ES e um servidor. Este script pode ser executado inteiramente no 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>

Além disso, como o sistema de modelos do Lit, o lit-html, não usa um DOM virtual convencional, e sim a API DOM diretamente. O tamanho do Lit 2 minificado e compactado é 5 KB, que é menor que o total de 40 KB do React (2,8 KB) + react-dom (39,4 KB), quando minificados e compactados.

Eventos

O React usa um sistema de eventos sintéticos. Isso significa que o react-dom precisa definir todos os eventos que serão usados em cada componente e fornecer um listener de evento camelCase equivalente para cada tipo de nó. Como resultado, o JSX não tem um método para definir um listener de eventos para um evento personalizado, e os desenvolvedores precisam usar uma ref e depois aplicar um listener. Isso gera uma experiência ruim para o desenvolvedor ao integrar bibliotecas que não consideram o React, sendo necessário criar um wrapper específico para ele.

O lit-html acessa diretamente o DOM e usa eventos nativos. Dessa forma, adicionar listeners de eventos passa a ser simples assim: @event-name=${eventNameListener}. Isso significa que o tempo de análise durante a execução para adicionar listeners de eventos e disparar eventos diminui.

Componentes e propriedades

Componentes do React e elementos personalizados

Internamente, o LitElement usa elementos personalizados para empacotar os componentes. Os elementos personalizados apresentam algumas vantagens sobre os componentes do React quando se trata da definição de componentes. O estado e o ciclo de vida são discutidos em mais detalhes na seção Estado e ciclo de vida.

Algumas vantagens dos elementos personalizados, como sistema de componentes, são que eles:

  • são nativos do navegador e não exigem ferramentas;
  • funcionam em todas as APIs do navegador, de innerHTML e document.createElement até querySelector;
  • geralmente, podem ser usados em vários frameworks;
  • podem ser registrados lentamente com customElements.define e "hydrate" do DOM.

Algumas desvantagens dos elementos personalizados comparados aos componentes do React:

  • Eles não conseguem criar um elemento personalizado sem definir uma classe, portanto, não criam componentes funcionais do tipo JSX.
  • Precisam ter uma tag de fechamento.
    • Observação: embora seja conveniente para os desenvolvedores, os navegadores tendem a ter problemas com a especificação de fechamento automático de tags e, por isso, as especificações mais recentes não costumam incluir esse recurso.
  • Inserem um nó extra na árvore do DOM, o que pode causar problemas no layout.
  • Precisam ser registrados usando JavaScript.

O Lit optou por usar elementos personalizados em vez de um sistema de elementos, porque eles são integrados ao navegador. A equipe do Lit acredita que os benefícios trazidos pelos elementos personalizados que podem ser usados em vários frameworks superam os benefícios de uma camada de abstração de componentes. Na realidade, com o esforço da equipe do Lit no espaço do lit-ssr, foi possível superar os principais problemas com o registro em JavaScript. Além disso, algumas empresas, como o GitHub, aproveitam o registro lento de elementos personalizados para aprimorar progressivamente as páginas com um toque especial.

Como transmitir dados para elementos personalizados

Um equívoco comum quando se trata de elementos personalizados é a ideia de que os dados só podem ser transmitidos como strings. Isso provavelmente se deve ao fato de que os atributos do elemento só podem ser programados como strings. Embora seja verdade que o Lit transmita os atributos de string para os tipos definidos, os elementos personalizados também podem aceitar dados complexos como propriedades.

Por exemplo, considerando a seguinte definição do LitElement:

código

// data-test.ts
import {LitElement, html} from 'lit';
import {customElement, property} from 'lit/decorators';

@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>`;
  }
}

Uma propriedade reativa primitiva num que converte o valor da string de um atributo em um number é definida. Em seguida, uma estrutura de dados complexa é introduzida por attribute:false, que desativa o gerenciamento de atributos do Lit.

A transmissão de dados para esse elemento personalizado é feita desta forma:

<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 e ciclo de vida

Outros callbacks de ciclo de vida do React

static getDerivedStateFromProps

Não existe um equivalente no Lit, já que as propriedades e o estado fazem parte das mesmas propriedades de classes.

shouldComponentUpdate

  • O equivalente no Lit é shouldUpdate.
  • Ao contrário do React, ele é chamado na primeira renderização.
  • Ele tem função semelhante ao shouldComponentUpdate do React.

getSnapshotBeforeUpdate

No Lit, o elemento getSnapshotBeforeUpdate é semelhante a update e willUpdate.

willUpdate

  • Ele é chamado antes de update.
  • Ao contrário de getSnapshotBeforeUpdate, willUpdate é chamado antes de render.
  • Mudanças em propriedades reativas em willUpdate não acionam novamente o ciclo de atualização.
  • Ele é ideal para calcular valores de propriedade que dependem de outras propriedades e são usados no restante do processo de atualização.
  • Esse método é chamado no servidor em SSR, por isso, não é recomendável acessar o DOM nele.

update

  • Esse método é chamado depois de willUpdate.
  • Ao contrário de getSnapshotBeforeUpdate, update é chamado antes de render.
  • Mudanças em propriedades reativas em update não acionam novamente o ciclo de atualização se ocorrerem antes de super.update ser chamado.
  • É ideal para capturar informações do DOM relacionadas ao componente antes que a saída renderizada seja confirmada para o DOM.
  • Esse método não é chamado no servidor em SSR.

Outros callbacks do ciclo de vida do Lit

Existem vários callbacks de ciclo de vida que não foram mencionados na seção anterior por não terem um equivalente no React. São eles:

attributeChangedCallback

É invocado quando um dos observedAttributes do elemento muda. Tanto observedAttributes quanto attributeChangedCallback fazem parte da especificação de elementos personalizados e são implementados pelo Lit internamente para fornecer uma API de atributos para elementos do Lit.

adoptedCallback

É invocado quando o componente é movido para um novo documento, por exemplo, do documentFragment de um HTMLTemplateElement para o document principal. Esse callback também faz parte da especificação de elementos personalizados e só pode ser implementado em casos de uso avançados em que o componente muda de documento.

Outros métodos e propriedades do ciclo de vida

Esses métodos e propriedades são membros de classe que podem ser chamados, substituídos ou esperados para ajudar a manipular o processo do ciclo de vida.

updateComplete

Essa é uma Promise que é resolvida quando o elemento termina a atualização, já que os ciclos de vida de atualização e renderização são assíncronos. Por exemplo:

async nextButtonClicked() {
  this.step++;
  // Wait for the next "step" state to render
  await this.updateComplete;
  this.dispatchEvent(new Event('step-rendered'));
}

getUpdateComplete

Esse é um método que precisa ser substituído para que ocorra a personalização quando o updateComplete for resolvido. Isso é comum quando um componente renderiza um componente filho e os ciclos de renderização precisam estar sincronizados. Por exemplo:

class MyElement extends LitElement {
  ...
  async getUpdateComplete() {
    await super.getUpdateComplete();
    await this.myChild.updateComplete;
  }
}

performUpdate

Esse é o método que chama os callbacks de ciclo de vida de atualização. Geralmente isso não é necessário, exceto em casos raros em que a atualização precisa ocorrer de forma sincronizada ou para programações personalizadas.

hasUpdated

Essa propriedade será true se o componente tiver sido atualizado pelo menos uma vez.

isConnected

Essa propriedade, que é parte da especificação dos elementos personalizados, será true se o elemento estiver anexado à árvore do documento principal.

Visualização do ciclo de vida de atualização do Lit

O ciclo de vida de atualização tem três partes:

  • Pré-atualização
  • Atualização
  • Pós-atualização

Pré-atualização

Gráfico acíclico direcionado de nós com nomes de callback: o &quot;construtor&quot; aponta para o método &quot;requestUpdate&quot;. &quot;@property&quot; aponta para &quot;Property Setter&quot; e &quot;attributeChangedCallback&quot; aponta para &quot;Property Setter&quot;. &quot;Property Setter&quot; aponta para &quot;hasChanged&quot;, &quot;hasChanged&quot; aponta para &quot;requestUpdate&quot; e &quot;requestUpdate&quot; para o próximo gráfico de ciclo de vida de atualização.

Depois de requestUpdate, uma atualização é necessária.

Atualização

Um gráfico acíclico direcionado de nós com nomes de callback. A seta da imagem anterior do ciclo de vida da pré-atualização aponta para &quot;performUpdate&quot;, &quot;performUpdate&quot; aponta para &quot;shouldUpdate&quot;, &quot;shouldUpdate&quot; para &quot;complete update if false&quot; e também para &quot;willUpdate&quot;. &quot;willUpdate&quot; aponta para &quot;update&quot;, &quot;update&quot; para &quot;render&quot; e também para o próximo gráfico do ciclo de vida da pós-atualização. &quot;render&quot; também aponta para o próximo gráfico do ciclo de vida da pós-atualização.

Pós-atualização

Um gráfico acíclico direcionado de nós com nomes de callback. A seta da imagem anterior de pontos do ciclo de vida da atualização aponta para &quot;firstUpdated&quot;, &quot;firstUpdated&quot; aponta para &quot;updated&quot; e &quot;updated&quot; para &quot;updateComplete&quot;.

Hooks

Por que hooks

Os hooks foram introduzidos no React para casos de uso simples de componentes de funções que exigiam estados. Em muitos casos simples, os componentes de funções com hooks costumam ser muito mais fáceis de usar e legíveis que os componentes de classe equivalentes. No entanto, ao introduzir atualizações de estado assíncronas e transmitir dados entre hooks ou efeitos, o padrão dos hooks tende a não ser o suficiente e uma solução baseada em classes, como os controles reativos, pode ser melhor.

Hooks de solicitações de API e controladores

É comum programar um hook que solicita dados de uma API. Por exemplo, considere este componente de função do React que faz isto:

  • index.tsx
    • Renderiza texto
    • Renderiza a resposta de useAPI
      • ID do usuário + nome do usuário
      • Mensagem de erro
        • Gera um erro 404 quando chega ao usuário 11 (por padrão)
        • Gera um erro de cancelamento se a busca da API for cancelada
      • Mensagem de carregamento
    • Renderiza um botão de ação
      • Próximo usuário: busca o próximo usuário na API
      • Cancelar: cancela a busca da API e exibe um erro
  • useApi.tsx
    • Define um hook useApi personalizado
    • Busca de forma assíncrona um objeto de usuário em uma API
    • Emite:
      • Nome de usuário
      • Se a busca está carregando
      • Qualquer mensagem de erro
      • Um callback para cancelar a busca
    • Cancela as buscas em andamento caso seja fechado

Veja a implementação do Lit com controlador reativo.

Principais pontos:

  • Os controles reativos são muito parecidos com os hooks personalizados.
  • Como transmitir dados não renderizáveis entre os callbacks e os efeitos:
    • O React usa useRef para transmitir dados entre useEffect e useCallback.
    • O Lit usa uma propriedade de classe particular.
    • O React basicamente imita o comportamento de uma propriedade de classe particular.

Filhos

Slot padrão

Quando elementos HTML não recebem um atributo slot, eles são atribuídos ao slot sem nome padrão. No exemplo abaixo, MyApp colocará um parágrafo em um slot com nome. O outro parágrafo será colocado no slot sem nome por padrão.

Playground

@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>
   `;
  }
}

Atualizações de slots

Quando a estrutura dos descendentes de slot mudar, um evento slotchange será disparado. Um componente Lit pode vincular um listener de eventos a um evento slotchange. No exemplo abaixo, o primeiro slot encontrado na shadowRoot terá os assignedNodes registrados no console por 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>
   `;
  }
}

Referências

Geração de referências

Tanto o Lit quanto o React expõe uma referência a um HTMLElement depois que as funções render são chamadas. No entanto, vale a pena analisar a forma como o React e o Lit formam o DOM que é retornado usando um decorador @query do Lit ou uma referência do React.

O React é um pipeline funcional que cria componentes do React, e não HTMLElements. Como a referência é declarada antes da renderização de um HTMLElement, um espaço na memória é alocado. É por isso que null é exibido como o valor inicial de uma referência: porque o elemento DOM real ainda não foi criado ou renderizado, ou seja, useRef(null).

Depois que o ReactDOM converte um componente do React em um HTMLElement, ele busca um atributo com o nome ref no ReactComponent. Se disponível, o ReactDOM define a referência do HTMLElement como ref.current.

O LitElement usa a função de tag de modelo html do lit-html para criar um elemento de modelo internamente. O LitElement marca o conteúdo do modelo como um shadow DOM de um elemento personalizado após a renderização. O shadow DOM é uma árvore do DOM com escopo, encapsulada por uma raiz shadow. Em seguida, o decorador @query cria um getter para a propriedade que executa this.shadowRoot.querySelector na raiz do escopo.

Consultar vários elementos

No exemplo abaixo, o decorador @queryAll retorna os dois parágrafos na raiz shadow 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>
   `;
  }
}

Basicamente, @queryAll cria um getter para paragraphs, que retorna os resultados de this.shadowRoot.querySelectorAll(). Em JavaScript, um getter pode ser declarado para executar a mesma função:

get paragraphs() {
  return this.renderRoot.querySelectorAll('p');
}

Elementos de consulta mutáveis

O decorador @queryAsync é mais adequado para processar um nó que pode mudar de acordo com o estado de outra propriedade do elemento.

No exemplo abaixo, @queryAsync encontrará o primeiro elemento de parágrafo. No entanto, um elemento de parágrafo só será renderizado quando renderParagraph gerar um número ímpar de forma aleatória. A diretiva @queryAsync retornará uma promessa que será resolvida quando o primeiro parágrafo ficar disponível.

@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()}
   `;
  }
}

Estado de mediação

No React, é comum usar callbacks, porque o estado é mediado pelo próprio React. O React faz o possível para não depender do estado fornecido pelos elementos. O DOM é simplesmente um efeito do processo de renderização.

Estado externo

É possível usar a Redux, a MobX ou qualquer outra biblioteca de gerenciamento de estado com o Lit.

Os componentes Lit são criados no escopo do navegador. Assim, qualquer biblioteca que existir dentro do escopo do navegador também estará disponível para o Lit. Muitas bibliotecas excelentes foram criadas para aproveitar os sistemas de gerenciamento de estado existentes no Lit.

Veja esta série do Vaadin que explica como aproveitar a Redux em um componente Lit.

Confira o lit-mobx da Adobe para ver como um site de grande escala pode usar a MobX no Lit.

Além disso, consulte Apollo Elements para ver como os desenvolvedores incluem o GraphQL em componentes da Web.

O Lit funciona com recursos nativos do navegador e a maioria das soluções de gerenciamento de estado no escopo do navegador pode ser usada em um componente Lit.

Estilo

Shadow DOM

Para encapsular estilos e o DOM nativamente em um elemento personalizado, o Lit usa o Shadow DOM. As raízes shadow geram uma árvore shadow separada da árvore do documento principal. Isso significa que a maioria dos estilos tem o escopo desse documento. Alguns estilos, como cor e outros estilos relacionados a fontes podem apresentar vazamento.

O Shadow DOM também introduz novos conceitos e seletores à especificação 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.
   */
}

Como compartilhar estilos

Com o Lit, é fácil compartilhar estilos entre os componentes na forma de CSSTemplateResults usando as tags de modelo css. Por exemplo:

// 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

Pode ser um pouco desafiador usar as raízes shadow nas definições convencionais de temas, que normalmente usam tags de estilo de cima para baixo. A maneira convencional de processar os temas com componentes da Web que usam o Shadow DOM é expor uma API de estilo usando as propriedades personalizadas de CSS. Por exemplo, este é um padrão usado pelo Material Design:

.mdc-textfield-outline {
  border-color: var(--mdc-theme-primary, /* default value */ #...);
}
.mdc-textfield--input {
  caret-color: var(--mdc-theme-primary, #...);
}

Para mudar o tema do site, o usuário precisa aplicar valores de propriedade personalizada:

html {
  --mdc-theme-primary: #F00;
}
html[dark] {
  --mdc-theme-primary: #F88;
}

Caso o tema de cima para baixo seja essencial e você não consiga expor os estilos, é sempre possível desativar o Shadow DOM substituindo createRenderRoot para retornar this, que renderizará o modelo dos componentes como o elemento personalizado, e não como uma raiz shadow anexada ao elemento personalizado. Isso fará com que você perca o encapsulamento de estilo, o encapsulamento de DOM e os slots.

Produção

IE 11

Caso você precise oferecer compatibilidade com navegadores mais antigos, como o IE 11, será necessário carregar alguns polyfills, que adicionam outros 33 KB, aproximadamente. Saiba mais neste link.

Pacotes condicionais

A recomendação da equipe do Lit é oferecer dois pacotes diferentes, um para o IE 11 e outro para navegadores modernos. Isso traz muitos benefícios:

  • O ES 6 é mais rápido e atenderá a maioria dos seus clientes.
  • O ES 5 convertido aumenta significativamente o tamanho do pacote.
  • Os pacotes condicionais oferecem o melhor de cada um:
    • Oferecem compatibilidade com o IE 11.
    • Não apresentam lentidão em navegadores modernos.

Mais informações sobre como criar um pacote oferecido de forma condicional estão disponíveis no nosso site de documentação neste link.