[Leitura recomendada] As outras 19 partes do ciclo Apresentamos a você uma tradução de 19 artigos da série
SessionStack de materiais sobre os recursos de vários mecanismos do ecossistema JavaScript. Hoje falaremos sobre o padrão Custom Elements - os chamados "elementos customizados". Falaremos sobre quais tarefas eles permitem resolver e como criá-las e usá-las.
Revisão
Em um dos artigos anteriores desta série, falamos sobre o
Shadow DOM e algumas outras tecnologias que fazem parte de um fenômeno maior - componentes da Web. Os componentes da Web são projetados para permitir que os desenvolvedores estendam os recursos padrão do HTML criando elementos compactos, modulares e reutilizáveis. Esse é o padrão W3C relativamente novo que os fabricantes de todos os principais navegadores já notaram. Ele pode ser encontrado em produção, embora, é claro, enquanto seu trabalho é fornecido por polifilos (falaremos sobre eles mais adiante).
Como você já deve saber, os navegadores nos fornecem algumas ferramentas essenciais para o desenvolvimento de sites e aplicativos da web. É sobre HTML, CSS e JavaScript. O HTML é usado para estruturar as páginas da Web. Graças ao CSS, elas têm uma boa aparência e o JavaScript é responsável por recursos interativos. No entanto, antes do aparecimento dos componentes da web, não era tão fácil associar ações implementadas por JavaScript a uma estrutura HTML.
Por uma questão de fato, aqui consideraremos a base dos componentes da web - elementos personalizados. Em poucas palavras, a API projetada para trabalhar com eles permite que o programador crie seus próprios elementos HTML com a lógica JavaScript incorporada e os estilos descritos por CSS. Muitos confundem elementos personalizados com a tecnologia Shadow DOM. No entanto, essas são duas coisas completamente diferentes que, de fato, se complementam, mas não são intercambiáveis.
Algumas estruturas (como Angular ou React) tentam resolver o mesmo problema que os elementos personalizados resolvem, introduzindo seus próprios conceitos. Elementos personalizados podem ser comparados com diretivas angulares ou com componentes React. No entanto, os elementos personalizados são um recurso padrão do navegador; você não precisa usar nada além de JavaScript, HTML e CSS comuns para trabalhar com eles. Obviamente, isso não nos permite dizer que eles substituem as estruturas JS comuns. As estruturas modernas nos dão muito mais do que apenas a capacidade de simular o comportamento de elementos personalizados. Como resultado, podemos dizer que as estruturas e os elementos do usuário são tecnologias que podem ser usadas juntas para resolver tarefas de desenvolvimento da web.
API
Antes de continuarmos, vamos ver quais oportunidades a API nos oferece para trabalhar com elementos personalizados. Ou seja, estamos falando de um objeto
customElements
global que possui vários métodos:
- O método
define(tagName, constructor, options)
permite definir (criar, registrar) um novo elemento do usuário. São necessários três argumentos - o nome da tag do elemento user, correspondente às regras de nomenclatura para esses elementos, uma declaração de classe e um objeto com parâmetros. Atualmente, apenas um parâmetro é suportado - extends
, que é uma sequência que especifica o nome do elemento inline a ser expandido. Esse recurso é usado para criar versões especiais de elementos padrão. - O método
get(tagName)
retorna o construtor do elemento user, desde que esse elemento já esteja definido, caso contrário ele retorna undefined
. É preciso um argumento - a tag de nome do elemento do usuário. - O
whenDefined(tagName)
retorna a promessa que é resolvida após a criação do elemento do usuário. Se um elemento já estiver definido, essa promessa será resolvida imediatamente. Uma promessa será rejeitada se o nome da tag passado para ela não for um nome válido para o elemento do usuário. Este método aceita o nome da tag do elemento do usuário.
Crie itens personalizados
Criar elementos personalizados é muito simples. Para fazer isso, duas coisas devem ser feitas: crie uma declaração de classe para o elemento que deve estender a classe
HTMLElement
e registre esse elemento com o nome selecionado. Aqui está o que parece:
class MyCustomElement extends HTMLElement { constructor() { super();
Se você não deseja poluir o escopo atual, pode usar uma classe anônima:
customElements.define('my-custom-element', class extends HTMLElement { constructor() { super();
Como você pode ver nos exemplos, o elemento user é registrado usando o método
customElements.define(...)
você já conhece.
Problemas que os Elementos Customizados Resolvem
Vamos falar sobre os problemas que nos permitem resolver elementos personalizados. Uma delas é melhorar a estrutura do código e eliminar o que é chamado de "div tag soup" (sopa de div). Esse fenômeno é uma estrutura de código muito comum em aplicativos da web modernos, na qual existem muitos elementos
div
incorporados um no outro. Aqui está o que pode parecer:
<div class="top-container"> <div class="middle-container"> <div class="inside-container"> <div class="inside-inside-container"> <div class="are-we-really-doing-this"> <div class="mariana-trench"> … </div> </div> </div> </div> </div> </div>
Esse código HTML é usado por razões justificáveis - descreve o layout da página e garante a exibição correta na tela. No entanto, isso prejudica a legibilidade do código HTML e complica sua manutenção.
Suponha que tenhamos um componente que se parece com a figura a seguir.
Aparência do componenteUsando a abordagem tradicional para descrever essas coisas, o código a seguir corresponderá a esse componente:
<div class="primary-toolbar toolbar"> <div class="toolbar"> <div class="toolbar-button"> <div class="toolbar-button-outer-box"> <div class="toolbar-button-inner-box"> <div class="icon"> <div class="icon-undo"> </div> </div> </div> </div> </div> <div class="toolbar-button"> <div class="toolbar-button-outer-box"> <div class="toolbar-button-inner-box"> <div class="icon"> <div class="icon-redo"> </div> </div> </div> </div> </div> <div class="toolbar-button"> <div class="toolbar-button-outer-box"> <div class="toolbar-button-inner-box"> <div class="icon"> <div class="icon-print"> </div> </div> </div> </div> </div> <div class="toolbar-toggle-button toolbar-button"> <div class="toolbar-button-outer-box"> <div class="toolbar-button-inner-box"> <div class="icon"> <div class="icon-paint-format"> </div> </div> </div> </div> </div> </div> </div>
Agora imagine que poderíamos, em vez deste código, usar esta descrição do componente:
<primary-toolbar> <toolbar-group> <toolbar-button class="icon-undo"></toolbar-button> <toolbar-button class="icon-redo"></toolbar-button> <toolbar-button class="icon-print"></toolbar-button> <toolbar-toggle-button class="icon-paint-format"></toolbar-toggle-button> </toolbar-group> </primary-toolbar>
Estou certo de que todos concordarão que o segundo fragmento de código parece muito melhor. Esse código é mais fácil de ler, mais fácil de manter e é compreensível para o desenvolvedor e o navegador. Tudo se resume ao fato de ser mais simples do que aquele em que existem muitas tags
div
aninhadas.
O próximo problema que pode ser resolvido com elementos personalizados é a reutilização de código. O código que os desenvolvedores escrevem não deve apenas estar funcionando, mas também suportado. Reutilizar código, em vez de escrever constantemente as mesmas construções, melhora os recursos de suporte ao projeto.
Aqui está um exemplo simples que o ajudará a entender melhor essa ideia. Suponha que tenhamos o seguinte elemento:
<div class="my-custom-element"> <input type="text" class="email" /> <button class="submit"></button> </div>
Se você precisar constantemente, com a abordagem usual, teremos que escrever o mesmo código HTML repetidamente. Agora imagine que você precisa fazer uma alteração nesse código que deve refletir-se onde quer que seja usado. Isso significa que precisamos encontrar todos os locais onde esse fragmento é usado e, em seguida, fazer as mesmas alterações em todos os lugares. É longo, difícil e cheio de erros.
Seria muito melhor se pudéssemos onde esse elemento é necessário, basta escrever o seguinte:
<my-custom-element></my-custom-element>
No entanto, aplicativos web modernos são muito mais que HTML estático. Eles são interativos. A fonte de sua interatividade é o JavaScript. Geralmente, para fornecer esses recursos, alguns elementos são criados e os ouvintes de eventos são conectados a eles, o que lhes permite responder às influências do usuário. Por exemplo, eles podem responder a cliques, ao "pairar" do ponteiro do mouse sobre eles, arrastando-os pela tela e assim por diante. Veja como conectar um ouvinte de evento a um elemento que ocorre quando você clica nele com o mouse:
var myDiv = document.querySelector('.my-custom-element'); myDiv.addEventListener('click', _ => { myDiv.innerHTML = '<b> I have been clicked </b>'; });
E aqui está o código HTML para este elemento:
<div class="my-custom-element"> I have not been clicked yet. </div>
Ao usar a API para trabalhar com elementos personalizados, toda essa lógica pode ser incluída no próprio elemento. Para comparação - abaixo está o código para declarar um elemento personalizado que inclui um manipulador de eventos:
class MyCustomElement extends HTMLElement { constructor() { super(); var self = this; self.addEventListener('click', _ => { self.innerHTML = '<b> I have been clicked </b>'; }); } } customElements.define('my-custom-element', MyCustomElement);
E aqui está como fica no código HTML da página:
<my-custom-element> I have not been clicked yet </my-custom-element>
À primeira vista, pode parecer que mais linhas de código JS são necessárias para criar um elemento customizado. No entanto, em aplicativos do mundo real, raramente acontece que esses elementos sejam criados apenas para serem usados apenas uma vez. Outro fenômeno típico em aplicativos da web modernos é que a maioria dos elementos neles são criados dinamicamente. Isso leva à necessidade de oferecer suporte a dois cenários diferentes de trabalho com elementos - situações em que eles são adicionados à página dinamicamente usando JavaScript e situações em que são descritos na estrutura HTML original da página. Graças ao uso de elementos personalizados, o trabalho nessas duas situações é simplificado.
Como resultado, se resumirmos os resultados desta seção, podemos dizer que os elementos do usuário tornam o código mais claro, simplificam seu suporte, ajudam a dividi-lo em pequenos módulos, que incluem toda a funcionalidade necessária e são adequados para reutilização.
Agora que discutimos os problemas gerais de trabalhar com elementos personalizados, vamos falar sobre seus recursos.
Exigências
Antes de começar a desenvolver seus próprios elementos personalizados, você deve conhecer algumas das regras que deve seguir ao criá-las. Aqui estão elas:
- O nome do componente deve incluir um hífen (símbolo
-
). Graças a isso, o analisador HTML pode distinguir entre elementos incorporados e elementos do usuário. Além disso, essa abordagem garante que não haja colisões de nomes com elementos internos (tanto com os que estão agora como com os que aparecerão no futuro). Por exemplo, o nome real do elemento customizado é >my-custom-element<
, e os nomes >myCustomElement<
e <my_custom_element>
não são adequados. - É proibido registrar a mesma tag mais de uma vez. Tentar fazer isso fará com que o navegador
DOMException
erro de DOMException
. Elementos personalizados não podem ser redefinidos. - Tags personalizadas não podem ser fechadas automaticamente. O analisador HTML suporta apenas um conjunto limitado de tags de fechamento automático padrão (por exemplo,
<img>
, <link>
, <br>
).
As possibilidades
Vamos falar sobre o que você pode fazer com elementos personalizados. Se você responder a essa pergunta em poucas palavras, acontece que você pode fazer muitas coisas interessantes com elas.
Um dos recursos mais notáveis dos elementos personalizados é que a declaração de uma classe de elemento se refere ao próprio elemento DOM. Isso significa que você pode usar a palavra-chave this em um anúncio para conectar ouvintes de eventos, acessar propriedades, nós filhos e assim por diante.
class MyCustomElement extends HTMLElement {
Isso, é claro, possibilita a gravação de novos dados nos nós filhos do elemento. No entanto, não é recomendável fazer isso, pois isso pode levar a um comportamento inesperado dos elementos. Se você imaginar que está usando elementos projetados por outra pessoa, provavelmente ficará surpreso se sua própria marcação colocada no elemento for substituída por outra.
Existem vários métodos que permitem executar o código em determinados pontos do ciclo de vida de um elemento.
- O método
constructor
é chamado uma vez, ao criar ou "atualizar" o elemento (falaremos sobre isso abaixo). Na maioria das vezes, é usado para inicializar o estado de um elemento, conectar ouvintes de eventos, criar um DOM DOM e assim por diante. Não esqueça que você sempre precisa chamar super()
no construtor. - O método
connectedCallback
é chamado toda vez que um elemento é adicionado ao DOM. Ele pode ser usado (e é exatamente dessa maneira que é recomendável usá-lo) para adiar a execução de qualquer ação até o momento em que o elemento aparecer na página (por exemplo, desta forma, você pode atrasar o carregamento de alguns dados). - O
disconnectedCallback
é chamado quando um item é removido do DOM. Geralmente é usado para liberar recursos. Observe que esse método não é chamado se o usuário fechar a guia do navegador com a página. Portanto, não confie nele quando necessário para executar algumas ações particularmente importantes. - O método
attributeChangedCallback
é chamado quando um attributeChangedCallback
elemento é adicionado, removido, atualizado ou substituído. Além disso, é chamado quando o elemento é criado pelo analisador. No entanto, observe que esse método se aplica apenas aos atributos listados na propriedade observedAttributes
. - O método
adoptedCallback
chamado quando o método document.adoptNode(...)
é usado, usado para mover o nó para outro documento.
Observe que todos os métodos acima são síncronos. Por exemplo, o método
connectedCallback
é chamado imediatamente após o elemento ser adicionado ao DOM, e o restante do programa aguarda a conclusão desse método.
Reflexão da propriedade
Os elementos HTML incorporados têm um recurso muito conveniente: reflexão de propriedade. Graças a esse mecanismo, os valores de algumas propriedades são refletidos diretamente no DOM como atributos. Digamos que isso seja característico da propriedade
id
. Por exemplo, realizamos a seguinte operação:
myDiv.id = 'new-id';
Alterações relevantes afetarão o DOM:
<div id="new-id"> ... </div>
Este mecanismo opera na direção oposta. É muito útil porque permite configurar elementos declarativamente.
Os elementos personalizados não possuem esse recurso interno, mas você pode implementá-lo. Para que algumas propriedades dos elementos do usuário se comportem de maneira semelhante, você pode configurar seus getters e setters.
class MyCustomElement extends HTMLElement {
Estendendo itens existentes
A API de elementos personalizados permite criar não apenas novos elementos HTML, mas também estender os existentes. Além disso, estamos falando de elementos padrão e personalizados. Isso é feito usando a
extends
ao declarar uma classe:
class MyAwesomeButton extends MyButton {
Elementos padrão estendidos também são chamados de "elemento interno personalizado".
É recomendável estabelecer uma regra para sempre expandir elementos existentes e fazê-lo progressivamente. Isso permitirá que você salve em novos elementos os recursos que foram implementados nos elementos criados anteriormente (ou seja, propriedades, atributos, funções).
Observe que agora os elementos internos personalizados são suportados apenas no Chrome 67 ou superior. Isso aparecerá em outros navegadores, no entanto, sabe-se que os desenvolvedores do Safari decidiram não implementar esta oportunidade.
Atualizar itens
Como já mencionado, o
customElements.define(...)
é usado para registrar elementos customizados. No entanto, o registro não pode ser chamado de ação que deve ser executada em primeiro lugar. O registro do elemento do usuário pode ser adiado por um tempo; além disso, esse momento pode chegar mesmo quando o elemento já foi adicionado ao DOM. Esse processo é chamado de atualização. Para descobrir quando um item será registrado, o navegador fornece o
customElements.whenDefined(...)
. Ele recebe o nome da tag do elemento e retorna a promessa que é resolvida após o registro do elemento.
customElements.whenDefined('my-custom-element').then(_ => { console.log('My custom element is defined'); });
Por exemplo, pode ser necessário adiar o registro de um elemento até que seus filhos sejam declarados. Essa linha de comportamento pode ser extremamente útil se o projeto aninhar elementos do usuário. Às vezes, um pai pode confiar na implementação de elementos filho. Nesse caso, você precisa garantir que os filhos sejam registrados antes dos pais.
Shadow dom
Como já mencionado, os elementos personalizados e o Shadow DOM são tecnologias complementares. A primeira permite encapsular a lógica JS nos elementos do usuário e a segunda permite criar ambientes isolados para fragmentos DOM que não são afetados pelo que está fora deles. Se você acha que precisa entender melhor o conceito Shadow DOM, dê uma olhada em uma de nossas
publicações anteriores .
Veja como usar o Shadow DOM para um elemento personalizado:
class MyCustomElement extends HTMLElement {
Como você pode ver, chamar
this.attachShadow
desempenha um papel fundamental
this.attachShadow
.
Padrões
Em um de nossos artigos
anteriores , falamos um pouco sobre modelos, embora eles sejam, de fato, dignos de um artigo separado. A seguir, veremos um exemplo simples de como incorporar modelos em elementos personalizados quando eles são criados. Portanto, usando a
<template>
, você pode descrever o fragmento DOM que será processado pelo analisador, mas não será exibido na página:
<template id="my-custom-element-template"> <div class="my-custom-element"> <input type="text" class="email" /> <button class="submit"></button> </div> </template>
Veja como aplicar um modelo em um elemento personalizado:
let myCustomElementTemplate = document.querySelector('#my-custom-element-template'); class MyCustomElement extends HTMLElement {
Como você pode ver, há uma combinação de um elemento personalizado, um Shadow DOM e modelos. Isso nos permitiu criar um elemento isolado em seu próprio espaço, no qual a estrutura HTML é separada da lógica JS.
Estilização
Até agora, falamos apenas de JavaScript e HTML, ignorando CSS. Portanto, agora abordamos o tema dos estilos. Obviamente, precisamos de uma maneira de estilizar elementos personalizados. Os estilos podem ser adicionados dentro do Shadow DOM, mas surge a questão de como estilizar esses elementos de fora, por exemplo - se eles não forem usados pela pessoa que os criou. A resposta a esta pergunta é bastante simples - os elementos personalizados são estilizados da mesma maneira que os elementos internos.
my-custom-element { border-radius: 5px; width: 30%; height: 50%;
Observe que os estilos externos têm precedência sobre os estilos declarados dentro de um elemento, substituindo-os.
Você pode ter visto como, quando uma página é exibida na tela, em algum momento você pode observar conteúdo não estilizado nela (é o que se chama FOUC - Flash Of Unstyled Content). Você pode evitar esse fenômeno definindo estilos para componentes não registrados e usando alguns efeitos visuais ao registrá-los. Para fazer isso, você pode usar o seletor
:defined
. Você pode fazer isso, por exemplo, assim:
my-button:not(:defined) { height: 20px; width: 50px; opacity: 0; }
Elementos desconhecidos e elementos de usuário indefinidos
A especificação HTML é muito flexível, permite declarar as tags necessárias para o desenvolvedor. E, se a tag não for reconhecida pelo navegador, será processada pelo analisador como
HTMLUnknownElement
:
var element = document.createElement('thisElementIsUnknown'); if (element instanceof HTMLUnknownElement) { console.log('The selected element is unknown'); }
No entanto, ao trabalhar com elementos personalizados, esse esquema não se aplica. , ? , ,
HTMLElement
.
var element = document.createElement('this-element-is-undefined'); if (element instanceof HTMLElement) { console.log('The selected element is undefined but not unknown'); }
HTMLElement
HTMLUnknownElement
, , , , - . , , , .
div
. .
Chrome 36+. API Custom Components v0, , , , . API, , —
. API Custom Elements v1 Chrome 54+ Safari 10.1+ ( ). Mozilla v50, , . , Microsoft Edge API. , , webkit. , , , — IE 11.
, , ,
customElements
window
:
const supportsCustomElements = 'customElements' in window; if (supportsCustomElements) {
:
function loadScript(src) { return new Promise(function(resolve, reject) { const script = document.createElement('script'); script.src = src; script.onload = resolve; script.onerror = reject; document.head.appendChild(script); }); }
Sumário
, :
- HTML- JavaScript-, , CSS-.
- HTML- ( , ).
- . , — JavaScript, HTML, CSS, , , .
- - (Shadow DOM, , , ).
- , .
- , .
,
Custom Elements v1 , , , , , .
Caros leitores! ?
