ViewEncapsulation - Angular 2+
Aprenda como o Angular encapsula o estilo visual (CSS) dos componentes e veja quais opções de configuração estão disponíveis
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:
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.
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:
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:
Para testarmos:
O resultado final será:
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>
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>
É 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.
Pegando nosso exemplo em Angular e alterando o encapsulation
para ViewEncapsulation.ShadowDom
, temos o seguinte resultado na renderização:
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::
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:
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:
- https://developer.mozilla.org/pt-BR/docs/Web/CSS/CSS_Selectors
- https://angular.io/guide/view-encapsulation
- https://angular.io/api/core/ViewEncapsulation
- https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM
- https://www.lambda3.com.br/2018/03/shadow-dom-voltando-ao-basico/
- https://medium.com/emanuelg-blog/entendendo-a-precedência-de-estilo-em-css-especificidade-herança-e-efeito-cascata-a437c4929173