docs(core): describe interactions between view-encapsulated components (#42397)

This commit adds information to the view encapsulation guide that
describes the styling interactions between components that use differing
view encapsulation modes.

Closes #40715

PR Close #42397
This commit is contained in:
Pete Bacon Darwin 2021-05-27 21:27:38 +01:00 committed by Jessica Janiuk
parent d10c38a8f8
commit 645cad5614
16 changed files with 324 additions and 33 deletions

View File

@ -409,7 +409,10 @@ groups:
'aio/content/guide/template-statements.md',
'aio/content/guide/user-input.md',
'aio/content/examples/user-input/**',
'aio/content/images/guide/user-input/**'
'aio/content/images/guide/user-input/**',
'aio/content/guide/view-encapsulation.md',
'aio/content/examples/view-encapsulation/**',
'aio/content/images/guide/view-encapsulation/**'
])
reviewers:
users:

View File

@ -1,6 +1,6 @@
/* tslint:disable:no-unused-variable */
// #docplaster
import { Component, ViewEncapsulation } from '@angular/core';
import { Component } from '@angular/core';
// #docregion
@Component({
@ -12,7 +12,7 @@ export class QuestSummaryComponent { }
// #enddocregion
/*
// #docregion encapsulation.shadow
// warning: few browsers support shadow DOM encapsulation at this time
// warning: not all browsers support shadow DOM encapsulation at this time
encapsulation: ViewEncapsulation.ShadowDom
// #enddocregion encapsulation.shadow
*/

View File

@ -0,0 +1,97 @@
import { browser, by, element, logging, WebElement } from 'protractor';
/* tslint:disable:max-line-length */
describe('View Encapsulation App', () => {
const RED = 'rgba(255, 0, 0, 1)';
const GREEN = 'rgba(0, 128, 0, 1)';
const BLUE = 'rgba(0, 0, 255, 1)';
beforeAll(() => browser.get(''));
it('should color the `NoEncapsulationComponent` heading red, when it is at the top level', async () => {
const noEncapsulationHeading = element(by.css('app-root > app-no-encapsulation > h2'));
expect(await noEncapsulationHeading.getCssValue('color')).toEqual(RED);
});
it('should color the `NoEncapsulationComponent` message red, when it is at the top level', async () => {
const noEncapsulationMessage = element(by.css('app-root > app-no-encapsulation > .none-message'));
expect(await noEncapsulationMessage.getCssValue('color')).toEqual(RED);
});
it('should color the `EmulatedEncapsulationComponent` heading green, when it is at the top level', async () => {
const noEncapsulationHeading = element(by.css('app-root > app-emulated-encapsulation > h2'));
expect(await noEncapsulationHeading.getCssValue('color')).toEqual(GREEN);
});
it('should color the `EmulatedEncapsulationComponent` message green, when it is at the top level', async () => {
const noEncapsulationMessage = element(by.css('app-root > app-emulated-encapsulation > .emulated-message'));
expect(await noEncapsulationMessage.getCssValue('color')).toEqual(GREEN);
});
it('should color the `NoEncapsulationComponent` heading red, when it is a child of `EmulatedEncapsulationComponent`)', async () => {
const noEncapsulationHeading = element(by.css('app-root > app-emulated-encapsulation > app-no-encapsulation > h2'));
expect(await noEncapsulationHeading.getCssValue('color')).toEqual(RED);
});
it('should color the `NoEncapsulationComponent` message red, when it is a child of `EmulatedEncapsulationComponent`)', async () => {
const noEncapsulationMessage = element(by.css('app-root > app-emulated-encapsulation > app-no-encapsulation > .none-message'));
expect(await noEncapsulationMessage.getCssValue('color')).toEqual(RED);
});
it('should color the `ShadowDomEncapsulationComponent` heading blue', async () => {
const noEncapsulationHeading = await findShadowDomElement('app-root > app-shadow-dom-encapsulation', 'h2');
expect(await noEncapsulationHeading.getCssValue('color')).toEqual(BLUE);
});
it('should color the `ShadowDomEncapsulationComponent` message blue', async () => {
const noEncapsulationHMessage = await findShadowDomElement('app-root > app-shadow-dom-encapsulation', '.shadow-message');
expect(await noEncapsulationHMessage.getCssValue('color')).toEqual(BLUE);
});
it('should color the `EmulatedEncapsulationComponent` heading green, when it is a child of `ShadowDomEncapsulationComponent`', async () => {
const noEncapsulationHeading = await findShadowDomElement('app-root > app-shadow-dom-encapsulation', 'app-emulated-encapsulation > h2');
expect(await noEncapsulationHeading.getCssValue('color')).toEqual(GREEN);
});
it('should color the `EmulatedEncapsulationComponent` message green, when it is a child of `ShadowDomEncapsulationComponent`', async () => {
const noEncapsulationMessage = await findShadowDomElement('app-root > app-shadow-dom-encapsulation', 'app-emulated-encapsulation > .emulated-message');
expect(await noEncapsulationMessage.getCssValue('color')).toEqual(GREEN);
});
it('should color the `NoEncapsulationComponent` heading blue (not red!), when it is a child of the `ShadowDomEncapsulationComponent`', async () => {
const noEncapsulationHeading = await findShadowDomElement('app-root > app-shadow-dom-encapsulation', 'app-no-encapsulation > h2');
expect(await noEncapsulationHeading.getCssValue('color')).toEqual(BLUE);
});
it('should color the `NoEncapsulationComponent` message red (not blue!), when it is a child of the `ShadowDomEncapsulationComponent`', async () => {
const noEncapsulationMessage = await findShadowDomElement('app-root > app-shadow-dom-encapsulation', 'app-no-encapsulation > .none-message');
expect(await noEncapsulationMessage.getCssValue('color')).toEqual(RED);
});
it('should color the `NoEncapsulationComponent` heading blue (not red!), when it is a child of the `EmulatedEncapsulationComponent`, which is a child of the `ShadowDomEncapsulationComponent`', async () => {
const noEncapsulationHeading = await findShadowDomElement('app-root > app-shadow-dom-encapsulation', 'app-emulated-encapsulation > app-no-encapsulation > h2');
expect(await noEncapsulationHeading.getCssValue('color')).toEqual(BLUE);
});
it('should color the `NoEncapsulationComponent` message red (not blue!), when it is a child of the `EmulatedEncapsulationComponent`, which is a child of the `ShadowDomEncapsulationComponent`', async () => {
const noEncapsulationMessage = await findShadowDomElement('app-root > app-shadow-dom-encapsulation', 'app-emulated-encapsulation > app-no-encapsulation > .none-message');
expect(await noEncapsulationMessage.getCssValue('color')).toEqual(RED);
});
afterEach(async () => {
// Assert that there are no errors emitted from the browser
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
expect(logs).not.toContain(jasmine.objectContaining({
level: logging.Level.SEVERE,
} as logging.Entry));
});
});
async function findShadowDomElement(shadowHostSelector: string, shadowElementSelector: string): Promise<WebElement> {
const shadowHost = browser.findElement(by.css(shadowHostSelector));
const shadowRoot: any = await browser.executeScript('return arguments[0].shadowRoot', shadowHost);
return shadowRoot.findElement(by.css(shadowElementSelector));
}

View File

@ -0,0 +1,18 @@
import { Component, ViewEncapsulation } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<app-no-encapsulation></app-no-encapsulation>
<app-emulated-encapsulation></app-emulated-encapsulation>
<app-shadow-dom-encapsulation></app-shadow-dom-encapsulation>
`,
styles: [
'app-no-encapsulation, app-emulated-encapsulation, app-shadow-dom-encapsulation { display: block; max-width: 500px; padding: 5px; margin: 5px 0; }',
'app-no-encapsulation { border: solid 2px red; }',
'app-emulated-encapsulation { border: solid 2px green; }',
'app-shadow-dom-encapsulation { border: solid 2px blue; }',
],
encapsulation: ViewEncapsulation.None,
})
export class AppComponent { }

View File

@ -0,0 +1,22 @@
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { NoEncapsulationComponent } from './no-encapsulation.component';
import { ShadowDomEncapsulationComponent } from './shadow-dom-encapsulation.component';
import { EmulatedEncapsulationComponent } from './emulated-encapsulation.component';
@NgModule({
declarations: [
AppComponent,
NoEncapsulationComponent,
ShadowDomEncapsulationComponent,
EmulatedEncapsulationComponent
],
imports: [
BrowserModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

View File

@ -0,0 +1,14 @@
import { Component, ViewEncapsulation } from '@angular/core';
// #docregion
@Component({
selector: 'app-emulated-encapsulation',
template: `
<h2>Emulated</h2>
<div class="emulated-message">Emulated encapsulation</div>
<app-no-encapsulation></app-no-encapsulation>
`,
styles: ['h2, .emulated-message { color: green; }'],
encapsulation: ViewEncapsulation.Emulated,
})
export class EmulatedEncapsulationComponent { }

View File

@ -0,0 +1,13 @@
import { Component, ViewEncapsulation } from '@angular/core';
// #docregion
@Component({
selector: 'app-no-encapsulation',
template: `
<h2>None</h2>
<div class="none-message">No encapsulation</div>
`,
styles: ['h2, .none-message { color: red; }'],
encapsulation: ViewEncapsulation.None,
})
export class NoEncapsulationComponent { }

View File

@ -0,0 +1,15 @@
import { Component, ViewEncapsulation } from '@angular/core';
// #docregion
@Component({
selector: 'app-shadow-dom-encapsulation',
template: `
<h2>ShadowDom</h2>
<div class="shadow-message">Shadow DOM encapsulation</div>
<app-emulated-encapsulation></app-emulated-encapsulation>
<app-no-encapsulation></app-no-encapsulation>
`,
styles: ['h2, .shadow-message { color: blue; }'],
encapsulation: ViewEncapsulation.ShadowDom,
})
export class ShadowDomEncapsulationComponent { }

View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Ponyracer</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<app-root></app-root>
</body>
</html>

View File

@ -0,0 +1,12 @@
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));

View File

@ -0,0 +1,16 @@
{
"description": "View Encapsulation",
"files": [
"!**/*.d.ts",
"!**/*.js",
"!**/*.[1,2].*"
],
"tags": [
[
"view encapsulation",
"shadow DOM",
"CSS",
"component styling"
]
]
}

View File

@ -3,34 +3,31 @@
In Angular, component CSS styles are encapsulated into the component's view and don't
affect the rest of the application.
To control how this encapsulation happens on a *per
component* basis, you can set the *view encapsulation mode* in the component metadata.
To control how this encapsulation happens on a _per
component_ basis, you can set the _view encapsulation mode_ in the component metadata.
Choose from the following modes:
* `ShadowDom` view encapsulation uses the browser's native shadow DOM implementation (see
[Shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Shadow_DOM)
on the [MDN](https://developer.mozilla.org) site)
- `ShadowDom` view encapsulation uses the browser's native shadow DOM implementation (see
[Shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Shadow_DOM))
to attach a shadow DOM to the component's host element, and then puts the component
view inside that shadow DOM. The component's styles are included within the shadow DOM.
* `Emulated` view encapsulation (the default) emulates the behavior of shadow DOM by preprocessing
- `Emulated` view encapsulation (the default) emulates the behavior of shadow DOM by preprocessing
(and renaming) the CSS code to effectively scope the CSS to the component's view.
For details, see [Inspecting generated CSS](guide/view-encapsulation#inspect-generated-css) below.
* `None` means that Angular does no view encapsulation.
- `None` means that Angular does no view encapsulation.
Angular adds the CSS to the global styles.
The scoping rules, isolations, and protections discussed earlier don't apply.
This is essentially the same as pasting the component's styles into the HTML.
This mode is essentially the same as pasting the component's styles into the HTML.
To set the component's encapsulation mode, use the `encapsulation` property in the component metadata:
<code-example path="component-styles/src/app/quest-summary.component.ts" region="encapsulation.shadow" header="src/app/quest-summary.component.ts"></code-example>
`ShadowDom` view encapsulation only works on browsers that have native support
for shadow DOM (see [Shadow DOM v1](https://caniuse.com/shadowdomv1) on the
[Can I use](https://caniuse.com/) site). The support is still limited,
which is why `Emulated` view encapsulation is the default mode and recommended
in most cases.
for shadow DOM (see [Can I use - Shadow DOM v1](https://caniuse.com/shadowdomv1)).
The support is still limited, which is why `Emulated` view encapsulation is the default mode and recommended in most cases.
{@a inspect-generated-css}
@ -50,14 +47,13 @@ attached to it:
&lt;h3 _ngcontent-pmm-6>Team&lt;/h3>
&lt;/hero-team>
&lt;/hero-detail>
</code-example>
There are two kinds of generated attributes:
* An element that would be a shadow DOM host in native encapsulation has a
- An element that would be a shadow DOM host in native encapsulation has a
generated `_nghost` attribute. This is typically the case for component host elements.
* An element within a component's view has a `_ngcontent` attribute
- An element within a component's view has a `_ngcontent` attribute
that identifies to which host's emulated shadow DOM this element belongs.
The exact values of these attributes aren't important. They are automatically
@ -79,3 +75,75 @@ by the generated component styles, which are in the `<head>` section of the DOM:
These styles are post-processed so that each selector is augmented
with `_nghost` or `_ngcontent` attribute selectors.
These extra selectors enable the scoping rules described in this page.
## Mixing encapsulation modes
Avoid mixing components that use different view encapsulation. Where it is necessary, you should be aware of how the component styles will interact.
- The styles of components with `ViewEncapsulation.Emulated` are added to the `<head>` of the document, making them available throughout the application, but are "scoped" so they only affect elements within the component's template.
- The styles of components with `ViewEncapsulation.None` are added to the `<head>` of the document, making them available throughout the application, and are not "scoped" so they can affect any element in the application.
- The styles of components with `ViewEncapsulation.ShadowDom` are only added to the shadow DOM host, ensuring that they only affect elements within the component's template.
**All the styles for `ViewEncapsulation.Emulated` and `ViewEncapsulation.None` components are also added to the shadow DOM host of each `ViewEncapsulation.ShadowDom` component.**
The result is that styling for components with `ViewEncapsulation.None` will affect matching elements within the shadow DOM.
This approach may seem counter-intuitive at first, but without it a component with `ViewEncapsulation.None` could not be used within a component with `ViewEncapsulation.ShadowDom`, since its styles would not be available.
### Examples
This section shows examples of how the styling of components with different `ViewEncapsulation` interact.
See the <live-example noDownload></live-example> to try out these components yourself.
#### No encapsulation
The first example shows a component that has `ViewEncapsulation.None`. This component colors its template elements red.
<code-example path="view-encapsulation/src/app/no-encapsulation.component.ts" header="src/app/no-encapsulation.component.ts"></code-example>>
Angular adds the styles for this component as global styles to the `<head>` of the document.
**Angular also adds the styles to all shadow DOM hosts.** Therefore, the styles are available throughout the application.
<img src="generated/images/guide/view-encapsulation/no-encapsulation.png" alt="component with no encapsulation">
#### Emulated encapsulation
The second example shows a component that has `ViewEncapsulation.Emulated`. This component colors its template elements green.
<code-example path="view-encapsulation/src/app/emulated-encapsulation.component.ts" header="src/app/emulated-encapsulation.component.ts"></code-example>>
Similar to `ViewEncapsulation.None`, Angular adds the styles for this component to the `<head>` of the document, and to all the shadow DOM hosts.
But in this case, the styles are "scoped" by the attributes described in ["Inspecting generated CSS"](#inspecting-generated-css).
Therefore, only the elements directly within this component's template will match its styles.
Since the "scoped" styles from the `EmulatedEncapsulationComponent` are very specific, they override the global styles from the `NoEncapsulationComponent`.
In this example, the `EmulatedEncapsulationComponent` contains a `NoEncapsulationComponent`.
The `NoEncapsulationComponent` is styled as expected because the scoped styles do not match elements in its template.
<img src="generated/images/guide/view-encapsulation/emulated-encapsulation.png" alt="component with no encapsulation">
#### Shadow DOM encapsulation
The third example shows a component that has `ViewEncapsulation.ShadowDom`. This component colors its template elements blue.
<code-example path="view-encapsulation/src/app/shadow-dom-encapsulation.component.ts" header="src/app/shadow-dom-encapsulation.component.ts"></code-example>>
Angular adds styles for this component only to the shadow DOM host, so they are not visible outside the shadow DOM.
Note that Angular also adds the global styles from the `NoEncapsulationComponent` and `ViewEncapsulationComponent` to the shadow DOM host, so those styles are still available to the elements in the template of this component.
In this example, the `ShadowDomEncapsulationComponent` contains both a `NoEncapsulationComponent` and `ViewEncapsulationComponent`.
The styles added by the `ShadowDomEncapsulationComponent` component are available throughout the shadow DOM of this component, and so to both the `NoEncapsulationComponent` and `ViewEncapsulationComponent`.
The `EmulatedEncapsulationComponent` has specific "scoped" styles, so the styling of this component's template is unaffected.
But since styles from `ShadowDomEncapsulationComponent` are added to the shadow host after the global styles, the `h2` style overrides the style from the `NoEncapsulationComponent`.
The result is that the `<h2>` element in the `NoEncapsulationComponent` is colored blue rather than red, which may not be what the component author intended.
<img src="generated/images/guide/view-encapsulation/shadow-dom-encapsulation.png" alt="component with no encapsulation">

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB