Aprenda sobre os benefícios do uso de componentes da Web, como funcionam e como começar.
Com os componentes da Web (doravante chamados de componentes), os desenvolvedores podem criar seus próprios elementos HTML. Neste guia, você aprenderá tudo o que há para saber sobre os componentes. Começaremos com o que são os componentes, quais são seus benefícios e de que são feitos.
Depois disso, começaremos a construir os componentes, primeiro usando modelos HTML e a interface DOM de sombra, em seguida, mergulharemos um pouco no tópico e veremos como criar um elemento integrado personalizado.
O que são componentes?
Os desenvolvedores adoram componentes (aqui, queremos dizer a implementação do padrão de design "Módulo"). Esta é uma ótima maneira de definir um bloco de código que pode ser usado a qualquer hora, em qualquer lugar. Ao longo dos anos, várias tentativas mais ou menos bem-sucedidas foram feitas para colocar essa ideia em prática.
O XML Binding Language da Mozilla e a especificação do componente HTML da Microsoft para o Internet Explorer 5 surgiram há cerca de 20 anos. Infelizmente, ambas as implementações eram muito complexas e não interessavam aos fabricantes de outros navegadores e, portanto, logo foram esquecidas. Apesar disso, foram eles que lançaram as bases do que temos hoje nesta área.
Estruturas JavaScript como React, Vue e Angular adotam uma abordagem semelhante. Um dos principais motivos de seu sucesso é a capacidade de encapsular a lógica geral do aplicativo em alguns modelos que se movem facilmente de um formulário para outro.
Embora essas estruturas melhorem a experiência de desenvolvimento, tudo tem um preço. Os recursos de linguagem, como JSX, precisam ser compilados e a maioria dos frameworks usa um mecanismo JavaScript para gerenciar suas abstrações. Existe outra abordagem para resolver o problema de divisão do código em componentes? A resposta são os componentes da web.
4 pilares de componentes
Os componentes consistem em três APIs - elementos personalizados, modelos HTML e DOM shadow, bem como seus módulos JavaScript subjacentes (módulos ES6). Usando as ferramentas fornecidas por essas interfaces, você pode criar elementos HTML personalizados que se comportam como suas contrapartes nativas.
Os componentes são usados da mesma maneira que os elementos HTML regulares. Eles podem ser personalizados usando atributos, recuperados usando JavaScript, estilizados usando CSS. O principal é notificar o navegador de que existem.
Isso permite que os componentes interajam com outras estruturas e bibliotecas. Ao usar o mesmo mecanismo de comunicação como elementos regulares, eles podem ser usados por qualquer estrutura existente, bem como por ferramentas que aparecerão no futuro.
Também deve ser observado que os componentes estão em conformidade com os padrões da web. A web é baseada na ideia de compatibilidade com versões anteriores. Isso significa que os componentes criados hoje funcionarão muito bem por muito tempo.
Vamos dar uma olhada em cada especificação separadamente.
1. Elementos personalizados
Características principais:
- Definindo o comportamento do elemento
- Reagindo às Mudanças de Atributo
- Estendendo elementos existentes
Freqüentemente, quando as pessoas falam sobre componentes, elas se referem à interface de elementos personalizados.
Esta API permite que você estenda os elementos definindo seu comportamento quando adicionados, atualizados e removidos.
class ExampleElement extends HTMLElement {
static get observedAttributes() {
return [...]
}
attributeChangedCallback(name, oldValue, newValue) {}
connectedCallback() {}
}
customElements.define('example-element', ExampleElement)
Cada elemento personalizado possui uma estrutura semelhante. Ele estende a funcionalidade da classe HTMLElements existente.
Dentro de um elemento personalizado, existem vários métodos chamados reações que são responsáveis por lidar com uma alteração específica em um elemento. Por exemplo, connectedCallback é chamado quando um item é adicionado à página. Isso é semelhante aos estágios do ciclo de vida usados em estruturas (componentDidMount no React, montado no Vue).
Mudar os atributos de um elemento acarreta uma mudança em seu comportamento. Quando ocorre uma atualização, o attributeChangedCallback é chamado contendo informações sobre a mudança. Isso acontece apenas para os atributos especificados na matriz retornada porobservedAttributes.
O elemento deve ser definido antes que o navegador possa usá-lo. O método "define" leva dois argumentos - o nome da tag e sua classe. Todas as tags devem conter o caractere "-" para evitar conflitos com elementos nativos existentes e futuros.
<example-element>Content</example-element>
O elemento pode ser usado como uma tag HTML normal. Quando tal elemento é encontrado, o navegador associa seu comportamento à classe especificada. Este processo é denominado "atualização".
Existem dois tipos de itens - “autônomo” e “integrado personalizado”. Até agora, vimos itens autônomos. Esses são elementos que não estão relacionados a elementos HTML existentes. Como as tags div e span, que não têm significado semântico específico.
Elementos embutidos personalizados - como o nome sugere - estendem a funcionalidade dos elementos HTML existentes. Eles herdam o comportamento semântico desses elementos e podem alterá-lo. Por exemplo, se o elemento “input” foi customizado, ele ainda permanecerá um campo de entrada e parte do formulário quando for enviado.
class CustomInput extends HTMLInputElement {}
customElements.define('custom-input', CustomInput, { extends: 'input' })
A classe de elemento embutido customizado estende a classe de elemento customizado. Ao definir um elemento embutido, o elemento expansível é passado como o terceiro argumento.
<input is="custom-input" />
O uso da tag também é um pouco diferente. Em vez de uma nova tag, a existente é usada, especificando o atributo especial de extensão "is". Quando o navegador encontra esse atributo, ele sabe que está lidando com um elemento personalizado e o atualiza de acordo.
Enquanto os elementos autônomos são suportados pela maioria dos navegadores modernos, os elementos embutidos personalizados são suportados apenas pelo Chrome e Firefox. Quando usados em um navegador que não os suporta, eles serão tratados como elementos HTML regulares, portanto, em geral, são seguros para uso mesmo nesses navegadores.
2. Modelos HTML
- Criação de estruturas prontas
- Não são exibidos na página antes da chamada
- Contém HTML, CSS e JS
Historicamente, a modelagem do lado do cliente envolveu a concatenação de strings em JavaScript ou o uso de bibliotecas como Handlebars para analisar blocos de marcação customizada. Recentemente, a especificação tem uma tag "template" que pode conter tudo o que desejamos usar.
<template id="tweet">
<div class="tweet">
<span class="message"></span>
Written by @
<span class="username"></span>
</div>
</template>
Por si só, não afeta a página de forma alguma, ou seja, não é analisado pelo mecanismo, as solicitações de recursos (áudio, vídeo) não são enviadas. JavaScript não pode acessá-lo e, para navegadores, é um elemento vazio.
const template = document.getElementById('tweet')
const node = document.importNode(template.content, true)
document.body.append(node)
Primeiro, obtemos o elemento "template". O método importNode cria uma cópia de seu conteúdo, o segundo argumento (true) significa cópia profunda. Finalmente, nós o adicionamos à página como qualquer outro elemento.
Os modelos podem conter qualquer coisa que o HTML normal possa conter, incluindo CSS e JavaScript. Quando um elemento é adicionado à página, estilos são aplicados a ele e os scripts são iniciados. Lembre-se de que os estilos e scripts são globais, o que significa que podem substituir outros estilos e valores usados pelos scripts.
Os modelos não se limitam a isso. Eles aparecem em toda a sua glória quando usados com outras partes dos componentes, em particular o DOM de sombra.
3. Shadow DOM
- Evita conflitos de estilo
- Torna-se mais fácil encontrar nomes (de classes, por exemplo)
- Encapsulando a lógica de implementação
O Document Object Model (DOM) é como o navegador interpreta a estrutura da página. Ao ler a marcação, o navegador determina quais elementos contêm qual conteúdo e, com base nisso, toma uma decisão sobre o que deve ser exibido na página. Ao usar document.getElemetById (), por exemplo, o navegador acessa o DOM para encontrar o elemento de que precisa.
Para um layout de página, isso é bom, mas e os detalhes ocultos dentro do elemento? Por exemplo, uma página não deve se preocupar com a interface contida no elemento "vídeo". É aqui que o shadow DOM é útil.
<div id="shadow-root"></div>
<script>
const host = document.getElementById('shadow-root')
const shadow = host.attachShadow({ mode: 'open' })
</script>
O DOM sombra é criado quando aplicado a um elemento. Qualquer conteúdo pode ser adicionado ao DOM sombra, como um DOM regular ("leve"). O DOM sombra não é afetado pelo que acontece fora, ou seja, fora dele. O DOM simples também não pode acessar o shadow diretamente. Isso significa que no DOM sombra podemos usar quaisquer nomes de classe, estilos e scripts e não nos preocupar com possíveis conflitos.
Os melhores resultados são obtidos usando shadow DOM em conjunto com elementos personalizados. Graças ao DOM sombra, quando um componente é reutilizado, seus estilos e estrutura não afetam outros elementos na página de forma alguma.
Módulos ES e HTML
- Adicionando conforme necessário
- Não é necessária pré-geração
- Tudo é armazenado em um só lugar
Embora as três especificações anteriores tenham percorrido um longo caminho em seu desenvolvimento, como são embaladas e reutilizadas permanece um assunto de intenso debate.
A especificação HTML Imports define como os documentos HTML, bem como CSS e JavaScript, são exportados e importados. Isso permitiria que elementos personalizados, junto com modelos e DOM sombra, fossem localizados em outro lugar e usados conforme necessário.
No entanto, o Firefox se recusou a implementar essa especificação em seu navegador e ofereceu uma maneira diferente baseada em módulos JavaScript.
export class ExampleElement external HTMLElement {}
import { ExampleElement } from 'ExampleElement.js'
Módulos têm seu próprio namespace por padrão, ou seja, seu conteúdo não é global. Variáveis, funções e classes exportadas podem ser importadas em qualquer lugar e a qualquer hora e usadas como recursos locais.
Isso funciona muito bem para componentes. Os elementos personalizados que contêm o template e o DOM shadow podem ser exportados de um arquivo e usados em outro.
import { ExampleElement } from 'ExampleElement.html'
A Microsoft apresentou uma proposta para estender a especificação de módulos JavaScript com exportação / importação de HTML. Isso permitirá que você crie componentes usando HTML declarativo e semântico. Esse recurso chegará ao Chrome e ao Edge em breve.
Criando seu próprio componente
Embora existam muitas coisas sobre os componentes que você pode achar complexas, criar e usar um componente simples requer apenas algumas linhas de código. Vamos considerar alguns exemplos.
Os componentes permitem que você exiba comentários do usuário usando modelos de HTML e interfaces DOM de sombra.
Vamos criar um componente para exibir os comentários do usuário usando modelos HTML e DOM de sombra.
1. Criação de um modelo
O componente precisa de um modelo para copiar antes de gerar a marcação. O modelo pode estar localizado em qualquer lugar da página, a classe do elemento personalizado pode acessá-lo por meio do ID.
Adicione o elemento “template” à página. Quaisquer estilos definidos neste elemento apenas o afetarão.
<template id="user-comment-template">
<style>
...
</style>
</template>
2. Adicionando marcação
Além dos estilos, um componente pode conter um layout (estrutura). Para isso, é usado o elemento "div".
O conteúdo dinâmico é passado por slots. Adicione slots para o avatar, nome e mensagem do usuário com os atributos de "nome" apropriados:
<div class="container">
<div class="avatar-container">
<slot name="avatar"></slot>
</div>
<div class="comment">
<slot name="username"></slot>
<slot name="comment"></slot>
</div>
</div>
Conteúdo de slot padrão
O conteúdo padrão será exibido quando nenhuma informação for passada para o slot. Os
dados passados para o slot substituem os dados no modelo. Se nenhuma informação for passada para o slot, o conteúdo padrão será exibido.
Neste caso, se o nome de usuário não foi transferido, a mensagem "Sem nome" é exibida em seu lugar:
<slot name="username">
<span class="unknown">No name</span>
</slot>
3. Criação de uma classe
A criação de um elemento personalizado começa estendendo a classe "HTMLElement". Parte do processo de configuração é criar uma raiz de sombra para renderizar o conteúdo do elemento. Nós o abrimos para acesso na próxima etapa.
Por fim, informamos ao navegador sobre a nova classe UserComment.
class UserComment extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: 'open' })
}
}
customElements.define('user-comment', UserComment)
4. Aplicação de conteúdo de sombra
Quando o navegador encontra o elemento "comentário do usuário", ele examina o nó raiz da sombra para recuperar seu conteúdo. O segundo argumento instrui o navegador a copiar todo o conteúdo, não apenas a primeira camada (elementos de nível superior).
Adicionamos marcação ao nó raiz da sombra, que atualiza imediatamente a aparência do componente.
connectedCallback() {
const template = document.getElementById('user-comment-template')
const node = document.importNode(template.content, true)
this.shadowRoot.append(node)
}
5. Usando o componente
O componente agora está pronto para uso. Adicione a tag "user-comment" e passe as informações necessárias para ela.
Como todos os slots têm nomes, tudo o que for passado fora deles será ignorado. Tudo dentro dos slots é copiado exatamente como passado, incluindo o estilo.
<user-comment>
<img alt="" slot="avatar" src="avatar.png" />
<span slot="username">Matt Crouch</span>
<div slot="comment">This is an example of a comment</div>
</user-comment>
Código de exemplo estendido:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Web Components Example</title>
<style>
body {
display: grid;
place-items: center;
}
img {
width: 80px;
border-radius: 4px;
}
</style>
</head>
<body>
<template id="user-comment-template">
<div class="container">
<div class="avatar-container">
<slot name="avatar">
<slot class="unknown"></slot>
</slot>
</div>
<div class="comment">
<slot name="username">No name</slot>
<slot name="comment"></slot>
</div>
</div>
<style>
.container {
width: 320px;
clear: both;
margin-bottom: 1rem;
}
.avatar-container {
float: left;
margin-right: 1rem;
}
.comment {
height: 80px;
display: flex;
flex-direction: column;
justify-content: center;
}
.unknown {
display: block;
width: 80px;
height: 80px;
border-radius: 4px;
background: #ccc;
}
</style>
</template>
<user-comment>
<img alt="" slot="avatar" src="avatar1.jpg" />
<span slot="username">Matt Crouch</span>
<div slot="comment">Fisrt comment</div>
</user-comment>
<user-comment>
<img alt="" slot="avatar" src="avatar2.jpg" />
<!-- no username -->
<div slot="comment">Second comment</div>
</user-comment>
<user-comment>
<!-- no avatar -->
<span slot="username">John Smith</span>
<div slot="comment">Second comment</div>
</user-comment>
<script>
class UserComment extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
}
connectedCallback() {
const template = document.getElementById("user-comment-template");
const node = document.importNode(template.content, true);
this.shadowRoot.append(node);
}
}
customElements.define("user-comment", UserComment);
</script>
</body>
</html>
Criação de um elemento embutido personalizado
Conforme observado anteriormente, os elementos personalizados podem estender os existentes. Isso economiza tempo, mantendo o comportamento padrão do elemento fornecido pela chocadeira. Nesta seção, veremos como você pode estender o elemento "tempo".
1. Criação de uma classe
Os elementos integrados, como os independentes, aparecem quando a classe é estendida, mas em vez da classe geral "HTMLElement", eles estendem a classe específica.
No nosso caso, essa classe é HTMLTimeElement - a classe usada pelos elementos "time". Inclui comportamento relacionado ao atributo "datetime", incluindo formato de dados.
class RelativeTime extends HTMLTimeElement {}
2. Definição do elemento
O elemento é registrado pelo navegador usando o método "definir". No entanto, ao contrário de um elemento autônomo, ao registrar um elemento embutido, o método "define" deve receber um terceiro argumento - um objeto com configurações.
Nosso objeto conterá uma chave com o valor do elemento personalizado. Leva o nome da tag. Na ausência de tal chave, uma exceção será lançada.
customElements.define('relative-time', RelativeTime, { extends: 'time' })
3. Definição da hora
Como podemos ter vários componentes em uma página, o componente deve fornecer um método para definir o valor de um elemento. Dentro desse método, o componente passa um valor de tempo para a biblioteca "timeago" e define o valor retornado por essa biblioteca como o valor do item (desculpe pela tautologia).
Finalmente, definimos o atributo title para que o usuário possa ver o valor definido ao passar o mouse.
setTime() {
this.innerHTML = timeago().format(this.getAttribute('datetime'))
this.setAttribute('title', this.getAttribute('datetime'))
}
4. Atualização de conexão
O componente pode usar o método imediatamente após ser exibido na página. Como os componentes inline não têm um DOM shadow, eles não precisam de um construtor.
connectedCAllback() {
this.setTime()
}
5. Acompanhamento da mudança de atributos
Se você atualizar a hora programaticamente, o componente não responderá. Ele não sabe que precisa observar as mudanças no atributo "datetime".
Depois que os atributos observados forem definidos, attributeChangedCallback será chamado sempre que forem alterados.
static get observedAttributes() {
return ['datetime']
}
attributeChangedCallback() {
this.setTime()
}
6. Adicionando à página
Como nosso elemento é uma extensão do elemento nativo, sua implementação é um pouco diferente. Para usá-lo, adicione uma tag "time" à página com um atributo especial "is", cujo valor é o nome do elemento integrado definido durante o registro. Navegadores que não oferecem suporte a componentes renderizarão conteúdo substituto.
<time is="relative-time" datetime="2020-09-20T12:00:00+0000">
20 2020 . 12:00
</time>
Código de exemplo estendido:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Web Components Another Example</title>
<!-- timeago.js -->
<script
src="https://cdnjs.cloudflare.com/ajax/libs/timeago.js/4.0.2/timeago.min.js"
integrity="sha512-SVDh1zH5N9ChofSlNAK43lcNS7lWze6DTVx1JCXH1Tmno+0/1jMpdbR8YDgDUfcUrPp1xyE53G42GFrcM0CMVg=="
crossorigin="anonymous"
></script>
<style>
body {
display: flex;
flex-direction: column;
align-items: center;
}
input,
button {
margin-bottom: 0.5rem;
}
time {
font-size: 2rem;
}
</style>
</head>
<body>
<input type="text" placeholder="2020-10-20" value="2020-08-19" />
<button>Set Time</button>
<time is="relative-time" datetime="2020-09-19">
19 2020 .
</time>
<script>
class RelativeTime extends HTMLTimeElement {
setTime() {
this.innerHTML = timeago.format(this.getAttribute("datetime"));
this.setAttribute("title", this.getAttribute("datetime"));
}
connectedCallback() {
this.setTime();
}
static get observedAttributes() {
return ["datetime"];
}
attributeChangedCallback() {
this.setTime();
}
}
customElements.define("relative-time", RelativeTime, { extends: "time" });
const button = document.querySelector("button");
const input = document.querySelector("input");
const time = document.querySelector("time");
button.onclick = () => {
const { value } = input;
time.setAttribute("datetime", value);
};
</script>
</body>
</html>
Espero ter ajudado você a obter uma compreensão básica do que são os componentes da web, para que servem e como são usados.