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:
parent
d10c38a8f8
commit
645cad5614
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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));
|
||||
}
|
|
@ -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 { }
|
|
@ -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 { }
|
|
@ -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 { }
|
|
@ -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 { }
|
|
@ -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 { }
|
|
@ -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>
|
|
@ -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));
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"description": "View Encapsulation",
|
||||
"files": [
|
||||
"!**/*.d.ts",
|
||||
"!**/*.js",
|
||||
"!**/*.[1,2].*"
|
||||
],
|
||||
"tags": [
|
||||
[
|
||||
"view encapsulation",
|
||||
"shadow DOM",
|
||||
"CSS",
|
||||
"component styling"
|
||||
]
|
||||
]
|
||||
}
|
|
@ -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}
|
||||
|
||||
|
@ -44,38 +41,109 @@ encapsulation enabled, each DOM element has some extra attributes
|
|||
attached to it:
|
||||
|
||||
<code-example format="">
|
||||
<hero-details _nghost-pmm-5>
|
||||
<h2 _ngcontent-pmm-5>Mister Fantastic</h2>
|
||||
<hero-team _ngcontent-pmm-5 _nghost-pmm-6>
|
||||
<h3 _ngcontent-pmm-6>Team</h3>
|
||||
</hero-team>
|
||||
</hero-detail>
|
||||
|
||||
<hero-details _nghost-pmm-5>
|
||||
<h2 _ngcontent-pmm-5>Mister Fantastic</h2>
|
||||
<hero-team _ngcontent-pmm-5 _nghost-pmm-6>
|
||||
<h3 _ngcontent-pmm-6>Team</h3>
|
||||
</hero-team>
|
||||
</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
|
||||
that identifies to which host's emulated shadow DOM this element belongs.
|
||||
- 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
|
||||
generated and you should never refer to them in application code. But they are targeted
|
||||
by the generated component styles, which are in the `<head>` section of the DOM:
|
||||
|
||||
<code-example format="">
|
||||
[_nghost-pmm-5] {
|
||||
display: block;
|
||||
border: 1px solid black;
|
||||
}
|
||||
[_nghost-pmm-5] {
|
||||
display: block;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
h3[_ngcontent-pmm-6] {
|
||||
background-color: white;
|
||||
border: 1px solid #777;
|
||||
}
|
||||
h3[_ngcontent-pmm-6] {
|
||||
background-color: white;
|
||||
border: 1px solid #777;
|
||||
}
|
||||
</code-example>
|
||||
|
||||
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 |
Loading…
Reference in New Issue