16
loading...
This website collects cookies to deliver better user experience
UserCard.js
que conterá o código JavaScript deste componente e criamos uma classe representando este componente:// arquivo UserCard.js
class UserCard {
}
HTMLElement
:// arquivo UserCard.js
class UserCard extends HTMLElement {
}
super()
da interface HTMLElement
:// arquivo UserCard.js
class UserCard extends HTMLElement {
constructor() {
super();
}
}
CustomElementRegistry
- que está disponível globalmente pela variável customElements
e permite registrar um elemento customizado em uma página:// arquivo UserCard.js
class UserCard extends HTMLElement {
constructor() {
super();
}
}
customElements.define("user-card", UserCard);
define()
de customElements
recebe como parâmetro o nome da tag a ser definida e o objeto que vai encapsular o código necessário para sua construção. O nome da tag exige o caractere "-" (traço). Caso este padrão não seja seguido e o nome da tag seja definido, por exemplo, como usercard
, receberemos um DOMException
no momento de usar a tag:Uncaught DOMException: Failed to execute 'define' on 'CustomElementRegistry': "usercard" is not a valid custom element name
user-card
. E para usar nossa nova tag, devemos importá-la em um arquivo HTML e utilizar com a mesma sintaxe de uma tag comum:<!-- arquivo index.html -->
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<h2>Web Components</h2>
<user-card></user-card>
<script src="UserCard.js"></script>
</body>
</html>
index.html
. Todo elemento HTML tem a propriedade innerHTML
que corresponde ao seu conteúdo. Para vermos algum resultado, vamos sobrescrever esta propriedade com algum conteúdo - por exemplo, com o nome do usuário do componente cartão que estamos desenvolvendo:// arquivo UserCard.js
class UserCard extends HTMLElement {
constructor() {
super();
this.innerHTML = "<h2>Fulano de Tal<h2>"
}
}
customElements.define("user-card", UserCard);
Templates
.this.innerHTML = "<h2>Fulano de Tal</h2>"
. Ou seja, construiria esse elemento várias vezes, sendo que uma única vez já seria necessário. innerHTML
toda vez que o objeto é construído, podemos usar templates. Como dito na documentação do MDN Web Docs: O elemento HTML <template>
é um mecanismo para encapsular um conteúdo do lado do cliente que não é renderizado quando a página é carregada, mas que pode ser instanciado posteriormente em tempo de execução usando JavaScript.<template>
, este conteúdo não é mostrado imediatamente. Mas pode ser clonado para ser posteriormente renderizado:// arquivo UserCard.js
const template = document.createElement('template');
template.innerHTML = `<h2>Fulano de Tal</h2>`;
class UserCard extends HTMLElement {
constructor() {
super();
// código removido
}
}
customElements.define("user-card", UserCard);
content
. E para clonar o conteúdo, utilizamos o método cloneNode()
:template.content.cloneNode(true)
cloneNode()
recebe um parâmetro booleano para indicar se os elementos filhos do nó que está sendo clonado devem ser clonados juntos ou não. Vamos definir com o valor true
para clonar os filhos também.appendChild()
:// arquivo UserCard.js
const template = document.createElement('template');
template.innerHTML = `<h2>Fulano de Tal</h2>`;
class UserCard extends HTMLElement {
constructor() {
super();
this.appendChild(template.content.cloneNode(true));
}
}
customElements.define("user-card", UserCard);
DOMParser
, enquanto que chamar innerHTML
dentro do construtor irá analisar o HTML para cada instância. Isso garante uma melhoria na performance de nosso componente.<!-- arquivo index.html -->
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<h2>Web Components</h2>
<user-card name="Fulano de Tal"></user-card>
<user-card name="Ciclano de Tal"></user-card>
<script src="UserCard.js"></script>
</body>
</html>
name
é definido por nós e pode ter o nome que considerarmos conveniente. Neste momento, nosso template está com um conteúdo fixo e devemos modificar de acordo com atributo name
recebido pela nossa tag.// arquivo UserCard.js
const template = document.createElement('template');
template.innerHTML = `<h2></h2>`;
class UserCard extends HTMLElement {
constructor() {
super();
this.appendChild(template.content.cloneNode(true));
this._name = this.getAttribute("name");
this.querySelector("h2").textContent = this._name;
}
}
customElements.define("user-card", UserCard);
HTMLElement
, podemos usar e abusar de todos os recursos que uma tag comum do HTML possui, como o método getAttribute()
para pegar o valor do atributo name
que definimos anteriormente. E teremos o resultado:h2
diretamente no arquivo index.html
:<!-- arquivo index.html -->
<html>
<head>
<meta charset="UTF-8">
<style>
h2 {
color: red;
}
</style
</head>
<body>
<h2>Web Components</h2>
<user-card name="Fulano de Tal"></user-card>
<user-card name="Ciclano de Tal"></user-card>
<script src="UserCard.js"></script>
</body>
</html>
h2
, todos receberão o estilo global. Mas podemos adicionar um estilo específico para nosso componente, modificando a cor para azul, por exemplo. Podemos adicionar a tag <style>
em nosso template:// arquivo UserCard.js
const template = document.createElement('template');
template.innerHTML = `
<style>
h2 {
color: blue;
}
</style>
<h2></h2>`;
class UserCard extends HTMLElement {
constructor() {
super();
this.appendChild(template.content.cloneNode(true));
this._name = this.getAttribute("name");
this.querySelector("h2").textContent = this._name;
}
}
customElements.define("user-card", UserCard);
h2
, o estilo global dentro do arquivo index.html
e o estilo dentro de nosso componente. Qual será que vai ser aplicado em cada caso? Ao renderizar a página, obtemos:h2
fora dele. Isso acontece porque o template com o estilo de nosso componente é carregado por último e acaba por sobrescrever o estilo da tag h2
externo.Shadow DOM
. A ideia é encapsular código HTML, CSS e JavaScript de nosso componente para não provocar e/ou sofrer alteração externa.Shadow DOM
é uma sub árvore do DOM que tem seu próprio escopo e que não faz parte do DOM original, tornando possível construir interfaces modulares sem que entrem em conflito uma com a outra.Shadow DOM
que devemos conhecer:Shadow DOM
. Para isso, precisamos criar o nó raiz Shadow Root
dentro de nosso componente - que será o Shadow Host
. A classe HTMLElement
possui o método attachShadow()
que podemos utilizar para abrir e criar uma referência para um Shadow Root
. Shadow Root
possui dois modos: aberto e fechado. Antes de entrarmos nas diferenças entre esses dois modos, iremos criar nosso Shadow Root
no modo aberto para ver como ele funciona. O método attachShadow()
exige que passemos o modo como parâmetro:// arquivo UserCard.js
const template = document.createElement('template');
template.innerHTML = `
<style>
h2 {
color: blue;
}
</style>
<h2></h2>`;
class UserCard extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'}); // criando o Shadow Root
this.appendChild(template.content.cloneNode(true));
this._name = this.getAttribute("name");
this.querySelector("h2").textContent = this._name;
}
}
customElements.define("user-card", UserCard);
h2
:Shadow Root
foi criado inspecionando a página através da ferramenta DevTools
do navegador pela aba Elemets
:<user-card>
mas não é mostrado já que está fora do Shadow Root
. Uma vez aberto o Shadow Root
, devemos anexar os conteúdos, como nosso template, dentro dele. Após a chamada do método attachShadow()
, uma referência ao objeto Shadow Root
aberto é disponibilizado através do atributo shadowRoot
:// arquivo UserCard.js
const template = document.createElement('template');
template.innerHTML = `
<style>
h2 {
color: blue;
}
</style>
<h2></h2>`;
class UserCard extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
this.shadowRoot.appendChild(template.content.cloneNode(true)); // código modificado
this._name = this.getAttribute("name");
this.shadowRoot.querySelector("h2").textContent = this._name; // código modificado
}
}
customElements.define("user-card", UserCard);
Shadow Root
, vamos novamente inspecioná-lo pela ferramenta DevTools
:Shadow Root
. E como ele está dentro de uma Shadow Tree
separada do DOM original, os estilos globais não afetam nosso componente e o resultado da renderização da página é este:Shadow DOM
vai garantir o encapsulamento. Shadow DOM
funciona, vamos entender a diferença entre os modos aberto e fechado. O Shadow Root
no modo aberto permite que façamos modificações em sua estrutura utilizando JavaScript. Se quisermos acessar o Shadow Root
de nosso componente, basta digitar no console:document.querySelector("user-card").shadowRoot
shadowRoot
de nosso componente:h2
de nosso componente:Shadow DOM
. Vamos modificar nosso componente para o modo fechado:// arquivo UserCard.js
const template = document.createElement('template');
template.innerHTML = `
<style>
h2 {
color: blue;
}
</style>
<h2></h2>`;
class UserCard extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'closed'}); // modificado para o modo fechado
this.shadowRoot.appendChild(template.content.cloneNode(true));
this._name = this.getAttribute("name");
this.shadowRoot.querySelector("h2").textContent = this._name;
}
}
customElements.define("user-card", UserCard);
shadowRoot
não é mais possível. this.shadowRoot
agora vai retornar null
e receberemos o seguinte erro no console:shadowRoot
externamente pelo JavaScript:// arquivo UserCard.js
const template = document.createElement('template');
template.innerHTML = `
<style>
h2 {
color: blue;
}
</style>
<h2></h2>`;
class UserCard extends HTMLElement {
constructor() {
super();
this._shadowRoot = this.attachShadow({mode: 'closed'});
this._shadowRoot.appendChild(template.content.cloneNode(true));
this._name = this.getAttribute("name");
this._shadowRoot.querySelector("h2").textContent = this._name;
}
}
customElements.define("user-card", UserCard);
shadowRoot
, via JavaScript, continua retornando null
:UserCard
.UserCard.css
./* arquivo UserCard.css */
h2 {
color: blue;
}
<link>
:// arquivo UserCard.js
const template = document.createElement('template');
template.innerHTML = `
<link type="text/css" rel="stylesheet" href="UserCard.css"></link>
<h2></h2>`;
class UserCard extends HTMLElement {
// código omitido
}
customElements.define("user-card", UserCard);
// arquivo UserCard.js
const template = document.createElement('template');
template.innerHTML = `
<style>@import url("UserCard.css")</style>
<h2></h2>`;
class UserCard extends HTMLElement {
// código omitido
}
customElements.define("user-card", UserCard);
ShadowRoot
pode causar o temido FOUC (Flash of Unstyled Content) - ou seja, pode ocorrer um flash do conteúdo não estilizado enquanto o CSS é carregado. <style>
no template string ao invés de tentar evitar FOUC com código adicional - até o momento não existe uma maneira fácil e rápida de evitar isso. <style>
.<!-- arquivo index.html -->
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<h2>Web Components</h2>
<user-card name="Fulano de Tal" job="Desenvolvedor de Software" image="user.png"></user-card>
<script src="UserCard.js"></script>
</body>
</html>
// arquivo UserCard.js
const template = document.createElement('template');
template.innerHTML = `
<style>
.card {
font-family: Arial;
border: 1px solid #c5c9d1;
border-radius: 4%;
width: 150px;
height: 60px;
display: flex;
color: #5b6069;
font-size: 12px;
padding: 10px;
}
.card:hover {
background-color: hsl(0, 0%, 97%);
}
.card-image,
.card-content {
padding: 5px;
}
.user-image {
width: 45px;
height: 45px;
}
.user-name {
font-weight: bold;
}
.user-job {
font-style: italic;
font-size: 10px;
margin-top: 2px;
}
</style>
<div class="card">
<div class="card-image">
<img class="user-image" src="user.png"/>
</div>
<div class="card-content">
<div class="user-name"></div>
<div class="user-job"></div>
</div>
</div>`;
class UserCard extends HTMLElement {
constructor() {
super();
this._shadowRoot = this.attachShadow({mode: 'closed'});
this._shadowRoot.appendChild(template.content.cloneNode(true));
this._name = this.getAttribute("name");
this._job = this.getAttribute("job");
this._image = this.getAttribute("image");
this._shadowRoot.querySelector(".user-name").textContent = this._name;
this._shadowRoot.querySelector(".user-job").textContent = this._job;
this._shadowRoot.querySelector(".user-image").src = this._image;
}
}
customElements.define("user-card", UserCard);
Web Components
(componentes web) possui uma especificação própria. Como descrito no MDN Web Docs, Web Components
é uma suíte de diferentes tecnologias que permite a criação de elementos customizados reutilizáveis — com a funcionalidade separada do resto do seu código — e que podem ser utilizados em suas aplicações web.Web Components
não é necessário nenhuma biblioteca adicional ou framework, desde que o navegador implemente as seguintes especificações da Web Api:Web Componentes
é suportado por padrão no Firefox (versão 63), Chrome, Opera e Edge (versão 79). O Safari já suporta boa parte delas mas não todas. De todo modo, é possível usar Web Components
em qualquer navegador através do Polyfill - que nada mais é do que um pedaço de código (geralmente JavaScript) utilizado para simular os recursos ausentes do navegador o mais próximo possível.Web Components
é ainda um conceito novo quando utilizado em JavaScript nativo. Componentes são muito utilizados por bibliotecas e frameworks como Angular
, React
e Vue
- ferramentas sólidas e muito famosas dentro da comunidade front-end. E Web Components
, por ser nativo, pode ser utilizado juntamente com essas ferramentas.Web Components
, é possível criar um componente nativo que seja compartilhado entre os times. Ou seja, facilita a interoperabilidade do sistema.Web Components
com demais ferramentas, levando em consideração estilos de código, performance e bundle-size, é o All the Ways to Make a Web Component do pessoal do WebComponents.dev. Vale dar uma conferida!Web Components
e como construir um simples componente com pouco código. Web Components
vai muito além. Em futuros posts desta série pretendo mostrar outros recursos como o ciclo de vida de um componente, registro de eventos, componentes compostos e como podemos gerenciar melhor o estado de seus atributos. Até a próxima!