Introdução
A Web tem pouca expressão. Para entender o que quero dizer, confira um app da Web "moderno", como o Gmail:
A sopa de <div>
não é nada moderna. No entanto, é assim que criamos apps da Web. É triste.
Não deveríamos exigir mais da nossa plataforma?
Marcação sexy. Vamos fazer isso acontecer
O HTML é uma ferramenta excelente para estruturar um documento, mas o vocabulário dele é limitado a elementos definidos pelo padrão HTML.
E se a marcação do Gmail não fosse horrível? E se fosse bonito:
<hangout-module>
<hangout-chat from="Paul, Addy">
<hangout-discussion>
<hangout-message from="Paul" profile="profile.png"
profile="118075919496626375791" datetime="2013-07-17T12:02">
<p>Feelin' this Web Components thing.
<p>Heard of it?
</hangout-message>
</hangout-discussion>
</hangout-chat>
<hangout-chat>...</hangout-chat>
</hangout-module>
Que bom! Este app também faz sentido. Ele é significativo, fácil de entender e, o melhor de tudo, é manutenção. No futuro, você vai saber exatamente o que ele faz apenas examinando a estrutura declarativa.
Primeiros passos
Os elementos personalizados permitem que os desenvolvedores da Web definam novos tipos de elementos HTML. A especificação é uma das várias primitivas de API novas que estão sendo lançadas sob o guarda-chuva dos Web Components, mas é possivelmente a mais importante. Os componentes da Web não existem sem os recursos desbloqueados pelos elementos personalizados:
- Definir novos elementos HTML/DOM
- Criar elementos que se estendem de outros elementos
- Agrupar logicamente a funcionalidade personalizada em uma única tag
- Estender a API de elementos DOM
Como registrar novos elementos
Os elementos personalizados são criados usando document.registerElement()
:
var XFoo = document.registerElement('x-foo');
document.body.appendChild(new XFoo());
O primeiro argumento de document.registerElement()
é o nome da tag do elemento.
O nome precisa conter um traço (-). Por exemplo, <x-tags>
, <my-element>
e <my-awesome-app>
são nomes válidos, enquanto <tabs>
e <foo_bar>
não são. Essa restrição permite que o analisador
distingue elementos personalizados de elementos normais, mas também garante a compatibilidade
futura quando novas tags são adicionadas ao HTML.
O segundo argumento é um objeto (opcional) que descreve o prototype
do elemento.
É aqui que você adiciona funcionalidades personalizadas (por exemplo, propriedades e métodos públicos) aos elementos.
Mais informações sobre isso mais tarde.
Por padrão, os elementos personalizados herdam de HTMLElement
. Assim, o exemplo anterior é equivalente a:
var XFoo = document.registerElement('x-foo', {
prototype: Object.create(HTMLElement.prototype)
});
Uma chamada para document.registerElement('x-foo')
ensina o navegador sobre o novo elemento
e retorna um construtor que pode ser usado para criar instâncias de <x-foo>
.
Como alternativa, use as outras técnicas de instanciação de elementos
se você não quiser usar o construtor.
Como estender elementos
Os elementos personalizados permitem estender elementos HTML (nativos) e outros
elementos personalizados. Para estender um elemento, é necessário transmitir registerElement()
o nome
e prototype
do elemento a ser herdado.
Como estender elementos nativos
Digamos que você não está satisfeito com o <button>
do Joe comum. Você quer
aumentar os recursos dele para que ele seja um "Mega botão". Para estender o elemento <button>
,
crie um novo elemento que herda o prototype
de HTMLButtonElement
e extends
o nome do elemento. Nesse caso, "button":
var MegaButton = document.registerElement('mega-button', {
prototype: Object.create(HTMLButtonElement.prototype),
extends: 'button'
});
Os elementos personalizados que herdam de elementos nativos são chamados de elementos personalizados de extensão de tipo.
Eles herdam de uma versão especializada de HTMLElement
como uma forma de dizer "o elemento X é um Y".
Exemplo:
<button is="mega-button">
Como estender um elemento personalizado
Para criar um elemento <x-foo-extended>
que estenda o elemento personalizado <x-foo>
, basta herdar o protótipo dele
e informar de qual tag você está herdando:
var XFooProto = Object.create(HTMLElement.prototype);
...
var XFooExtended = document.registerElement('x-foo-extended', {
prototype: XFooProto,
extends: 'x-foo'
});
Consulte Como adicionar propriedades e métodos do JS abaixo para mais informações sobre como criar protótipos de elementos.
Como os elementos são atualizados
Você já se perguntou por que o analisador de HTML não gera erros em tags não padrão?
Por exemplo, ele vai funcionar perfeitamente se declararmos <randomtag>
na página. De acordo com a especificação HTML:
Desculpe, <randomtag>
. Você não é padrão e herda de HTMLUnknownElement
.
Isso não é verdade para elementos personalizados. Elementos com nomes de elementos personalizados válidos herdam de HTMLElement
. Para verificar isso, abra o console: Ctrl + Shift + J
(ou Cmd + Opt + J
no Mac) e cole as linhas de código a seguir. Elas retornam true
:
// "tabs" is not a valid custom element name
document.createElement('tabs').__proto__ === HTMLUnknownElement.prototype
// "x-tabs" is a valid custom element name
document.createElement('x-tabs').__proto__ == HTMLElement.prototype
Elementos não resolvidos
Como os elementos personalizados são registrados por script usando document.registerElement()
, eles podem ser
declarados ou criados antes de a definição ser registrada pelo navegador. Por exemplo,
é possível declarar <x-tabs>
na página, mas invocar document.registerElement('x-tabs')
muito mais tarde.
Antes de serem atualizados para a definição, os elementos são chamados de elementos não resolvidos. São elementos HTML que têm um nome de elemento personalizado válido, mas não foram registrados.
Esta tabela pode ajudar a esclarecer as coisas:
Nome | Herda de | Exemplos |
---|---|---|
Elemento não resolvido | HTMLElement |
<x-tabs> , <my-element> |
Elemento desconhecido | HTMLUnknownElement |
<tabs> , <foo_bar> |
Como instanciar elementos
As técnicas comuns de criação de elementos ainda se aplicam aos elementos personalizados. Como qualquer elemento padrão, eles podem ser declarados em HTML ou criados no DOM usando JavaScript.
Criar instâncias de tags personalizadas
Declare:
<x-foo></x-foo>
Criar DOM no JS:
var xFoo = document.createElement('x-foo');
xFoo.addEventListener('click', function(e) {
alert('Thanks!');
});
Use o operador new
:
var xFoo = new XFoo();
document.body.appendChild(xFoo);
Como instanciar elementos de extensão de tipo
A instanciação de elementos personalizados com estilo de extensão de tipo é muito semelhante às tags personalizadas.
Declare:
<!-- <button> "is a" mega button -->
<button is="mega-button">
Criar DOM em JS:
var megaButton = document.createElement('button', 'mega-button');
// megaButton instanceof MegaButton === true
Como você pode ver, agora há uma versão sobrecarregada de document.createElement()
que usa o atributo is=""
como segundo parâmetro.
Use o operador new
:
var megaButton = new MegaButton();
document.body.appendChild(megaButton);
Até agora, aprendemos a usar document.registerElement()
para informar ao navegador sobre uma nova tag, mas
ela não faz muito. Vamos adicionar propriedades e métodos.
Como adicionar propriedades e métodos do JS
O elemento personalizado é poderoso porque você pode agrupar a funcionalidade personalizada com o elemento definindo propriedades e métodos na definição do elemento. Pense nisso como uma maneira de criar uma API pública para seu elemento.
Confira um exemplo completo:
var XFooProto = Object.create(HTMLElement.prototype);
// 1. Give x-foo a foo() method.
XFooProto.foo = function() {
alert('foo() called');
};
// 2. Define a property read-only "bar".
Object.defineProperty(XFooProto, "bar", {value: 5});
// 3. Register x-foo's definition.
var XFoo = document.registerElement('x-foo', {prototype: XFooProto});
// 4. Instantiate an x-foo.
var xfoo = document.createElement('x-foo');
// 5. Add it to the page.
document.body.appendChild(xfoo);
É claro que existem milhares de maneiras de construir um prototype
. Se você não
gosta de criar protótipos assim, aqui está uma versão mais condensada da mesma coisa:
var XFoo = document.registerElement('x-foo', {
prototype: Object.create(HTMLElement.prototype, {
bar: {
get: function () {
return 5;
}
},
foo: {
value: function () {
alert('foo() called');
}
}
})
});
O primeiro formato permite o uso de Object.defineProperty
ES5. A segunda permite o uso de get/set.
Métodos de callback do ciclo de vida
Os elementos podem definir métodos especiais para aproveitar momentos interessantes da existência deles. Esses métodos são chamados de callbacks do ciclo de vida. Cada um tem um nome e uma finalidade específicos:
Nome do callback | Chamada quando |
---|---|
createdCallback | uma instância do elemento é criada |
attachedCallback | uma instância foi inserida no documento |
detachedCallback | uma instância foi removida do documento |
attributeChangedCallback(attrName, oldVal, newVal) | um atributo foi adicionado, removido ou atualizado |
Exemplo:definição de createdCallback()
e attachedCallback()
em <x-foo>
:
var proto = Object.create(HTMLElement.prototype);
proto.createdCallback = function() {...};
proto.attachedCallback = function() {...};
var XFoo = document.registerElement('x-foo', {prototype: proto});
Todos os callbacks de ciclo de vida são opcionais, mas defina-os quando e se fizer sentido.
Por exemplo, digamos que seu elemento seja suficientemente complexo e abra uma conexão com o IndexedDB
em createdCallback()
. Antes de ser removido do DOM, faça a limpeza
necessária em detachedCallback()
. Observação:não confie nesse recurso,
por exemplo, se o usuário fechar a guia, mas pense nele como um possível gancho de otimização.
Outro caso de uso de callbacks de ciclo de vida é para configurar listeners de eventos padrão no elemento:
proto.createdCallback = function() {
this.addEventListener('click', function(e) {
alert('Thanks!');
});
};
Adicionar marcação
Criamos <x-foo>
e atribuímos a ele uma API JavaScript, mas ele está em branco. Vamos
renderizar algum HTML?
Os callbacks do ciclo de vida são úteis aqui. Em particular, podemos usar
createdCallback()
para dar a um elemento um HTML padrão:
var XFooProto = Object.create(HTMLElement.prototype);
XFooProto.createdCallback = function() {
this.innerHTML = "**I'm an x-foo-with-markup!**";
};
var XFoo = document.registerElement('x-foo-with-markup', {prototype: XFooProto});
A instanciação dessa tag e a inspeção no DevTools (clique com o botão direito do mouse e selecione "Inspecionar elemento") vão mostrar:
▾<x-foo-with-markup>
**I'm an x-foo-with-markup!**
</x-foo-with-markup>
Encapsular os elementos internos no shadow DOM
O DOM shadow é uma ferramenta poderosa para encapsular conteúdo. Use-o com elementos personalizados e veja a mágica acontecer.
O shadow DOM oferece aos elementos personalizados:
- Uma maneira de esconder os detalhes da implementação, protegendo os usuários de detalhes sangrentos.
- Encapsulamento de estilo… é sem custo financeiro.
Criar um elemento do shadow DOM é como criar um que
renderiza marcação básica. A diferença está em createdCallback()
:
var XFooProto = Object.create(HTMLElement.prototype);
XFooProto.createdCallback = function() {
// 1. Attach a shadow root on the element.
var shadow = this.createShadowRoot();
// 2. Fill it with markup goodness.
shadow.innerHTML = "**I'm in the element's Shadow DOM!**";
};
var XFoo = document.registerElement('x-foo-shadowdom', {prototype: XFooProto});
Em vez de definir o .innerHTML
do elemento, criei uma
raiz de sombra para <x-foo-shadowdom>
e a preenchi com marcação.
Com a configuração "Mostrar DOM shadow" ativada nas ferramentas do desenvolvedor, você vai encontrar um
#shadow-root
que pode ser expandido:
▾<x-foo-shadowdom>
▾#shadow-root
**I'm in the element's Shadow DOM!**
</x-foo-shadowdom>
Essa é a raiz de sombra.
Como criar elementos usando um modelo
Os modelos HTML são outra primitiva de API nova que se encaixa bem no mundo dos elementos personalizados.
Exemplo:registro de um elemento criado com um <template>
e um shadow DOM:
<template id="sdtemplate">
<style>
p { color: orange; }
</style>
<p>I'm in Shadow DOM. My markup was stamped from a <template>.
</template>
<script>
var proto = Object.create(HTMLElement.prototype, {
createdCallback: {
value: function() {
var t = document.querySelector('#sdtemplate');
var clone = document.importNode(t.content, true);
this.createShadowRoot().appendChild(clone);
}
}
});
document.registerElement('x-foo-from-template', {prototype: proto});
</script>
<template id="sdtemplate">
<style>:host p { color: orange; }</style>
<p>I'm in Shadow DOM. My markup was stamped from a <template>.
</template>
<div class="demoarea">
<x-foo-from-template></x-foo-from-template>
</div>
Essas poucas linhas de código são muito eficientes. Vamos entender o que está acontecendo:
- Registramos um novo elemento no HTML:
<x-foo-from-template>
- O DOM do elemento foi criado a partir de um
<template>
- Os detalhes assustadores do elemento são ocultados usando o shadow DOM
- O shadow DOM fornece o encapsulamento de estilo do elemento (por exemplo,
p {color: orange;}
não está tornando toda a página laranja).
Muito bem!
Estilo de elementos personalizados
Como acontece com qualquer tag HTML, os usuários da sua tag personalizada podem estilizá-la com seletores:
<style>
app-panel {
display: flex;
}
[is="x-item"] {
transition: opacity 400ms ease-in-out;
opacity: 0.3;
flex: 1;
text-align: center;
border-radius: 50%;
}
[is="x-item"]:hover {
opacity: 1.0;
background: rgb(255, 0, 255);
color: white;
}
app-panel > [is="x-item"] {
padding: 5px;
list-style: none;
margin: 0 7px;
}
</style>
<app-panel>
<li is="x-item">Do</li>
<li is="x-item">Re</li>
<li is="x-item">Mi</li>
</app-panel>
Aplicar estilos em elementos que usam o shadow DOM
A toca do coelho fica muito mais profunda quando você inclui o shadow DOM. Os elementos personalizados que usam o shadow DOM herdam os grandes benefícios dele.
O shadow DOM infunde um elemento com encapsulamento de estilo. Os estilos definidos em uma raiz de sombra não são transmitidos para fora do host e não são transmitidos para a página. No caso de um elemento personalizado, o próprio elemento é o host. As propriedades da encapsulação de estilo também permitem que elementos personalizados definam estilos padrão para si mesmos.
O estilo do shadow DOM é um tópico enorme. Se quiser saber mais sobre o assunto, recomendo alguns dos meus outros artigos:
- "A Guide to Styling Elements" na documentação do Polymer.
- "Shadow DOM 201: CSS & Styling" (em inglês) aqui.
Prevenção de FOUC usando :unresolved
Para reduzir o FOUC, os elementos personalizados especificam
uma nova pseudoclasse de CSS, :unresolved
. Use-o para segmentar elementos não resolvidos,
até o ponto em que o navegador invoca o createdCallback()
(consulte métodos de ciclo de vida).
Depois disso, o elemento não é mais considerado não resolvido. O processo de upgrade é
concluído e o elemento é transformado na definição.
Exemplo: as tags "x-foo" aparecem gradualmente quando são registradas:
<style>
x-foo {
opacity: 1;
transition: opacity 300ms;
}
x-foo:unresolved {
opacity: 0;
}
</style>
:unresolved
se aplica apenas a elementos não resolvidos,
não a elementos que herdam de HTMLUnknownElement
. Consulte Como os elementos são atualizados.
<style>
/* apply a dashed border to all unresolved elements */
:unresolved {
border: 1px dashed red;
display: inline-block;
}
/* x-panel's that are unresolved are red */
x-panel:unresolved {
color: red;
}
/* once the definition of x-panel is registered, it becomes green */
x-panel {
color: green;
display: block;
padding: 5px;
display: block;
}
</style>
<panel>
I'm black because :unresolved doesn't apply to "panel".
It's not a valid custom element name.
</panel>
<x-panel>I'm red because I match x-panel:unresolved.</x-panel>
Histórico e suporte a navegadores
Detecção de recursos
A detecção de recursos é uma questão de verificar se document.registerElement()
existe:
function supportsCustomElements() {
return 'registerElement' in document;
}
if (supportsCustomElements()) {
// Good to go!
} else {
// Use other libraries to create components.
}
Suporte ao navegador
O document.registerElement()
começou a ser lançado atrás de uma flag no Chrome 27 e no Firefox ~23. No entanto, a especificação evoluiu bastante desde então. O Chrome 31 é o primeiro a ter
suporte real para a especificação atualizada.
Até que o suporte do navegador seja excelente, há um polyfill que é usado pelo Polymer do Google e pela X-Tag do Mozilla.
O que aconteceu com o HTMLElementElement?
Para quem acompanhou o trabalho de padronização, sabe que já houve <element>
.
Era a nata da nata. Você pode usá-lo para registrar novos elementos de forma declarativa:
<element name="my-element">
...
</element>
Infelizmente, houve muitos problemas de tempo com o processo de upgrade,
casos extremos e cenários de Armagedom para resolver tudo. <element>
teve que ser arquivado. Em agosto de 2013, Dimitri Glazkov postou no public-webapps anunciando a remoção, pelo menos por enquanto.
Vale ressaltar que o Polymer implementa uma forma declarativa de registro de elemento
com <polymer-element>
. Como? Ele usa document.registerElement('polymer-element')
e
as técnicas descritas em Como criar elementos usando um modelo.
Conclusão
Os elementos personalizados nos dão a ferramenta para ampliar o vocabulário do HTML, ensinar novos truques
e pular pelos buracos de minhoca da plataforma da Web. Combine-os com as outras
primitivas da plataforma, como o Shadow DOM e o <template>
, e você vai começar a entender
a imagem dos Web Components. A marcação pode ser sexy novamente!
Se você quiser começar a usar componentes da Web, recomendamos conferir o Polymer. Ele tem mais do que o suficiente para você começar.