Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
July 13, 2021 12:27 pm GMT

Web Components: uma introduo

Imagine um projeto web que mostre os dados dos usurios em um componente carto que ser usado em vrias pginas no projeto:

Card do usurio

Ao invs de copiar e colar este cdigo em vrios arquivos HTML diferentes, podemos criar nossa prpria tag que renderiza este carto e encapsula os estilos (CSS) e comportamentos (JavaScript).

Primeiro, criamos o um arquivo UserCard.js que conter o cdigo JavaScript deste componente e criamos uma classe representando este componente:

// arquivo UserCard.jsclass UserCard {}

At aqui, esta apenas a declarao de uma classe em JavaScript.

Custom Elements

Como queremos criar uma tag, devemos defin-la como um elemento HTML. Para isto, basta fazer nossa classe implementar a interface HTMLElement:

// arquivo UserCard.jsclass UserCard extends HTMLElement {}

HTMLElement uma interface que implementa outra chamada Element - que a interface base mais geral da qual todos os objetos em um Document implementam. De acordo com a documentao, se queremos criar uma tag, o mais indicado usar HTMLElement, j que esta prov todos os recursos necessrios para construo de uma tag HTML.

Aps isso, colocamos o construtor e chamamos o super() da interface HTMLElement:

// arquivo UserCard.jsclass UserCard extends HTMLElement {    constructor() {        super();    }}

E, por ltimo, precisamos registrar nossa tag no CustomElementRegistry - que est disponvel globalmente pela varivel customElements e permite registrar um elemento customizado em uma pgina:

// arquivo UserCard.jsclass UserCard extends HTMLElement {    constructor() {        super();    }}customElements.define("user-card", UserCard);

O mtodo define() de customElements recebe como parmetro o nome da tag a ser definida e o objeto que vai encapsular o cdigo necessrio para sua construo. O nome da tag exige o caractere "-" (trao). Caso este padro no 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

Por este motivo manteremos o nome como 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>

Como nossa tag no faz nada at o momento, nada vai aparecer no navegador alm da frase "Web Components" ao abrir o arquivo index.html. Todo elemento HTML tem a propriedade innerHTML que corresponde ao seu contedo. Para vermos algum resultado, vamos sobrescrever esta propriedade com algum contedo - por exemplo, com o nome do usurio do componente carto que estamos desenvolvendo:

// arquivo UserCard.jsclass UserCard extends HTMLElement {    constructor() {        super();        this.innerHTML = "<h2>Fulano de Tal<h2>"    }}customElements.define("user-card", UserCard);

Que vai gerar o resultado:

Resultado da pgina renderizada

Templates

Nossa tag customizada, apesar de simples, j funciona como esperado. Agora vamos utilizar e entender um pouco sobre outro recurso bastante utilizado quando trabalhamos com componentes web que so os Templates.

Com templates, possvel definir blocos de cdigos reutilizveis. Apesar de j conseguirmos isso sem eles, os templates apresentam uma forma mais racional de fazer isso.

Suponha que queremos repetir o uso de nosso componente vrias vezes na pgina. Seriam muitas chamadas de this.innerHTML = "<h2>Fulano de Tal</h2>". Ou seja, construiria esse elemento vrias vezes, sendo que uma nica vez j seria necessrio.

Ao invs de adicionarmos o contedo com innerHTML toda vez que o objeto construdo, podemos usar templates. Como dito na documentao do MDN Web Docs: O elemento HTML <template> um mecanismo para encapsular um contedo do lado do cliente que no renderizado quando a pgina carregada, mas que pode ser instanciado posteriormente em tempo de execuo usando JavaScript.

Portanto, ao criarmos algum contedo dentro da tag <template>, este contedo no mostrado imediatamente. Mas pode ser clonado para ser posteriormente renderizado:

// arquivo UserCard.jsconst template = document.createElement('template');template.innerHTML = `<h2>Fulano de Tal</h2>`;class UserCard extends HTMLElement {    constructor() {        super();        // cdigo removido    }}customElements.define("user-card", UserCard);

Note que criamos o template fora da classe. Agora ser preciso clonar o contedo deste template que est disponvel pelo atributo content. E para clonar o contedo, utilizamos o mtodo cloneNode():

template.content.cloneNode(true)

O mtodo cloneNode() recebe um parmetro booleano para indicar se os elementos filhos do n que est sendo clonado devem ser clonados juntos ou no. Vamos definir com o valor true para clonar os filhos tambm.

Agora devemos pegar este elemento clonado e adicionar em nosso componente atravs do mtodo appendChild():

// arquivo UserCard.jsconst 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);

Esta tcnica reduz o custo de anlise do HTML porque o contedo do modelo analisado apenas uma vez pelo DOMParser, enquanto que chamar innerHTML dentro do construtor ir analisar o HTML para cada instncia. Isso garante uma melhoria na performance de nosso componente.

Atributos

E se quisermos que cada componente que ser renderizado na pgina tenha um contedo diferente? Podemos, como qualquer tag HTML, definir atributos. Por exemplo:

<!-- 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>

O atributo name definido por ns e pode ter o nome que considerarmos conveniente. Neste momento, nosso template est com um contedo fixo e devemos modificar de acordo com atributo name recebido pela nossa tag.

// arquivo UserCard.jsconst 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);

Como nosso componente um HTMLElement, podemos usar e abusar de todos os recursos que uma tag comum do HTML possui, como o mtodo getAttribute() para pegar o valor do atributo name que definimos anteriormente. E teremos o resultado:

Resultado da pgina renderizada

Shadow DOM

Agora que aprendemos um pouco sobre templates, vamos adicionar um estilo para nosso componente. Primeiro, vamos adicionar um estilo para a tag 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>

E vamos obter o seguinte resultado:

Resultado da pgina renderizada

Como todos os elementos da pgina, inclusive de nosso componente, esto dentro de uma tag h2, todos recebero o estilo global. Mas podemos adicionar um estilo especfico para nosso componente, modificando a cor para azul, por exemplo. Podemos adicionar a tag <style> em nosso template:

// arquivo UserCard.jsconst 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);

Agora temos dois estilos na pgina para tag 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 pgina, obtemos:

Resultado da pgina renderizada

Repare que o estilo de nosso componente foi aplicado tambm para o contedo da tag 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.

Voc pode argumentar que podemos evitar isso utilizando classes CSS e voc est totalmente correto! Mas imagine o cenrio de um projeto grande em que cada desenvolvedor responsvel por um componente especfico. H grandes chances de nomes iguais de classes CSS serem utilizadas, e isso pode gerar um grande transtorno.

Para evitar este tipo de conflito que trabalharemos com outro recurso chamado de Shadow DOM. A ideia encapsular cdigo HTML, CSS e JavaScript de nosso componente para no provocar e/ou sofrer alterao externa.

O Shadow DOM uma sub rvore do DOM que tem seu prprio escopo e que no faz parte do DOM original, tornando possvel construir interfaces modulares sem que entrem em conflito uma com a outra.

Shadow DOM
fonte da imagem: MDN Web Docs

Como especificado no MDN Web Docs, existem algumas terminologias do Shadow DOM que devemos conhecer:

  • Shadow host: o n DOM regular ao qual o Shadow DOM est anexado.
  • Shadow tree: a rvore DOM dentro do Shadow DOM.
  • Shadow boundary: o lugar onde termina o Shadow DOM e comea o DOM regular.
  • Shadow root: o n raiz da Shadow tree.

Dito isto, vamos ver como funciona na prtica. Iremos isolar nosso componente dentro de um 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 mtodo attachShadow() que podemos utilizar para abrir e criar uma referncia para um Shadow Root.

Um Shadow Root possui dois modos: aberto e fechado. Antes de entrarmos nas diferenas entre esses dois modos, iremos criar nosso Shadow Root no modo aberto para ver como ele funciona. O mtodo attachShadow() exige que passemos o modo como parmetro:

// arquivo UserCard.jsconst 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);

Aps esta mudana, ao renderizar novamente a pgina, vemos que nosso componente no renderizado e volta a receber o estilo global definido para a tag h2:

Resultado da pgina renderizada

Mas possvel verificar que o Shadow Root foi criado inspecionando a pgina atravs da ferramenta DevTools do navegador pela aba Elemets:

Shadow Root

Note que o contedo do template tambm foi anexado a tag <user-card> mas no mostrado j que est fora do Shadow Root. Uma vez aberto o Shadow Root, devemos anexar os contedos, como nosso template, dentro dele. Aps a chamada do mtodo attachShadow(), uma referncia ao objeto Shadow Root aberto disponibilizado atravs do atributo shadowRoot:

// arquivo UserCard.jsconst 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));  // cdigo modificado        this._name = this.getAttribute("name");        this.shadowRoot.querySelector("h2").textContent = this._name; // cdigo modificado    }}customElements.define("user-card", UserCard);

Agora, nosso componente renderizado como antes porque foi anexado ao Shadow Root, vamos novamente inspecion-lo pela ferramenta DevTools:

Shadow Root

Note que agora o contedo est dentro do Shadow Root. E como ele est dentro de uma Shadow Tree separada do DOM original, os estilos globais no afetam nosso componente e o resultado da renderizao da pgina este:

Resultado da pgina renderizada

Este foi um exemplo utilizado para encapsular os estilos. Mas o mesmo vale para eventos que possam ser registrados em nosso componente - como um evento de click que pode afetar muitos elementos de uma pgina e o Shadow DOM vai garantir o encapsulamento.

Agora que vimos um pouco como o Shadow DOM funciona, vamos entender a diferena entre os modos aberto e fechado. O Shadow Root no modo aberto permite que faamos modificaes em sua estrutura utilizando JavaScript. Se quisermos acessar o Shadow Root de nosso componente, basta digitar no console:

document.querySelector("user-card").shadowRoot

Isso nos permite acessar o shadowRoot de nosso componente:

Shadow Root

E fazer modificaes em seu contedo, como modificar o contedo da tag h2 de nosso componente:

Shadow Root

Repare que o encapsulamento, neste sentido, quebrado j que podemos modificar sua estrutura atravs do JavaScript. Para que o encapsulamento seja realmente aplicado que existe o modo fechado do Shadow DOM. Vamos modificar nosso componente para o modo fechado:

// arquivo UserCard.jsconst 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);

Mas, ao fazer isso, nosso componente nem sequer renderizado:

Resultado da pgina renderizada

Isso acontece porque o acesso ao atributo shadowRoot no mais possvel. this.shadowRoot agora vai retornar null e receberemos o seguinte erro no console:

Console JavaScript

Portanto, no ser mais possvel acessar o shadowRoot externamente pelo JavaScript:

Shadow Root

S ser possvel faz-lo dentro de nosso componente. Para isso, criaremos uma referncia para ele e assim ser possvel manipul-lo e clonar o template para que seja renderizado na pgina:

// arquivo UserCard.jsconst 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);

Dessa maneira, nosso componente renderizado como antes:

Resultado da pgina renderizada

E o acesso, via JavaScript, ao shadowRoot continua retornando null:

Console JavaScript

Agora temos nosso componente encapsulado e fechado para modificaes externas com JavaScript. evidente que ainda podemos acess-lo da seguinte forma:

Console JavaScript

Mas, seguindo as boas prticas da linguagem, isso deve ser evitado j que indica que este atributo privado e no deve ser acessado fora da classe UserCard.

Isolando o CSS

Escrever cdigo CSS dentro de um template string no o ideal. O melhor seria se o cdigo CSS de nosso componente ficasse em um arquivo externo de estilos.

Primeiro, vamos criar o arquivo UserCard.css.

/* arquivo UserCard.css */h2 {    color: blue;}

Em seguida, modificamos nosso componente para utilizar este arquivo CSS - importando o arquivo atravs da tag <link>:

// arquivo UserCard.jsconst template = document.createElement('template');template.innerHTML = `     <link type="text/css" rel="stylesheet" href="UserCard.css"></link>    <h2></h2>`;class UserCard extends HTMLElement {    // cdigo omitido}customElements.define("user-card", UserCard);

Tambm possvel utilizar o recurso de regra atribuda do CSS atravs do @import:

// arquivo UserCard.jsconst template = document.createElement('template');template.innerHTML = `     <style>@import url("UserCard.css")</style>    <h2></h2>`;class UserCard extends HTMLElement {    // cdigo omitido}customElements.define("user-card", UserCard);

Mas como comentado no MDN Web Docs, o carregamento de um estilo externo feito dessa maneira dentro do ShadowRoot pode causar o temido FOUC (Flash of Unstyled Content) - ou seja, pode ocorrer um flash do contedo no estilizado enquanto o CSS carregado.

Por este motivo, muitos desenvolvedores mantm o contedo dos estilos dentro da tag <style> no template string ao invs de tentar evitar FOUC com cdigo adicional - at o momento no existe uma maneira fcil e rpida de evitar isso.

Por facilidade e para evitar este tipo de problema, optaremos por manter o cdigo de estilos dentro do template string, utilizando a tag <style>.

Finalizando o componente do carto

Agora que entendemos um pouco sobre componentes, podemos voltar ao nosso objetivo final que era criar o componente de carto de usurios. Basta refatorarmos o cdigo modificando o template de nosso componente e fazendo os ajustes em seu construtor. O cdigo final ficaria assim:

<!-- 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.jsconst 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;    }}

E temos como resultado o componente do carto do usurio, podendo ser reutilizado em qualquer outra pgina HTML de nosso projeto:

Resultado da pgina renderizada

Concluso

Web Components (componentes web) possui uma especificao prpria. Como descrito no MDN Web Docs, Web Components uma sute de diferentes tecnologias que permite a criao de elementos customizados reutilizveis com a funcionalidade separada do resto do seu cdigo e que podem ser utilizados em suas aplicaes web.

Para utilizar Web Components no necessrio nenhuma biblioteca adicional ou framework, desde que o navegador implemente as seguintes especificaes da Web Api:

  • Custom Elements - permite definir tags customizadas
  • Templates - permite definir blocos de cdigos reutilizveis
  • Shadow DOM - permite encapsular o cdigo do componente em uma rvore separada do DOM

Segundo a documentao, atualmente Web Componentes suportado por padro no Firefox (verso 63), Chrome, Opera e Edge (verso 79). O Safari j suporta boa parte delas mas no todas. De todo modo, possvel usar Web Components em qualquer navegador atravs do Polyfill - que nada mais do que um pedao de cdigo (geralmente JavaScript) utilizado para simular os recursos ausentes do navegador o mais prximo possvel.

Web Components ainda um conceito novo quando utilizado em JavaScript nativo. Componentes so muito utilizados por bibliotecas e frameworks como Angular, React e Vue - ferramentas slidas e muito famosas dentro da comunidade front-end. E Web Components, por ser nativo, pode ser utilizado juntamente com essas ferramentas.

Se considerarmos uma equipe grande, separada em vrios times, em que cada time utiliza uma ferramenta diferente para cada parte de um projeto, pode acontecer de existir partes em comum entre elas como uma tela de login - com a mesma estrutura para dar unidade ao projeto. Com Web Components, possvel criar um componente nativo que seja compartilhado entre os times. Ou seja, facilita a interoperabilidade do sistema.

Um artigo interessante que compara Web Components com demais ferramentas, levando em considerao estilos de cdigo, performance e bundle-size, o All the Ways to Make a Web Component do pessoal do WebComponents.dev. Vale dar uma conferida!

No mais, a ideia deste post era apresentar conceitos bsicos sobre Web Components e como construir um simples componente com pouco cdigo. Web Components vai muito alm. Em futuros posts desta srie 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 prxima!


Original Link: https://dev.to/thaisandre/web-components-uma-introducao-2c40

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To