Como vimos na publicação anterior, o termo web components se refere ao conjunto de APIs disponibilizadas na plataforma web que permitem a criação de elementos personalizados que podem ser instanciados de forma declarativa como qualquer tag HTML.
Com a API de elementos personalizados, os desenvolvedores são capazes de criar novas tags HTML. É a API fundamental para os web components e traz uma forma padronizada de criar componentes reutilizáveis com nada mais que HTML, CSS e JavaScript. Sem a necessidade de utilizar uma bibliotecas ou frameworks. Dessa forma, temos menos código, mais compatibilidade e mais reutilização.
Introdução 🙝
Como já vimos, para definir um elemento personalizado basta você estender a classe HTMLElement
e associar ela à uma tag, utilizando o método define
do global customElements
. Para exemplificar, vamos criar um componente chamado wc-counter
, que irá ser um simples contador.
class WCCounter extends HTMLElement {}
window.customElements.define('wc-counter', WCCounter);
Assim, podemos utilizar nosso elemento da seguinte forma:
<wc-counter></wc-counter>
É importante lembrar que utilizar um elemento personalizado não é diferente de utilizar um div
ou qualquer outro elemento do HTML. No entanto, algumas regras de nomenclatura precisam ser seguidas:
- O nome precisa conter um hífen (
-
). Dessa forma o analisador sintático pode distinguir um elemento personalizado de elementos padrões. Além disso, previne conflitos caso novas tags forem adicionadas ao HTML no futuro. - Você não pode registrar uma mesma tag mais uma vez. Fazer isso irá lançar um
DOMException
. Uma vez que uma tag está associada à uma classe, não há volta. - Elementos personalizados só podem ser utilizados em par. Ou seja, se você declarou um elemento como
meu-elemento
, você deve escrevê-lo como<meu-elemento></meu-elemento>
. Escrever<meu-elemento />
não funciona.
Definindo a API do elemento 🙝
Como utilizamos a sintaxe class
para definir o construtor do elemento, já utilizamos o extends
para estender o comportamento do HTMLElement
. Estender o comportamento do HTMLElement
garante que seu elemento irá seguir a API do DOM e significa que qualquer propriedade ou métodos que você adicione à classe se tornam parte da interface DOM do elemento. Ou seja, podemos utilizar a classe para definir a API pública da sua tag.
Imagine que queremos adicionar duas propriedades: value
e disabled
, que indica o valor atual do contador e se está desabilitado ou não, respectivamente. Vamos adicionar também dois métodos: increment
e decrement
, que aumentam e diminuem o contador em uma unidade. Veja:
Nesse exemplo estamos criando as propriedades value
e disabled
utilizando getters e setters de objetos do JavaScript. Além disso, estamos refletindo seus valores em atributos no DOM por meio dos métodos setAttribute
e removeAttribute
.
Definindo o comportamento do elemento 🙝
Na publicação anterior, vimos que é possível adicionar conteúdo ao elemento manipulando o DOM. Também vimos que para garantir o encapsulamento do elemento, é importante utilizar um shadow DOM. Vamos adicionar, ao shadow DOM, três elementos: um button
para decrementar o valor, um span
para exibir o valor e outro button
para incrementar o valor.
Podemos exibir os valor no span
apropriado e adicionar ouvintes para reagir as interações do usuário quando o elemento for conectado à árvore principal do DOM, por meio do método connectedCallback
. Além disso, podemos reagir às mudanças nas propriedades value
, assim, quando value
for alterado, vamos alterar o conteúdo do elemento. Podemos fazer isso alterando os setters de cada propriedade. Veja abaixo:
E se eu quiser instanciar nosso wc-counter
com um valor definido? Relembrando como outros elementos do HTML funcionam, bastaria eu adicionar o nome da propriedade e o valor na marcação, como abaixo:
<wc-counter value="8"></wc-counter>
No entanto, você vai perceber que isso não tem efeito algum. Não funciona porque ainda não sincronizamos o atributo value
à propriedade value
. Atributos e propriedades são mecanismos distintos no DOM e não necessariamente sincronizados. Um atributo indica uma marcação no documento e serializado em uma string, já uma propriedade é um valor no objeto e pode ter qualquer tipo de valor do JavaScript.
Reagindo a mudanças nos atributos 🙝
Para reagir a mudanças nos atributos precisamos definir quais atributos serão observados. Para isso, crie a propriedade estática observedAttributes
, que retorne o nome dos atributos que queira observar. Após isso, sobrescreva o método attributeChangedCallback
, que recebe como argumento três valores: o nome, o valor anterior e o valor atual do atributo. O método attributeChangedCallback
sempre é chamado quando um atributo observado for alterado. Veja:
Veja que o qualquer mudança no atributo irá ser refletida na propriedade. A marcação no HTML funciona como o esperado e inicializa a propriedade com o valor 8
. Também adicionamos a reação de desativar os botões caso o atributo disabled
esteja definido.
Estilizando de acordo com o estado 🙝
Como também já vimos, podemos estilizar o elemento utilizando um <style>
encapsulado pelo shadow DOM. Suponha que você queira estilizar o texto do span
para negrito, bastaria mudar a marcação atribuída no shadow DOM. No entanto, se você estilizar o “próprio” elemento hospedeiro do shadow DOM, você utiliza o pseudo-seletor :host
. Vamos mudar para que elemento hospedeiro se comporte tal como um block
:
Veja que também diminuímos a opacidade caso o elemento esteja desabilitado. Uma vez que nosso elemento reflete a propriedade no atributo disabled
, podemos utilizar seletores de atributos para reagir a essa mudança.
Elementos personalizados podem ser estilizados utilizando CSS como qualquer outro elemento HTML. Ou seja, se o usuário do nosso elemento wc-counter
quiser mudar o elemento, ele pode. Lembre-se, o encapsulamento só impede que os estilos da shadow DOM “vazem” para o DOM principal, no entanto o efeito cascata do CSS continua valendo.
Veja também que “adicionamos” um delay na definição do elemento, e, enquanto ele não é definido, aplicou-se um estilo temporário com o uso da pseudo-classe :defined
. Caso não tenha visto, recarregue o resultado clicando no botão “↻”.
Elementos não definidos e desconhecidos 🙝
O HTML é bastante flexível. Por exemplo, ao declarar uma tag <accordion>
em um documento, o navegador vai aceitá-la sem problemas, mesmo que <accordion>
não faça parte do vocabulário. Esse comportamento é previsto pela própria especificação do HTML. Elementos desconhecidos são instâncias de HTMLUnknownElement
.
O mesmo não vale para elementos personalizados. Elementos personalizados “potenciais” são sempre instâncias de HTMLElement
, mesmo que ainda não foram definidos. Ou seja, se você criar um elemento com uma tag que contém um -
, esse elemento será instância de HTMLElement
. Veja:
// "accordion" não é um elemento conhecido do HTML nem um elemento personalizado
document.createElement('accordion') instanceof HTMLUnknownElement; // → true
// "wc-accordion" é um nome válido para elemento personalizado
document.createElement('wc-accordion') instanceof HTMLUnknownElement; // → false
O global customElements
também contém alguns métodos úteis. Por exemplo, caso você já tenha registrado um elemento personalizado, você pode pegar uma referência do construtor utilizando o método get
e passando a tag. Caso o elemento não tenha sido registrado ainda, get
retorna undefined
.
let WCCounter = customElements.get('wc-counter');
let counter = new WCCounter();
Você também pode definir callbacks para futuras definições de elementos com o método whenDefined
. Este método retorna uma instância de Promise
que resolve-se quando o elemento for definido ou rejeita-se quando o nome passado não for um nome de elemento personalizado válido.
customElements.whenDefined('wc-counter').then(() => {
console.log('"wc-counter" está pronto!');
});
Conclusão 🙝
Elementos personalizados são uma forma de definir novas tags HTML e permitem a criação de componentes reutilizáveis quando combinado com outras tecnologias, como o shadow DOM. Eles fornecem várias vantagens:
- Interoperabilidade entre os navegadores;
- São bem integrados à ferramentas de debug já presentes nos navegadores e conhecidas pelos desenvolvedores;
- Não precisam de qualquer biblioteca ou framework para iniciar e provêm um modelo de programação familiar ao DOM. Você precisa apenas de HTML, CSS e JavaScript.
Mas essa série de publicações ainda não acabou. Ainda temos que falar sobre outras especificações que pertencem ao “ecossistema” de web components. Até a próxima!