From 7526ef77b778b101060152401a5fddde2f010318 Mon Sep 17 00:00:00 2001 From: Kara Erickson Date: Thu, 30 Jan 2020 14:09:10 -0800 Subject: [PATCH] docs(ivy): add docs for styling priority order (#35066) PR Close #35066 --- aio/angular.json | 5 +- .../attribute-binding/e2e/src/app.e2e-spec.ts | 19 +- .../src/app/app.component.html | 52 ++-- .../src/app/app.component.ts | 3 +- .../attribute-binding/src/app/app.module.ts | 4 +- .../app/comp-with-host-binding.component.ts | 16 ++ aio/content/guide/template-syntax.md | 243 ++++++++++++++---- 7 files changed, 247 insertions(+), 95 deletions(-) create mode 100644 aio/content/examples/attribute-binding/src/app/comp-with-host-binding.component.ts diff --git a/aio/angular.json b/aio/angular.json index 28e22467a8..222b569341 100644 --- a/aio/angular.json +++ b/aio/angular.json @@ -5,7 +5,8 @@ "packageManager": "yarn", "warnings": { "typescriptMismatch": false - } + }, + "analytics": false }, "newProjectRoot": "projects", "projects": { @@ -192,4 +193,4 @@ } }, "defaultProject": "site" -} +} \ No newline at end of file diff --git a/aio/content/examples/attribute-binding/e2e/src/app.e2e-spec.ts b/aio/content/examples/attribute-binding/e2e/src/app.e2e-spec.ts index c02fa87e1b..95281656a9 100644 --- a/aio/content/examples/attribute-binding/e2e/src/app.e2e-spec.ts +++ b/aio/content/examples/attribute-binding/e2e/src/app.e2e-spec.ts @@ -25,23 +25,12 @@ describe('Attribute binding example', 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 () { - expect(element.all(by.css('div')).get(5).getAttribute('class')).toEqual('new-class'); - }); - - 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'); + it('should display a div with many classes', function () { + expect(element.all(by.css('div')).get(1).getAttribute('class')).toContain('special'); + expect(element.all(by.css('div')).get(1).getAttribute('class')).toContain('clearance'); }); }); diff --git a/aio/content/examples/attribute-binding/src/app/app.component.html b/aio/content/examples/attribute-binding/src/app/app.component.html index 20f7ca2add..69f82857fd 100644 --- a/aio/content/examples/attribute-binding/src/app/app.component.html +++ b/aio/content/examples/attribute-binding/src/app/app.component.html @@ -27,43 +27,41 @@
-

Class binding

+

Styling precedence

- -

Bind to a specific class

+ +

Basic specificity

-
This class binding is special.
- + +
Some text.
- + +
Some text.
+ -

Using the bind- syntax:

+ +

Source specificity

-
This class binding is special too.
- + +Some text. - -

Bind to multiple classes

+ +Some text. + -
Add multiple classes
- + +

Dynamic vs static

-
+ +
Some text.
-

Style binding

+ +
Some text.
- - - - + - - - - + + + - -

Bind to multiple styles

-
Add multiple styles
- diff --git a/aio/content/examples/attribute-binding/src/app/app.component.ts b/aio/content/examples/attribute-binding/src/app/app.component.ts index 043ee92e95..ab35bb09a0 100644 --- a/aio/content/examples/attribute-binding/src/app/app.component.ts +++ b/aio/content/examples/attribute-binding/src/app/app.component.ts @@ -9,8 +9,7 @@ export class AppComponent { actionName = 'Go for it'; isSpecial = true; canSave = true; - someClasses = 'foo bar'; - classExpr = 'special foo'; + classExpr = 'special clearance'; styleExpr = 'color: red'; color = 'blue'; } diff --git a/aio/content/examples/attribute-binding/src/app/app.module.ts b/aio/content/examples/attribute-binding/src/app/app.module.ts index 926975afe8..82bb10d175 100644 --- a/aio/content/examples/attribute-binding/src/app/app.module.ts +++ b/aio/content/examples/attribute-binding/src/app/app.module.ts @@ -3,11 +3,13 @@ import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; +import { CompWithHostBindingComponent } from './comp-with-host-binding.component'; @NgModule({ declarations: [ - AppComponent + AppComponent, + CompWithHostBindingComponent ], imports: [ BrowserModule diff --git a/aio/content/examples/attribute-binding/src/app/comp-with-host-binding.component.ts b/aio/content/examples/attribute-binding/src/app/comp-with-host-binding.component.ts new file mode 100644 index 0000000000..f41c3d4278 --- /dev/null +++ b/aio/content/examples/attribute-binding/src/app/comp-with-host-binding.component.ts @@ -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'; +} diff --git a/aio/content/guide/template-syntax.md b/aio/content/guide/template-syntax.md index 40d2652fcf..b197073594 100644 --- a/aio/content/guide/template-syntax.md +++ b/aio/content/guide/template-syntax.md @@ -892,68 +892,97 @@ Instead, you'd use property binding and write it like this: ### Class binding -Add and remove CSS class names from an element's `class` attribute with -a **class binding**. - -Here's how to set the attribute without a binding in plain HTML: +Here's how to set the `class` attribute without a binding in plain HTML: ```html -
Item clearance special
+
Some text
``` -Class binding syntax resembles property binding, but instead of an element property between brackets, start with the prefix `class`, -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. +You can also add and remove CSS class names from an element's `class` attribute with a **class binding**. -In the example below, the final class list for the `
` 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)). - +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: - - - -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*. - - - -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. +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. -*This is true for Angular version 9 and later. For Angular version 8, see v8.angular.io +If there are multiple bindings to the same class name, conflicts are resolved using [styling precedence](#styling-precedence). + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Binding Type + + Syntax + + Input Type + + Example Input Values +
Single class binding[class.foo]="hasFoo"boolean | undefined | nulltrue, false
Multi-class binding[class]="classExpr"string"my-class-1 my-class-2 my-class-3"
{[key: string]: boolean | undefined | null}{foo: true, bar: false}
Array<string>['foo', 'bar']
+ + +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. +
### 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 -
Item clearance special
+
Some text
``` -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. -Instead of an element property between brackets, start with the prefix `style`, -followed by a dot (`.`) and the name of a CSS style property: `[style.style-property]`. - - - -Some style binding styles have a unit extension. -The following example conditionally sets the font size in “em” and “%” units. - - +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"`). +The property will be set to the value of the bound expression, which is normally a string. +Optionally, you can add a unit extension like `em` or `%`, which requires a number type.
@@ -963,22 +992,140 @@ Note that a _style property_ name can be written in either
-If there are multiple styles you'd like to toggle, you can bind to the `[style]` property directly. -Binding to `[style]` is additive, so it shouldn't overwrite other style bindings or static styles unless the same style property is duplicated. - - - +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"`). 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'}`. -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. -*This is true for Angular version 9 and later. For Angular version 8, see v8.angular.io +If there are multiple bindings to the same style property, conflicts are resolved using [styling precedence rules](#styling-precedence). + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Binding Type + + Syntax + + Input Type + + Example Input Values +
Single style binding[style.width]="width"string | undefined | null"100px"
Single style binding with units[style.width.px]="width"number | undefined | null100
Multi-style binding[style]="styleExpr"string"width: 100px; height: 100px"
{[key: string]: string | undefined | null}{width: '100px', height: '100px'}
Array<string>['width', '100px']
+ +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.
+{@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. + +
+

Styling precedence (highest to lowest)

+ +1. Template bindings + 1. Property binding (for example, `
` or `
`) + 1. Map binding (for example, `
` or `
`) + 1. Static value (for example, `
` or `
`) +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'}`) + +
+ +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. + + + +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. + + + +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. + + + +{@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: + + + +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} ## Event binding `(event)`