Front End

9 out, 2018

Criando componente com Shadow DOM

Publicidade

Sim, eu sei, Shadow DOM está um pouco ultrapassado. Talvez seu uso seja pequeno, porém, neste artigo eu vou explicar como ele funciona e vamos criar um componente (também chamado de web component).

Em outras palavras, seria uma tag customizada que irá renderizar um código HTML conhecido pelo browser.

Afinal de contas, o que é esse tal de Shadow DOM? Para quê ele serve? Quando devíamos usá-lo? Calma, essa e outras perguntas eu tentarei responder ao decorrer deste artigo.

Se você está se perguntando: “Shadow DOM está ultrapassado, poucos devem utilizá-lo, então por que falar dele?”. Este artigo é apenas uma base para que você entenda a diferença entre Shadow DOM e Virtual DOM (assunto do próximo artigo – sim, um pequeno spoiler).

Se você não tem interesse em entender como ele funciona ou para o que serve, fique a vontade para não lê-lo e aguarde o próximo sobre Virtual DOM.

Conheça o Shadow DOM

O Shadow DOM trata-se de uma API dos browsers (navegadores). Com ela é possível anexar/adicionar um DOM separado para um elemento existente na página de forma escondida. Calma, logo as coisas farão sentido.

Pense o seguinte: hoje trabalhamos muito com componentes. Bibliotecas como o React, Angular e Vue fazem uso excessivo deles (cada uma do seu jeito, mas no final praticamente tudo é um componente). Na web também temos os famosos web components, mas, o que seriam esses “componentes”?

Para um melhor entendimento e simplicidade do artigo, entenda, de forma resumida, que componentes são tag’s HTML ou elementos que não existem. Ou seja, o navegador nativamente não os conhecem, mas, de alguma forma ele irá renderizar e criar um HTML conhecido, com as tag’s nativas.

Concluindo: através do Shadow DOM conseguimos criar tags customizadas e “transformá-las” em tags nativas.

Para quê usá-lo?

Apenas para solidificar, podemos usar o Shadow DOM para criar componentes, também chamados de web components. Além disso, também é possível aplicar estilos para elementos específicos na página, sem refletir nos demais.

Criando nosso componente

Finalmente hora de começar a criar nosso primeiro web component.

O primeiro passo será definir qual componente vamos criar. Para o exemplo do artigo, será um componente de mensagem. Agora com o componente definido, devemos criar uma classe para representá-lo:

class Mensagem { }

Mas onde essa classe será salva? Para escrever os códigos do nosso componente, vamos criar um arquivo JavaScript chamado Mensagem.js – todos os seus códigos devem estar dentro desse arquivo. Aproveite que o arquivo acabou de ser criado e faça a importação dele em seu HTML:

<script src="./assets/js/Mensagem.js"></script>

No meu caso, criei e salvei o arquivo em assets/js.

Continuando a implementação e criação do nosso componente, até o momento criamos uma simples classe, mas precisamos transformá-la em um elemento HTML (para que ele possa utilizar as funções do HTML). Como podemos fazer isso? Podemos utilizar a herança e herdar da classe HTMLElement:

class Mensagem extends HTMLElement { }

Através da palavra extends é possível realizar a herança em JavaScript.

Agora precisamos implementar como e o que será nosso componente. Nossos códigos ficarão dentro do construtor (constructor) (mais pra frente explicarei o porquê):

class Mensagem extends HTMLElement {

    constructor() {
        super()
        // nossa implementação deve vim aqui...
    }

}

Devemos chamar o construtor da clase mãe, ou seja, da classe HTMLElement. Isso é feito através do super().

Legal, o primeiro passo será, de fato, criar um Shadow DOM. Podemos fazer isso através da função attachShadow:

class Mensagem extends HTMLElement {

    constructor() {
        super()

        const shadow = this.attachShadow({ mode: 'closed' })
    }

}

Aqui estamos criando e adicionado um shadow root para nosso componente. A opção mode serve para definir se o nosso shadow root pode ser acessado fora do componente, ou seja, de forma global em nosso JavaScript, com o valor definido como closed isso não será possível, apenas nosso componente consegue interagir e acessar o shadow root.

Agora precisamos criar nossa tag conhecida pelo HTML para ser renderizada pelo nosso componente. Para isso podemos utilizar a velha conhecida createElement:

class Mensagem extends HTMLElement {

    constructor() {
        super()

        const shadow = this.attachShadow({ mode: 'closed' })

        const mensagem = document.createElement('p')
    }

}

Para o exemplo vamos renderizar um parágrafo (p). Além do componente, vamos adicionar algum estilo também:

class Mensagem extends HTMLElement {

    constructor() {
        super()

        const shadow = this.attachShadow({ mode: 'closed' })

        const mensagem = document.createElement('p')
        mensagem.classList.add('mensagem')

        const style  = document.createElement('style')
        style.textContent = `
            .mensagem {
                background: red;
                color: white;
                padding: 1rem;
            }
        `
    }

}

Pronto! Temos nosso elemento e estilo criado. Agora precisamos passá-los para nosso shadow root. Isso pode ser feito através da função appendChild:

class Mensagem extends HTMLElement {

    constructor() {
        super()

        const shadow = this.attachShadow({ mode: 'closed' })

        const mensagem = document.createElement('p')
        mensagem.classList.add('mensagem')

        const style  = document.createElement('style')
        style.textContent = `
            .mensagem {
                background: red;
                color: white;
                padding: 1rem;
            }
        `

        shadow.appendChild(style)
        shadow.appendChild(mensagem)
    }

}

Adicionamos como filhos do shadow root nosso estilo e mensagem. Por fim, precisamos registrar nosso componente e definir uma tag para ele. Isso pode ser feito através da função customElements:

class Mensagem extends HTMLElement {

    constructor() {
        super()

        const shadow = this.attachShadow({ mode: 'closed' })

        const mensagem = document.createElement('p')
        mensagem.classList.add('mensagem')

        const style  = document.createElement('style')
        style.textContent = `
            .mensagem {
                background: red;
                color: white;
                padding: 1rem;
            }
        `

        shadow.appendChild(style)
        shadow.appendChild(mensagem)
    }

}

customElements.define('app-mensagem', Mensagem)

Para definir nosso componente, devemos informar dois parâmetros:

  • Passar um nome para ele: esse nome será a tag dele
  • Passar a classe responsável por criá-lo

Observação: Lembra quando eu falei para adicionarmos nossos códigos dentro do construtor (constructor) da nossa classe? Isso foi necessário porque a função .define invoca o construtor da classe passada para ele.

Agora, dentro do nosso HTML já podemos tentar utilizá-lo:

<app-mensagem>Olá Shadow DOM</app-mensagem>

Mas algo deu errado. Veja como ele foi renderizado:

E nosso DOM:

Repare que ele está sem conteúdo, mas ele deveria mostrar nossa mensagem: “Olá Shadow DOM, o que está acontecendo?”.

Em nenhum momento definimos o conteúdo do nosso componente. Para isso, podemos utilizar a função .textContent:

class Mensagem extends HTMLElement {

    constructor() {
        super()

        const shadow = this.attachShadow({ mode: 'closed' })

        const mensagem = document.createElement('p')
        mensagem.classList.add('mensagem')
        mensagem.textContent = this.textContent // linha adicionada

        const style  = document.createElement('style')
        style.textContent = `
            .mensagem {
                background: red;
                color: white;
                padding: 1rem;
            }
        `

        shadow.appendChild(style)
        shadow.appendChild(mensagem)
    }

}

customElements.define('app-mensagem', Mensagem)

Dessa maneira estamos falando: “Pega o .textContent da tag customizada e passa para nosso componente que será renderizado pelo shadow root”. Agora, se voltar no navegador e recarregar:

E nosso DOM:

Agora tudo está funcionando!

Saiba mais

As primeiras versões do framework Angular utilizava o Shadow DOM, porém, por questões de performance foi migrado para o tão famoso Virtual DOM.

Lembra que eu falei que era possível definir um estilo apenas para nosso shadow DOM? Pois é! Para ver isso na prática adicionaremos outro elemento na página com a mesma classe da nossa mensagem:

<app-mensagem>Olá Shadow DOM</app-mensagem>
<p class="mensagem">Outra mensagem</p>

Sabemos que nosso componente app-mensagem irá renderizar um parágrafo com a classe mensagem.

Vamos ver como ficou o HTML renderizado:

Apenas nosso shadow root foi estilizado.

Conclusão

O Shadow DOM foi muito legal e bastante utilizado, apesar de muitos o terem substituído pelo Virtual DOM. Acho bem legal a forma como ele funciona – dá para fazer altas brincadeiras.

E aí, você já conhecia o Shadow DOM? Não deixe de comentar!

Caso queira, o projeto feito para o exemplo do artigo pode ser encontrado em meu GitHub.

Até a próxima!