View Encapsulation w Angularze - czyli o kapsułkowaniu słów kilka
Tworząc komponenty w Angularze mamy możliwość zarządzania kapsułkowaniem (enkapsulacją) stylów - czyli tym jak style z jednego komponentu wpływają na inne komponenty.
Zanim omówimy kapsułkowanie, wyjaśnijmy w kilku słowach czym jest Shadow DOM.
Shadow DOM
Shadow DOM wprowadza kapsułkowanie do DOM-u. Pozwala to odseparować styl i kod potrzebny do wyświetlenia elementu od dokumentu, w którym się znajduje. Przykładem może być np. element HTML <video>
<video width="320" height="240">
<source src="movie.mp4" type="video/mp4">
<source src="movie.ogg" type="video/ogg">
</video>
Po włączeniu opcji wyświetlania Shadow Root w przeglądarce (na przykładzie Google Chrome):
DevTools > Settings > Preferences > Elements
możemy zobaczyć z czego tak naprawdę składa się element <video>
:
<video width="320" height="240">
#shadow-root
<div pseudo="-webkit-media-controls" class="sizing-small phase-ready state-stopped">
<div pseudo="-internal-media-controls-loading-panel" aria-label="buforowanie" aria-live="polite"
style="display: none;"></div>
<div pseudo="-webkit-media-controls-overlay-enclosure"><input
pseudo="-internal-media-controls-overlay-cast-button" type="button"
aria-label="odtwarzanie na urządzeniu zdalnym" style="display: none;"></div>
<div pseudo="-webkit-media-controls-enclosure">
<div pseudo="-webkit-media-controls-panel">
<div pseudo="-internal-media-controls-scrubbing-message" style="display: none;"></div>
<div pseudo="-internal-media-controls-button-panel"><input type="button"
pseudo="-webkit-media-controls-play-button" aria-label="odtwórz" class="pause" style="">
<div aria-label="upłynęło: 0:00" pseudo="-webkit-media-controls-current-time-display" style="">0:00
</div>
<div aria-label="pozostało: / 0:12" pseudo="-webkit-media-controls-time-remaining-display" style="">
/
0:12</div>
<div pseudo="-internal-media-controls-button-spacer"></div>
<div pseudo="-webkit-media-controls-volume-control-container" class="closed" style="">
<div pseudo="-webkit-media-controls-volume-control-hover-background"></div><input type="range"
step="any" max="1" aria-valuemax="100" aria-valuemin="0" aria-label="volume"
pseudo="-webkit-media-controls-volume-slider" aria-valuenow="100" class="closed"
style=""><input type="button" pseudo="-webkit-media-controls-mute-button"
aria-label="wyciszenie" style="">
</div><input type="button" role="button" aria-label="włącz tryb obrazu w obrazie"
pseudo="-internal-media-controls-picture-in-picture-button" style="display: none;"><input
type="button" pseudo="-webkit-media-controls-fullscreen-button"
aria-label="przejdź do pełnego ekranu" style=""><input type="button"
aria-label="pokaż więcej opcji sterowania multimediami" title="więcej opcji"
pseudo="-internal-media-controls-overflow-button" style="">
</div><input type="range" step="any" pseudo="-webkit-media-controls-timeline" max="12.612"
aria-label="pasek czasu odtwarzania filmu 0:00 / 0:12" aria-valuetext="upłynęło: 0:00">
</div>
</div>
<div role="menu" aria-label="Opcje" pseudo="-internal-media-controls-text-track-list" style="display: none;">
</div>
<div pseudo="-internal-media-controls-overflow-menu-list" role="menu" class="closed" style="display: none;">
<label pseudo="-internal-media-controls-overflow-menu-list-item" role="menuitem" tabindex="0"
aria-label=" Odtwórz " style="display: none;"><input type="button"
pseudo="-webkit-media-controls-play-button" tabindex="-1" aria-label="odtwórz" class="pause"
style="display: none;">
<div aria-hidden="true"><span>Odtwórz</span></div>
</label><label pseudo="-internal-media-controls-overflow-menu-list-item" role="menuitem" tabindex="0"
aria-label="przejdź do pełnego ekranu Pełny ekran " style="display: none;"><input type="button"
pseudo="-webkit-media-controls-fullscreen-button" aria-label="przejdź do pełnego ekranu"
tabindex="-1" style="display: none;">
<div aria-hidden="true"><span>Pełny ekran</span></div>
</label><label pseudo="-internal-media-controls-overflow-menu-list-item" role="menuitem" tabindex="0"
aria-label="pobierz multimedia Pobierz " class="animated-1" style=""><input type="button"
aria-label="pobierz multimedia" pseudo="-internal-media-controls-download-button" tabindex="-1"
style="">
<div aria-hidden="true"><span>Pobierz</span></div>
</label><label pseudo="-internal-media-controls-overflow-menu-list-item" role="menuitem" tabindex="0"
aria-label=" Wycisz " class="animated-2" style="display: none;"><input type="button"
pseudo="-webkit-media-controls-mute-button" tabindex="-1" aria-label="wyciszenie"
style="display: none;">
<div aria-hidden="true"><span>Wycisz</span></div>
</label><label pseudo="-internal-media-controls-overflow-menu-list-item" role="menuitem" tabindex="0"
aria-label="odtwarzanie na urządzeniu zdalnym Przesyłaj " class="animated-1"
style="display: none;"><input pseudo="-internal-media-controls-cast-button" type="button"
aria-label="odtwarzanie na urządzeniu zdalnym" tabindex="-1" style="display: none;">
<div aria-hidden="true"><span>Przesyłaj</span></div>
</label><label pseudo="-internal-media-controls-overflow-menu-list-item" role="menuitem" tabindex="0"
aria-label="wyświetlanie menu napisów Napisy " class="animated-0" style="display: none;"><input
aria-label="wyświetlanie menu napisów" type="button"
pseudo="-webkit-media-controls-toggle-closed-captions-button" tabindex="-1" style="display: none;">
<div aria-hidden="true"><span>Napisy</span></div>
</label><label pseudo="-internal-media-controls-overflow-menu-list-item" role="menuitem" tabindex="0"
aria-label="włącz tryb obrazu w obrazie Obraz w obrazie " class="animated-0" style=""><input
type="button" role="button" aria-label="włącz tryb obrazu w obrazie"
pseudo="-internal-media-controls-picture-in-picture-button" tabindex="-1" style="">
<div aria-hidden="true"><span>Obraz w obrazie</span></div>
</label></div>
</div>
<source src="movie.mp4" type="video/mp4">
<source src="movie.ogg" type="video/ogg">
</video>
Shadow DOM ukrywa całą implementację pod prostym tagiem.
Dzięki temu style zaaplikowane do naszego elementu nie wpływają na inne elementy DOM-u.
Wsparcie Shadow DOM przez główne przeglądarki
źródło: www.webcomponents.org - dostęp: 2019-08-20
View Encapsulation w Angularze
Domyślnie Angular korzysta z własnego kapsułkowania stylów (ViewEncapsulation.Emulated
), ale udostępnia jeszcze 3 inne tryby kapsułkowania (w tym jeden deprecated).
Aby zmienić domyślny tryb kapsułkowania, wystarczy dodać odpowiednią opcję w dekoratorze @Component
, np.:
encapsulation: ViewEncapsulation.ShadowDom
Omówimy je na przykładzie kodu z projektu demo. Link do repozytorium
Projekt demo składa się z 4 komponentów:
Każdy z komponentów app-red
, app-green
oraz app-blue
składa się z jednego paragrafu z odpowiednim kolorem tekstu dla tego elementu. Oprócz tego istnieją 3 branche, po jednym dla każdego z omawianych trybów, co pozwoli na zobrazowanie nakładania się oraz kapsułkowania stylów.
ViewEncapsulation.None
Brak kapsułkowania, czyli style utworzone w komponencie są globalne (w sekcji <head>
).
W tym trybie elementy HTML i odpowiadające im selektory CSS wyglądają tak samo jak te, które napisaliśmy w kodzie.
Może to spowodować niechciane nadpisywanie stylów lub dodawanie ich do elementów, które nie posiadają żadnego stylu.
W przykładzie usunęliśmy styl paragrafu w komponencie app-green
.
Link do repozytorium
import {Component, ViewEncapsulation} from '@angular/core';
@Component({
selector: 'app-red',
template: `
<p>Red paragraph!</p>
`,
styles: [`
p {
color: red;
}
`],
encapsulation: ViewEncapsulation.None
})
export class RedComponent {
}
import {Component, ViewEncapsulation} from '@angular/core';
@Component({
selector: 'app-green',
template: `
<p>Green paragraph!</p>
`,
encapsulation: ViewEncapsulation.None
})
export class GreenComponent {
}
import {Component, ViewEncapsulation} from '@angular/core';
@Component({
selector: 'app-blue',
template: `
<p>Blue paragraph!</p>
`,
styles: [`
p {
color: blue;
}
`],
encapsulation: ViewEncapsulation.None
})
export class BlueComponent {
}
Wynikowy kod HTML:
<head>
<style>
p {
color: red;
}
</style>
<style>
p {
color: blue;
}
</style>
</head>
<body>
<app-root ng-version="8.2.2">
<app-red>
<p>Red paragraph!</p>
</app-red>
<app-green>
<p>Green paragraph!</p>
</app-green>
<app-blue>
<p>Blue paragraph!</p>
</app-blue>
</app-root>
</body>
Wynik widoczny w przeglądarce
Jak widzimy, style zostały dodane w sekcji <head>
, co spowodowało nadpisanie pierwszego stylu paragrafu drugim - color: blue
. W efekcie wszystkie paragrafy mają ten sam kolor, również paragraf z komponentu app-green
, który nie posiada żadnego stylu i powinien mieć kolor domyślny.
ViewEncapsulation.Emulated (default)
Domyślny tryb kapsułkowania w Angularze, w którym style są domknięte w komponencie.
W tym trybie style również znajdują się w sekcji <head>
, ale posiadają dodatkowe atrybuty które wiążą je z elementami HTML pochodzącymi z tego samego komponentu.
Dzięki temu na stronie może istnieć kilka komponentów zawierających element tego samego typu, ale z różnymi stylami.
Uwaga! W tym trybie style nie mają wpływu na inne elementy na stronie (jednak mogą mieć wpływ na elementy komponentu dziecka - jeśli komponent dziecka posiada tryb kapsułkowania inny niż Shadow DOM), ponieważ są domknięte unikalnymi atrybutami. Globalne style strony (oraz style innych komponentów, które mają wyłączony tryb kapsułkowania) mogą jednak mieć wpływ na ten komponent.
W przykładzie przenieśliśmy komponent app-green
z komponentu app-root
do komponentu app-blue
i usunęliśmy jego style.
Link do repozytorium
import {Component} from '@angular/core';
@Component({
selector: 'app-red',
template: `
<p>Red paragraph!</p>
`,
styles: [`
p {
color: red;
}
`]
})
export class RedComponent {
}
import {Component} from '@angular/core';
@Component({
selector: 'app-green',
template: `
<p>Green paragraph!</p>
`
})
export class GreenComponent {
}
import {Component} from '@angular/core';
@Component({
selector: 'app-blue',
template: `
<app-green></app-green>
<p>Blue paragraph!</p>
`,
styles: [`
p {
color: blue;
}
`]
})
export class BlueComponent {
}
Wynikowy kod HTML:
<head>
<style>
p[_ngcontent-pes-c0] {
color: red;
}
</style>
<style>
p[_ngcontent-pes-c1] {
color: blue;
}
</style>
</head>
<body>
<app-root ng-version="8.2.2">
<app-red _nghost-pes-c0>
<p _ngcontent-pes-c0>Red paragraph!</p>
</app-red>
<app-blue _nghost-pes-c1>
<app-green _ngcontent-pes-c1>
<p>Green paragraph!</p>
</app-green>
<p _ngcontent-pes-c1>Blue paragraph!</p>
</app-blue>
</app-root>
</body>
Wynik widoczny w przeglądarce
Domyślny tryb pozwolił nam odseparować style między poszczególnymi komponentami. W kodzie wynikowym widzimy, że style z komponentu rodzica app-blue
nie zostały zaaplikowane do komponentu dziecka app-green
, w efekcie czego paragraf ma kolor domyślny. Stało się tak, ponieważ Angular dodał atrybut do stylu. Gdybyśmy dodali styl w runtime, to zostałby zaaplikowany również do komponentu dziecka.
Na przykładzie komponentu app-red
- Angular dodał atrybut _ngcontent-pes-c0
do selektora CSS oraz elementu HTML. W ten sposób style dodane w sekcji <head>
aplikują się tylko do odpowiednich elementów z tego samego komponentu. Oprócz tego, na komponencie dodany został atrybut _nghost-pes-c0
. Z czego składają się te atrybuty?
_ngcontent
- określa typ elementu, w tym przypadku zawartość komponentu_nghost
- określa elementroot
komponentu-pes
- oznacza ID aplikacji (APP_ID), jeśli nie został ustawiony to zostanie przyjęty wygenerowany ciąg znaków - dzięki temu nie nakładają się style między różnymi aplikacjami wyświetlanymi w jednym oknie-c0
- numeruje kolejno elementy w komponencie
ViewEncapsulation.ShadowDom
Kapsułkowanie oparte na Shadow DOM (wymaga wsparcia przeglądarki dla Shadow DOM).
W tym trybie style nie są dodawane w sekcji <head>
, a istnieją w Shadow Root.
Uwaga! W tym trybie style nie mają wpływu na inne elementy na stronie (jednak mogą mieć wpływ na elementy komponentu dziecka - jeśli komponent dziecka posiada tryb kapsułkowania inny niż Shadow DOM). Globalne style strony (oraz style innych komponentów) również nie mają wpływu na ten komponent.
W przykładzie przenieśliśmy komponent app-green
z komponentu app-root
do komponentu app-blue
, usunęliśmy jego style i ustawiliśmy domyślny tryb kapsułkowania.
Link do repozytorium
import {Component, ViewEncapsulation} from '@angular/core';
@Component({
selector: 'app-red',
template: `
<p>Red paragraph!</p>
`,
styles: [`
p {
color: red;
}
`],
encapsulation: ViewEncapsulation.ShadowDom
})
export class RedComponent {
}
import {Component} from '@angular/core';
@Component({
selector: 'app-green',
template: `
<p>Green paragraph!</p>
`
})
export class GreenComponent {
}
import {Component, ViewEncapsulation} from '@angular/core';
@Component({
selector: 'app-blue',
template: `
<app-green></app-green>
<p>Blue paragraph!</p>
`,
styles: [`
p {
color: blue;
}
`],
encapsulation: ViewEncapsulation.ShadowDom
})
export class BlueComponent {
}
Wynikowy kod HTML:
<head>
</head>
<body>
<app-root ng-version="8.2.2">
<app-red>
#shadow-root
<style>
p {
color: red;
}
</style>
<p>Red paragraph!</p>
</app-red>
<app-blue>
#shadow-root
<style>
p {
color: blue;
}
</style>
<app-green>
<p>Green paragraph!</p>
</app-green>
<p>Blue paragraph!</p>
</app-blue>
</app-root>
</body>
Wynik widoczny w przeglądarce
Tryb Shadow DOM również pozwolił nam odseparować style między poszczególnymi komponentami. W sekcji <head>
nie ma już żadnych stylów, natomiast są ukryte w Shadow Root elementów DOM-u. Na przykładzie widzimy, że style z komponentu rodzica app-blue
zostały zaaplikowane do komponentu dziecka app-green
, w efekcie czego paragraf ma kolor niebieski. Gdyby komponent app-green
również posiadał tryb ViewEncapsulation.ShadowDom
, to style rodzica nie zostałyby zaaplikowane, ponieważ korzystałby ze stylów z własnego Shadow Root. Tryb Shadow DOM zabezpiecza nasz komponent również przed stylami z komponentu rodzica, dodanymi w runtime.
ViewEncapsulation.Native
Do niedawna zamiast ViewEncapsulation.ShadowDom
dostępny był tryb ViewEncapsulation.Native
.
Działał on w podobny sposób, ale został wycofany z powodu wykorzystywania przestarzałego standardu Shadow DOM.
Podsumowanie
Ogólnie rzecz biorąc, powinniśmy unikać braku kapsułkowania stylów, ponieważ powoduje to często niechciane efekty.
Tryb Shadow DOM zapewnia całkowite domknięcie stylów w komponencie, dzięki czemu style globalne oraz inne komponenty nie mają na niego wpływu, tak samo jak komponent w tym trybie nie ma wpływu na inne komponenty na stronie (za wyjątkiem komponentów dzieci które mają włączony tryb kapsułkowania inny niż Shadow DOM).
Niestety nie wszystkie przeglądarki mogą wspierać ten tryb, dlatego Angular domyślnie udostępnił własny, emulowany tryb kapsułkowania. W trybie domyślnym na nasz komponent mają jednak wpływ style globalne, a także mogą mieć wpływ inne komponenty, ponieważ komponent w tym trybie nadal wykorzystuje style z sekcji <head>
. W większości przypadków tryb domyślny jest wystarczający, więc jeśli zależy nam na jak najlepszym wsparciu przeglądarek i nie mamy problemów z nadpisywaniem stylów przez inne komponenty lub aplikacje, to możemy z powodzeniem z niego korzystać.
Musimy jednak pamiętać, że mieszanie różnych trybów kapsułkowania między komponentami również może spowodować niezamierzone efekty.
-
SENIOR FULLSTACK DEVELOPER (JAVA + ANGULAR) Poznań (hybrydowo) lub zdalnie UoP 14 900 - 20 590 PLN brutto
B2B 19 680 - 27 220 PLN netto -
REGULAR FULLSTACK DEVELOPER (JAVA + ANGULAR) Poznań (hybrydowo) lub zdalnie UoP 11 300 - 15 900 PLN brutto
B2B 14 950 - 21 000 PLN netto -
ZOBACZ WSZYSTKIE OGŁOSZENIA
newsletter
techniczny
Podobne wpisy
Czy wiesz, że w Angular 17 została wprowadzona alternatywa dla *ngIf?
-
SENIOR FULLSTACK DEVELOPER (JAVA + ANGULAR) Poznań (hybrydowo) lub zdalnie UoP 14 900 - 20 590 PLN brutto
B2B 19 680 - 27 220 PLN netto -
REGULAR FULLSTACK DEVELOPER (JAVA + ANGULAR) Poznań (hybrydowo) lub zdalnie UoP 11 300 - 15 900 PLN brutto
B2B 14 950 - 21 000 PLN netto -
ZOBACZ WSZYSTKIE OGŁOSZENIA