docs(ivy): add docs for styling priority order (#35066)

PR Close #35066
This commit is contained in:
Kara Erickson 2020-01-30 14:09:10 -08:00 committed by Misko Hevery
parent 4e9d62ff81
commit 7526ef77b7
7 changed files with 247 additions and 95 deletions

View File

@ -5,7 +5,8 @@
"packageManager": "yarn", "packageManager": "yarn",
"warnings": { "warnings": {
"typescriptMismatch": false "typescriptMismatch": false
} },
"analytics": false
}, },
"newProjectRoot": "projects", "newProjectRoot": "projects",
"projects": { "projects": {
@ -192,4 +193,4 @@
} }
}, },
"defaultProject": "site" "defaultProject": "site"
} }

View File

@ -25,23 +25,12 @@ describe('Attribute binding example', function () {
}); });
it('should display a blue div with a red border', function () { it('should display a blue div with a red border', function () {
expect(element.all(by.css('div')).get(4).getCssValue('border')).toEqual('2px solid rgb(212, 30, 46)'); expect(element.all(by.css('div')).get(1).getCssValue('border')).toEqual('2px solid rgb(212, 30, 46)');
}); });
it('should display a div with replaced classes', function () { it('should display a div with many classes', function () {
expect(element.all(by.css('div')).get(5).getAttribute('class')).toEqual('new-class'); expect(element.all(by.css('div')).get(1).getAttribute('class')).toContain('special');
}); expect(element.all(by.css('div')).get(1).getAttribute('class')).toContain('clearance');
it('should display four buttons', function() {
let redButton = element.all(by.css('button')).get(1);
let saveButton = element.all(by.css('button')).get(2);
let bigButton = element.all(by.css('button')).get(3);
let smallButton = element.all(by.css('button')).get(4);
expect(redButton.getCssValue('color')).toEqual('rgba(255, 0, 0, 1)');
expect(saveButton.getCssValue('background-color')).toEqual('rgba(0, 255, 255, 1)');
expect(bigButton.getText()).toBe('Big');
expect(smallButton.getText()).toBe('Small');
}); });
}); });

View File

@ -27,43 +27,41 @@
<hr /> <hr />
<h2>Class binding</h2> <h2>Styling precedence</h2>
<!-- #docregion add-class --> <!-- #docregion basic-specificity -->
<h3>Bind to a specific class</h3> <h3>Basic specificity</h3>
<div class="item clearance" [class.special]="isSpecial">This class binding is special.</div> <!-- The `class.special` binding will override any value for the `special` class in `classExpr`. -->
<!-- #enddocregion add-class --> <div [class.special]="isSpecial" [class]="classExpr">Some text.</div>
<!-- #docregion bind-syntax --> <!-- The `style.color` binding will override any value for the `color` property in `styleExpr`. -->
<div [style.color]="color" [style]="styleExpr">Some text.</div>
<!-- #enddocregion basic-specificity -->
<h3>Using the bind- syntax:</h3> <!-- #docregion source-specificity -->
<h3>Source specificity</h3>
<div bind-class.special="isSpecial">This class binding is special too.</div> <!-- The `class.special` template binding will override any host binding to the `special` class set by `dirWithClassBinding` or `comp-with-host-binding`.-->
<!-- #enddocregion bind-syntax --> <comp-with-host-binding [class.special]="isSpecial" dirWithClassBinding>Some text.</comp-with-host-binding>
<!-- #docregion direct-class-binding --> <!-- The `style.color` template binding will override any host binding to the `color` property set by `dirWithStyleBinding` or `comp-with-host-binding`. -->
<h3>Bind to multiple classes</h3> <comp-with-host-binding [style.color]="color" dirWithStyleBinding>Some text.</comp-with-host-binding>
<!-- #enddocregion source-specificity -->
<div [class]="someClasses">Add multiple classes</div> <!-- #docregion dynamic-priority -->
<!-- #enddocregion direct-class-binding --> <h3>Dynamic vs static</h3>
<hr /> <!-- If `classExpr` has a value for the `special` class, this value will override the `class="special"` below -->
<div class="special" [class]="classExpr">Some text.</div>
<h2>Style binding</h2> <!-- If `styleExpr` has a value for the `color` property, this value will override the `style="color: blue"` below -->
<div style="color: blue" [style]="styleExpr">Some text.</div>
<!-- #docregion style-binding--> <!-- #enddocregion dynamic-priority -->
<button [style.color]="isSpecial ? 'red': 'green'">Red</button>
<button [style.background-color]="canSave ? 'cyan': 'grey'" >Save</button>
<!-- #enddocregion style-binding -->
<!-- #docregion style-binding-condition--> <!-- #docregion style-delegation -->
<button [style.font-size.em]="isSpecial ? 3 : 1" >Big</button> <comp-with-host-binding dirWithHostBinding></comp-with-host-binding>
<button [style.font-size.%]="!isSpecial ? 150 : 50" >Small</button> <!-- #enddocregion style-delegation -->
<!-- #enddocregion style-binding-condition-->
<!-- #docregion direct-style-binding -->
<h3>Bind to multiple styles</h3>
<div style="color: blue" [style]="styleExpr">Add multiple styles</div>
<!-- #enddocregion direct-style-binding -->

View File

@ -9,8 +9,7 @@ export class AppComponent {
actionName = 'Go for it'; actionName = 'Go for it';
isSpecial = true; isSpecial = true;
canSave = true; canSave = true;
someClasses = 'foo bar'; classExpr = 'special clearance';
classExpr = 'special foo';
styleExpr = 'color: red'; styleExpr = 'color: red';
color = 'blue'; color = 'blue';
} }

View File

@ -3,11 +3,13 @@ import { NgModule } from '@angular/core';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { CompWithHostBindingComponent } from './comp-with-host-binding.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
AppComponent AppComponent,
CompWithHostBindingComponent
], ],
imports: [ imports: [
BrowserModule BrowserModule

View File

@ -0,0 +1,16 @@
import { Component } from '@angular/core';
@Component({
selector: 'comp-with-host-binding',
template: 'I am a component!',
host: {
'[class.special]': 'isSpecial',
'[style.color]': 'color',
'[style.width]': 'width'
}
})
export class CompWithHostBindingComponent {
isSpecial = false;
color = 'green';
width = '200px';
}

View File

@ -892,68 +892,97 @@ Instead, you'd use property binding and write it like this:
### Class binding ### Class binding
Add and remove CSS class names from an element's `class` attribute with Here's how to set the `class` attribute without a binding in plain HTML:
a **class binding**.
Here's how to set the attribute without a binding in plain HTML:
```html ```html
<!-- standard class attribute setting --> <!-- standard class attribute setting -->
<div class="item clearance special">Item clearance special</div> <div class="foo bar">Some text</div>
``` ```
Class binding syntax resembles property binding, but instead of an element property between brackets, start with the prefix `class`, You can also add and remove CSS class names from an element's `class` attribute with a **class binding**.
optionally followed by a dot (`.`) and the name of a CSS class: `[class.class-name]`.
Angular adds the class when the template expression evaluates to truthy.
It removes the class when the expression is falsy.
Binding to a specific class is additive, so it won't overwrite other class bindings or static classes unless the class names are duplicated.
In the example below, the final class list for the `<div>` will be `"item clearance special"` if `isSpecial` is truthy, and only `"item clearance"` if it is falsy. To create a single class binding, start with the prefix `class` followed by a dot (`.`) and the name of the CSS class (for example, `[class.foo]="hasFoo"`).
Angular adds the class when the bound expression is truthy, and it removes the class when the expression is falsy (with the exception of `undefined`, see [styling delegation](#styling-delegation)).
<code-example path="attribute-binding/src/app/app.component.html" region="add-class" header="src/app/app.component.html"></code-example> To create a binding to multiple classes, use a generic `[class]` binding without the dot (for example, `[class]="classExpr"`).
The expression can be a space-delimited string of class names, or you can format it as an object with class names as the keys and truthy/falsy expressions as the values.
With object format, Angular will add a class only if its associated value is truthy.
You can also use the alternative class binding syntax that replaces square brackets with the `bind-` keyword: It's important to note that with any object-like expression (`object`, `Array`, `Map`, `Set`, etc), the identity of the object must change for the class list to be updated.
<code-example path="attribute-binding/src/app/app.component.html" region="bind-syntax" header="src/app/app.component.html"></code-example>
If there are multiple classes you'd like to toggle, you can bind to the `[class]` property directly.
Binding to `[class]` is additive, so it shouldn't overwrite other class bindings or static classes unless the class names are duplicated*.
<code-example path="attribute-binding/src/app/app.component.html" region="direct-class-binding" header="src/app/app.component.html"></code-example>
The expression attached to the `[class]` binding is most often a string list of class names like `"clearance special"`.
You can also format the expression as an object with class names as the keys and truthy/falsy expressions as the values, like `{clearance: true, special: false}`.
In this case, Angular will add a class only if its associated value is truthy.
It's important to note that with object format, the identity of the object must change for the class list to be updated.
Updating the property without changing object identity will have no effect. Updating the property without changing object identity will have no effect.
*This is true for Angular version 9 and later. For Angular version 8, see <a href="http://v8.angular.io/guide/template-syntax#class-binding">v8.angular.io</a> If there are multiple bindings to the same class name, conflicts are resolved using [styling precedence](#styling-precedence).
<style>
td, th {vertical-align: top}
</style>
<table width="100%">
<col width="15%">
</col>
<col width="20%">
</col>
<col width="35%">
</col>
<col width="30%">
</col>
<tr>
<th>
Binding Type
</th>
<th>
Syntax
</th>
<th>
Input Type
</th>
<th>
Example Input Values
</th>
</tr>
<tr>
<td>Single class binding</td>
<td><code>[class.foo]="hasFoo"</code></td>
<td><code>boolean | undefined | null</code></td>
<td><code>true</code>, <code>false</code></td>
</tr>
<tr>
<td rowspan=3>Multi-class binding</td>
<td rowspan=3><code>[class]="classExpr"</code></td>
<td><code>string</code></td>
<td><code>"my-class-1 my-class-2 my-class-3"</code></td>
</tr>
<tr>
<td><code>{[key: string]: boolean | undefined | null}</code></td>
<td><code>{foo: true, bar: false}</code></td>
</tr>
<tr>
<td><code>Array</code><<code>string</code>></td>
<td><code>['foo', 'bar']</code></td>
</tr>
</table>
The [NgClass](#ngclass) directive can be used as an alternative to direct `[class]` bindings.
However, using the above class binding syntax without `NgClass` is preferred because due to improvements in class binding in Angular, `NgClass` no longer provides significant value, and might eventually be removed in the future.
<hr/> <hr/>
### Style binding ### Style binding
Here's how to set the style attribute without a binding in plain HTML: Here's how to set the `style` attribute without a binding in plain HTML:
```html ```html
<!-- standard style attribute setting --> <!-- standard style attribute setting -->
<div style="color: blue">Item clearance special</div> <div style="color: blue">Some text</div>
``` ```
You can set styles dynamically with a **style binding**. You can also set styles dynamically with a **style binding**.
Style binding syntax resembles property binding. To create a single style binding, start with the prefix `style` followed by a dot (`.`) and the name of the CSS style property (for example, `[style.width]="width"`).
Instead of an element property between brackets, start with the prefix `style`, The property will be set to the value of the bound expression, which is normally a string.
followed by a dot (`.`) and the name of a CSS style property: `[style.style-property]`. Optionally, you can add a unit extension like `em` or `%`, which requires a number type.
<code-example path="attribute-binding/src/app/app.component.html" region="style-binding" header="src/app/app.component.html"></code-example>
Some style binding styles have a unit extension.
The following example conditionally sets the font size in “em” and “%” units.
<code-example path="attribute-binding/src/app/app.component.html" region="style-binding-condition" header="src/app/app.component.html"></code-example>
<div class="alert is-helpful"> <div class="alert is-helpful">
@ -963,22 +992,140 @@ Note that a _style property_ name can be written in either
</div> </div>
If there are multiple styles you'd like to toggle, you can bind to the `[style]` property directly. If there are multiple styles you'd like to toggle, you can bind to the `[style]` property directly without the dot (for example, `[style]="styleExpr"`).
Binding to `[style]` is additive, so it shouldn't overwrite other style bindings or static styles unless the same style property is duplicated.
<code-example path="attribute-binding/src/app/app.component.html" region="direct-style-binding" header="src/app/app.component.html"></code-example>
The expression attached to the `[style]` binding is most often a string list of styles like `"width: 100px; height: 100px;"`. The expression attached to the `[style]` binding is most often a string list of styles like `"width: 100px; height: 100px;"`.
You can also format the expression as an object with style names as the keys and style values as the values, like `{width: '100px', height: '100px'}`. You can also format the expression as an object with style names as the keys and style values as the values, like `{width: '100px', height: '100px'}`.
It's important to note that with object format, the identity of the object must change for the styles to be updated. It's important to note that with any object-like expression (`object`, `Array`, `Map`, `Set`, etc), the identity of the object must change for the class list to be updated.
Updating the property without changing object identity will have no effect. Updating the property without changing object identity will have no effect.
*This is true for Angular version 9 and later. For Angular version 8, see <a href="http://v8.angular.io/guide/template-syntax#style-binding">v8.angular.io</a> If there are multiple bindings to the same style property, conflicts are resolved using [styling precedence rules](#styling-precedence).
<style>
td, th {vertical-align: top}
</style>
<table width="100%">
<col width="15%">
</col>
<col width="20%">
</col>
<col width="35%">
</col>
<col width="30%">
</col>
<tr>
<th>
Binding Type
</th>
<th>
Syntax
</th>
<th>
Input Type
</th>
<th>
Example Input Values
</th>
</tr>
<tr>
<td>Single style binding</td>
<td><code>[style.width]="width"</code></td>
<td><code>string | undefined | null</code></td>
<td><code>"100px"</code></td>
</tr>
<tr>
<tr>
<td>Single style binding with units</td>
<td><code>[style.width.px]="width"</code></td>
<td><code>number | undefined | null</code></td>
<td><code>100</code></td>
</tr>
<tr>
<td rowspan=3>Multi-style binding</td>
<td rowspan=3><code>[style]="styleExpr"</code></td>
<td><code>string</code></td>
<td><code>"width: 100px; height: 100px"</code></td>
</tr>
<tr>
<td><code>{[key: string]: string | undefined | null}</code></td>
<td><code>{width: '100px', height: '100px'}</code></td>
</tr>
<tr>
<td><code>Array</code><<code>string</code>></td>
<td><code>['width', '100px']</code></td>
</tr>
</table>
The [NgStyle](#ngstyle) directive can be used as an alternative to direct `[style]` bindings.
However, using the above style binding syntax without `NgStyle` is preferred because due to improvements in style binding in Angular, `NgStyle` no longer provides significant value, and might eventually be removed in the future.
<hr/> <hr/>
{@a styling-precedence}
### Styling Precedence
A single HTML element can have its CSS class list and style values bound to a multiple sources (for example, host bindings from multiple directives).
When there are multiple bindings to the same class name or style property, Angular uses a set of precedence rules to resolve conflicts and determine which classes or styles are ultimately applied to the element.
<div class="alert is-helpful">
<h4>Styling precedence (highest to lowest)</h4>
1. Template bindings
1. Property binding (for example, `<div [class.foo]="hasFoo">` or `<div [style.color]="color">`)
1. Map binding (for example, `<div [class]="classExpr">` or `<div [style]="styleExpr">`)
1. Static value (for example, `<div class="foo">` or `<div style="color: blue">`)
1. Directive host bindings
1. Property binding (for example, `host: {'[class.foo]': 'hasFoo'}` or `host: {'[style.color]': 'color'}`)
1. Map binding (for example, `host: {'[class]': 'classExpr'}` or `host: {'[style]': 'styleExpr'}`)
1. Static value (for example, `host: {'class': 'foo'}` or `host: {'style': 'color: blue'}`)
1. Component host bindings
1. Property binding (for example, `host: {'[class.foo]': 'hasFoo'}` or `host: {'[style.color]': 'color'}`)
1. Map binding (for example, `host: {'[class]': 'classExpr'}` or `host: {'[style]': 'styleExpr'}`)
1. Static value (for example, `host: {'class': 'foo'}` or `host: {'style': 'color: blue'}`)
</div>
The more specific a class or style binding is, the higher its precedence.
A binding to a specific class (for example, `[class.foo]`) will take precedence over a generic `[class]` binding, and a binding to a specific style (for example, `[style.bar]`) will take precedence over a generic `[style]` binding.
<code-example path="attribute-binding/src/app/app.component.html" region="basic-specificity" header="src/app/app.component.html"></code-example>
Specificity rules also apply when it comes to bindings that originate from different sources.
It's possible for an element to have bindings in the template where it's declared, from host bindings on matched directives, and from host bindings on matched components.
Template bindings are the most specific because they apply to the element directly and exclusively, so they have the highest precedence.
Directive host bindings are considered less specific because directives can be used in multiple locations, so they have a lower precedence than template bindings.
Directives often augment component behavior, so host bindings from components have the lowest precedence.
<code-example path="attribute-binding/src/app/app.component.html" region="source-specificity" header="src/app/app.component.html"></code-example>
In addition, bindings take precedence over static attributes.
In the following case, `class` and `[class]` have similar specificity, but the `[class]` binding will take precedence because it is dynamic.
<code-example path="attribute-binding/src/app/app.component.html" region="dynamic-priority" header="src/app/app.component.html"></code-example>
{@a styling-delegation}
### Delegating to styles with lower precedence
It is possible for higher precedence styles to "delegate" to lower precedence styles using `undefined` values.
Whereas setting a style property to `null` ensures the style is removed, setting it to `undefined` will cause Angular to fall back to the next-highest precedence binding to that style.
For example, consider the following template:
<code-example path="attribute-binding/src/app/app.component.html" region="style-delegation" header="src/app/app.component.html"></code-example>
Imagine that the `dirWithHostBinding` directive and the `comp-with-host-binding` component both have a `[style.width]` host binding.
In that case, if `dirWithHostBinding` sets its binding to `undefined`, the `width` property will fall back to the value of the `comp-with-host-binding` host binding.
However, if `dirWithHostBinding` sets its binding to `null`, the `width` property will be removed entirely.
{@a event-binding} {@a event-binding}
## Event binding `(event)` ## Event binding `(event)`