feat(ivy): Change static priority resolution to be same level as directive it belongs to (#34938)

This change changes the priority order of static styling.

Current priority:
```
(least priority)
- Static
  - Component
  - Directives
  - Template
- Dynamic Binding
  - Component
    - Map/Interpolation
    - Property
  - Directives
    - Map/Interpolation
    - Property
  - Template
    - Map/Interpolation
    - Property
(highest priority)
```

The issue with the above priority is this use case:

```
<div style="color: red;" directive-which-sets-color-blue>
```
In the above case the directive will win and the resulting color will be `blue`. However a small change of adding interpolation to the example like so. (Style interpolation is coming in https://github.com/angular/angular/pull/34202)
```
<div style="color: red; width: {{exp}}px" directive-which-sets-color-blue>
```
Changes the priority from static binding to interpolated binding which means now the resulting color is `red`. It is very surprising that adding an unrelated interpolation and style can change the `color` which was not changed. To fix that we need to make sure that the static values are associated with priority of the source (directive or template) where they were declared. The new resulting priority is:

```
(least priority)
- Component
  - Static
  - Map/Interpolation
  - Property
- Directives
  - Static
  - Map/Interpolation
  - Property
- Template
  - Static
  - Map/Interpolation
  - Property
(highest priority)
```

PR Close #34938
This commit is contained in:
Misko Hevery
2020-01-26 12:29:03 -08:00
committed by Andrew Kushnir
parent cf07d428a1
commit ee8b8f52aa
16 changed files with 953 additions and 207 deletions

View File

@ -8,6 +8,7 @@
import {ArrayMap} from '../../util/array_utils';
import {TStylingRange} from '../interfaces/styling';
import {DirectiveDef} from './definition';
import {CssSelector} from './projection';
import {RNode} from './renderer';
import {LView, TView} from './view';
@ -345,6 +346,13 @@ export interface TNode {
*/
mergedAttrs: TAttributes|null;
// TODO(misko): pre discussion with Kara, it seems that we don't need `directives` since the same
// information is already present in the TData. Maybe worth refactoring.
/**
* Stores the directive defs matched on the current TNode (along with style cursor.)
*/
directives: TDirectiveDefs|null;
/**
* A set of local names under which a given element is exported in a template and
* visible to queries. An entry in this array can be created for different reasons:
@ -481,7 +489,7 @@ export interface TNode {
projection: (TNode|RNode[])[]|number|null;
/**
* A collection of all style bindings and/or static style values for an element.
* A collection of all style static values for an element.
*
* This field will be populated if and when:
*
@ -490,21 +498,36 @@ export interface TNode {
styles: string|null;
/**
* An `ArrayMap` version of `styles.
* An `ArrayMap` version of residual `styles`.
*
* We need this when style bindings are resolving. This gets populated only if there are styling
* binding instructions. The laziness is important since we don't want to allocate the memory
* because most styling is static. For tree shaking purposes the code to create these only comes
* with styling.
* When there are styling instructions than each instruction stores the static styling
* which is of lower priority than itself. This means that there may be a higher priority styling
* than the instruction.
*
* Imagine:
* ```
* <div style="color: highest;" my-dir>
*
* @Directive({
* host: {
* style: 'color: lowest; ',
* '[styles.color]': 'exp' // ɵɵstyleProp('color', ctx.exp);
* }
* })
* ```
*
* In the above case:
* - `color: lowest` is stored with `ɵɵstyleProp('color', ctx.exp);` instruction
* - `color: highest` is the residual and is stored here.
*
* - `undefined': not initialized.
* - `null`: initialized but `styles` is `null`
* - `ArrayMap`: parsed version of `styles`.
*/
stylesMap: ArrayMap<any>|undefined|null;
residualStyles: ArrayMap<any>|undefined|null;
/**
* A collection of all class bindings and/or static class values for an element.
* A collection of all class static values for an element.
*
* This field will be populated if and when:
*
@ -513,18 +536,15 @@ export interface TNode {
classes: string|null;
/**
* An `ArrayMap` version of `classes`.
* An `ArrayMap` version of residual `classes`.
*
* We need this when style bindings are resolving. This gets populated only if there are styling
* binding instructions. The laziness is important since we don't want to allocate the memory
* because most styling is static. For tree shaking purposes the code to create these only comes
* with styling.
* Same as `TNode.residualStyles` but for classes.
*
* - `undefined': not initialized.
* - `null`: initialized but `classes` is `null`
* - `ArrayMap`: parsed version of `S`.
*/
classesMap: ArrayMap<any>|undefined|null;
residualClasses: ArrayMap<any>|undefined|null;
/**
* Stores the head/tail index of the class bindings.
@ -793,4 +813,55 @@ export function hasClassInput(tNode: TNode) {
*/
export function hasStyleInput(tNode: TNode) {
return (tNode.flags & TNodeFlags.hasStyleInput) !== 0;
}
/**
* Constant enums for accessing data in the `TDirectiveDefs`
*/
export const enum DirectiveDefs {
/// Location where the STYLING_CURSOR is stored.
STYLING_CURSOR = 0,
/// Header offset from which iterating over `DirectiveDefs` should start.
HEADER_OFFSET = 1
}
/**
* Constant enums for initial values in the `TDirectiveDefs`
*/
export const enum DirectiveDefsValues {
// Initial value for the `STYLING_CURSOR`
INITIAL_STYLING_CURSOR_VALUE = 0,
}
/**
* Stores `DirectiveDefs` associated with the current `TNode` as well as styling cursor.
*/
export interface TDirectiveDefs extends Array<number|DirectiveDef<any>> {
/**
* As styling instructions (`ɵɵstyleProp`/`ɵɵclassProp`/`ɵɵstyleMap`/`ɵɵclassMap`) are executing
* they also need to get a hold of the `DirectiveDef.hostAttrs` and so that they know what
* static styling values to use. The styling instructions need this information so that they can
* lazily create `TStylingStatic`.
*
* When styling is executing it can get a hold of its `DirectiveDefs` but that alone is not
* sufficient for two reasons:
* 1. Styling instruction needs to coalesce other directives which came before it and which have
* static value but may not have a styling instruction to attach the static values to.
* 2. There may be more than one styling instruction per `hostBindings` and only the first
* styling instruction should create the `TStylingStatic`.
*
* The algorithm for doing this is:
* - look up the current `DirectiveDef` associated with the current instruction.
* - If `STYLING_CURSOR === 0 || tDirectiveDefs[stylingCursor] !== currentDirectiveDef` than
* create `TStylingStatic` and:
* - iterate over `TDirectiveDefs[++stylingCursor]` and insert them into the `TStylingStatic`
* until you reach `DirectiveDef` associated with the current instruction.
* - If new `TStylingStatic` was created, recompute the residual styling values.
*
* The above algorithm will ensure that the styling instructions consume static styling values
* associated until a given instruction. After consuming instructions, it is always important to
* clear the residual (See `TNode.residualClass`/`TNode.residualStyle`), since this may be the
* last styling instruction, and we need to lazily recreate the residual value on as needed basis.
*/
[DirectiveDefs.STYLING_CURSOR]: number;
}