ViewEncapsulation - Angular 2+

Se você é do tipo de pessoa que fica "fuçando" no intellisense para descobrir coisas novas, provavelmente já tenha visto dentro do @Component({...}) a presença do encapsulation como opção. Já se perguntou para que serve?

De forma bem resumida, esta opção possibilita o encapsulamento (ou não)  do CSS dentro do componente para que seu estilo não impacte o visual de outros componentes. Agora vamos detalhar um pouco mais.

Introdução

É bem comum ao se criar um componente, por exemplo abc.component.ts, vincularmos um arquivo de estilo visual, por exemplo, abc.component.css. Conforme vamos criando componentes estes arquivos tendem a aumentar e consequentemente temos eventuais repetições nos seletores CSS, por exemplo:

p {
  background: red;
}
abc.component.css
p {
  background: blue;
}
xyz.component.css

Acima temos dois arquivos .css com dois seletores idênticos. Cada arquivo está vinculado à um componente.

Quando executamos o projeto, vemos que o componente AbcComponent terá seus elementos <p> com background: red e o componente XyzComponent com background: blue. Mas como o Angular consegue isolar/encapsular estes estilos visuais dentro dos componentes? É justamente aí que o encapsulation: ViewEncapsulation entra em cena para fazer as coisas funcionarem.

enum ViewEncapsulation {
  Emulated: 0
  None: 2
  ShadowDom: 3
}
Opções de encapsulamento - https://angular.io/api/core/ViewEncapsulation

ViewEncapsulation.Emulated (default)

Quando criamos um componente podemos vincular um ou mais arquivos .css (ou .sass, .scss , etc). O conteúdo deste(s) arquivo (estilos) resulta em um estilo visual para o componente. Este estilo fica "dentro" do componente e não "vaza" para outros componentes. Veja no exemplo abaixo:

import {
    Component,
    ViewEncapsulation
} from '@angular/core';

@Component({
    selector: 'app-botao',

    // **************************************
    // Default
    encapsulation: ViewEncapsulation.Emulated
    // **************************************

    template: `
        <button class="button">
    	    Dentro do botao.component
        </button>`,
    styleUrls: ['./botao.component.css'],
})
export class BotaoComponent {

}
botao.component.ts
.button {
  background: blue;
  color: #fff;
  padding: 8px 16px;
}
botao.component.css

O <button class="button"> dentro do botao.component.ts será renderizado na cor azul. Agora vamos adicionar um <button class="button"> dentro do template do app.component e aplicar um estilo visual na cor vermelha:

import { Component } from '@angular/core';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {

}
app.component.ts
.button {
  background: red;
  color: #fff;
  padding: 8px 16px;
}
app.component.css

Para testarmos:

<button class="button">Dentro do app.component</button>
<app-botao></app-botao>
app.component.html

O resultado final será:

Resultado da renderização com cada componente utilizando um arquivo css

Provavelmente a maioria sabe que isso funciona mas poucos sabem explicar o motivo.

Quando a aplicação acima é executada, o navegador carrega inicialmente o arquivo index.html e na sequência carrega os demais arquivos (.js, .css, .jpg, etc). Tudo isso pertence a mesma instância, ou em outras palavras, todo esse conteúdo (HTML + JavaScript + CSS + assets) está dentro da mesma caixa. Se todo este conteúdo está dentro da mesma caixa e temos dois seletores CSS iguais (.button { ... }) , como o navegador consegue distingui-los aplicando o estilo visual correto para cada <button>?


Lembre-se de que o navegador "junta" todo esse HTML + JS + CSS na mesma página, como o código abaixo ilustra:

<style>
  .btn {
    background: blue;
    color: #fff;
    padding: 8px 16px;
  }

  .btn {
    background: red;
    color: #fff;
    padding: 8px 16px;
  }
</style>
<button class="btn">Botão 1</button>
<button class="btn">Botão 2</button>
Renderização do código acima - dois botões com o background vermelho

A razão para o Angular conseguir isolar o conteúdo CSS de cada componente, é a inclusão dinâmica de um atributo nos seletores CSS e nos elementos dentro do template. A grosso modo o Angular cria um código para "ligar" todo o CSS do componente à todo HTML do mesmo. Em outras palavras, a ideia é especificar que determinada classe CSS deve ser aplicada apenas se o elemento possuir um determinado atributo, por exemplo:

.button[item1] {
  background: blue;
  color: #fff;
  padding: 8px 16px;
}

.button[item2=valor] {
  background: red;
  color: #fff;
  padding: 8px 16px;
}
<button class="button" item1>
  Sou azul
</button>

<button class="button" item2="valor">
  Sou vermelho
</button>

<button class="button" item2="valor2">
  Não tenho estilo
</button>
Renderização do código acima

É justamente este mecanismo que o Angular utiliza no encapsulation: ViewEncapsulation.Emulated (default). Se pegarmos o HTML renderizado no navegador as coisas ficam mais claras, abaixo está o código gerado no primeiro exemplo deste tópico:

<my-app _nghost-bsw-c61="" ng-version="12.1.0">
  <button _ngcontent-bsw-c61="" class="button">
    Dentro do app.component
  </button>
  <app-botao _ngcontent-bsw-c61="" _nghost-bsw-c60="">
    <button _ngcontent-bsw-c60="" class="button">
      Dentro do botao.component
    </button>
  </app-botao>
</my-app>
.button[_ngcontent-bsw-c60] {
    background: blue;
    color: #fff;
    padding: 8px 16px;
}

.button[_ngcontent-bsw-c61] {
  background: red;
  color: #fff;
  padding: 8px 16px;
}

Veja que o código _ngcontent-bsw-c60 corresponde a coisas do botao.template.html e o _ngcontent-bsw-c61 a coisas que são do app.template.html. É justamente desta forma que os seletores não se misturam e ficam encapsulados dentro do componente a qual pertence. Este é o comportamento padrão do ViewEncapsulation.Emulated.

ViewEncapsulation.ShadowDom

Nesta configuração, @Component({ encapsulation: ViewEncapsulation.ShadowDom }), o Angular utiliza um recurso chamado Shadow DOM. Atualmente poucos navegadores dão suporte a este recurso, então se for utilizar certifique-se de que seu público alvo utilize navegadores que dêem suporte necessário.

A ideia é similar ao Emulated, porém aqui é utilizado um recurso do próprio navegador para isolar os CSS do restante da sua página. Veja um exemplo bem simples:

<div id="container1"></div>
<div id="container2"></div>

<template id="template1">
  <style>
    .btn {
      background: blue;
      color: #fff;
      padding: 8px 16px;
    }
  </style>
  <button class="btn">Botão 1</button>
</template>

<template id="template2">
  <style>
    .btn {
      background: red;
      color: #fff;
      padding: 8px 16px;
    }
  </style>
  <button class="btn">Botão 2</button>
</template>

<script>
  const shadow1 = document
  	.querySelector('#container1')
    .attachShadow({mode: 'open'});

  const shadow2 = document
  	.querySelector('#container2')
    .attachShadow({mode: 'open'});

  const template1 = document
  	.querySelector('#template1');

  const template2 = document
  	.querySelector('#template2');

  const clone1 = document
  	.importNode(template1.content, true);
    
  const clone2 = document
  	.importNode(template2.content, true);

  shadow1.appendChild(clone1);
  shadow2.appendChild(clone2);
</script>

Repare no resultado abaixo que temos dois <style> isolados, cada um dentro de seu Shadow Content. O navegador isola esta área do restante da página, sendo assim os <style> e <script> que estão dentro dele não impactam no restante da página.

Renderização utilizando Shadow DOM

Pegando nosso exemplo em Angular e alterando o encapsulation para ViewEncapsulation.ShadowDom, temos o seguinte resultado na renderização:

HTML gerado pela aplicação com o ViewEncapsulation.ShadowDom
Utilizando o ViewEncapsulation.ShadowDom

Veja que aqueles atributos ( <button _ngcontent-bsw-c61=""...>) não são mais gerados e no lugar deles o Shadow DOM entra em cena para isolar o estilo visual.

ViewEncapsulation.None

Por fim temos o mais simples,  encapsulation: ViewEncapsulation.None. Nesta configuração o Angular não utiliza o Shadow DOM nem a inclusão dinâmica de atributos para isolar o estilo visual, ou seja, ele não faz nada!

Particularmente efetuo esta configuração quando o componente não precisa ter um estilo visual próprio, ou seja, um estilo que seja exclusivo dele e precise ser isolado do restante do código.

Veja no exemplo abaixo o resultado da renderização nesta configuração::

HTML gerado pela aplicação Angular

Repare que temos dois seletores CSS iguais (lado direito na imagem acima). O navegador considerou o de background: blue e descartou o de background: red. A decisão do navegador em escolher qual estilo ele irá apresentar é em decorrência da precedência de estilo, que é um papo para outro post, mas deixarei um link para leitura.

Então basicamente o Angular entregou para o navegador todo o CSS e deixou a cargo do mesmo decidir qual estilo será aplicado. Abaixo está o resultado:

Renderização do código da imagem acima

Considerações

É importante saber a diferença entre estas 3 opções abordadas. Conhecendo estas diferenças será mais fácil construir uma aplicação melhor para cada cenário.

Como sugestão, invista um pouco de tempo sobre Shadow DOM e também faça alguns testes em sua ambiente para fixar o conteúdo abordado neste post.

Links interessantes: