fix(core): Host classes should not be fed back into @Input (#35889)

Previously all static styling information (including the ones from component/directive host bindings) would get merged into a single value before it would be written into the `@Input('class'/'style')`. The new behavior specifically excludes host values from the `@Input` bindings.

Fix #35383

PR Close #35889
This commit is contained in:
Miško Hevery
2020-03-05 16:17:26 -08:00
committed by Kara Erickson
parent aaa89bb715
commit cda2530df5
10 changed files with 154 additions and 34 deletions

View File

@ -177,7 +177,7 @@ export function createRootComponentView(
const tNode: TElementNode = getOrCreateTNode(tView, null, 0, TNodeType.Element, null, null);
const mergedAttrs = tNode.mergedAttrs = def.hostAttrs;
if (mergedAttrs !== null) {
computeStaticStyling(tNode, mergedAttrs);
computeStaticStyling(tNode, mergedAttrs, true);
if (rNode !== null) {
setUpAttributes(hostRenderer, rNode, mergedAttrs);
if (tNode.classes !== null) {

View File

@ -39,8 +39,12 @@ function elementStartFirstCreatePass(
resolveDirectives(tView, lView, tNode, getConstant<string[]>(tViewConsts, localRefsIndex));
ngDevMode && logUnknownElementError(tView, lView, native, tNode, hasDirectives);
if (tNode.attrs !== null) {
computeStaticStyling(tNode, tNode.attrs, false);
}
if (tNode.mergedAttrs !== null) {
computeStaticStyling(tNode, tNode.mergedAttrs);
computeStaticStyling(tNode, tNode.mergedAttrs, true);
}
if (tView.queries !== null) {
@ -148,12 +152,12 @@ export function ɵɵelementEnd(): void {
}
}
if (tNode.classes !== null && hasClassInput(tNode)) {
setDirectiveInputsWhichShadowsStyling(tView, tNode, getLView(), tNode.classes, true);
if (tNode.classesWithoutHost != null && hasClassInput(tNode)) {
setDirectiveInputsWhichShadowsStyling(tView, tNode, getLView(), tNode.classesWithoutHost, true);
}
if (tNode.styles !== null && hasStyleInput(tNode)) {
setDirectiveInputsWhichShadowsStyling(tView, tNode, getLView(), tNode.styles, false);
if (tNode.stylesWithoutHost != null && hasStyleInput(tNode)) {
setDirectiveInputsWhichShadowsStyling(tView, tNode, getLView(), tNode.stylesWithoutHost, false);
}
}

View File

@ -33,7 +33,7 @@ function elementContainerStartFirstCreatePass(
// While ng-container doesn't necessarily support styling, we use the style context to identify
// and execute directives on the ng-container.
if (attrs !== null) {
computeStaticStyling(tNode, attrs);
computeStaticStyling(tNode, attrs, true);
}
const localRefs = getConstant<string[]>(tViewConsts, localRefsIndex);

View File

@ -179,8 +179,10 @@ class TNode implements ITNode {
public parent: TElementNode|TContainerNode|null, //
public projection: number|(ITNode|RNode[])[]|null, //
public styles: string|null, //
public stylesWithoutHost: string|null, //
public residualStyles: KeyValueArray<any>|undefined|null, //
public classes: string|null, //
public classesWithoutHost: string|null, //
public residualClasses: KeyValueArray<any>|undefined|null, //
public classBindings: TStylingRange, //
public styleBindings: TStylingRange, //

View File

@ -851,8 +851,10 @@ export function createTNode(
tParent, // parent: TElementNode|TContainerNode|null
null, // projection: number|(ITNode|RNode[])[]|null
null, // styles: string|null
null, // stylesWithoutHost: string|null
undefined, // residualStyles: string|null
null, // classes: string|null
null, // classesWithoutHost: string|null
undefined, // residualClasses: string|null
0 as any, // classBindings: TStylingRange;
0 as any, // styleBindings: TStylingRange;
@ -881,8 +883,10 @@ export function createTNode(
parent: tParent,
projection: null,
styles: null,
stylesWithoutHost: null,
residualStyles: undefined,
classes: null,
classesWithoutHost: null,
residualClasses: undefined,
classBindings: 0 as any,
styleBindings: 0 as any,

View File

@ -221,7 +221,7 @@ export function checkStylingMap(
// the binding has removed it. This would confuse `[ngStyle]`/`[ngClass]` to do the wrong
// thing as it would think that the static portion was removed. For this reason we
// concatenate it so that `[ngStyle]`/`[ngClass]` can continue to work on changed.
let staticPrefix = isClassBased ? tNode.classes : tNode.styles;
let staticPrefix = isClassBased ? tNode.classesWithoutHost : tNode.stylesWithoutHost;
ngDevMode && isClassBased === false && staticPrefix !== null &&
assertEqual(
staticPrefix.endsWith(';'), true, 'Expecting static portion to end with \';\'');

View File

@ -502,14 +502,30 @@ export interface TNode {
projection: (TNode|RNode[])[]|number|null;
/**
* A collection of all style static values for an element.
* A collection of all `style` static values for an element (including from host).
*
* This field will be populated if and when:
*
* - There are one or more initial styles on an element (e.g. `<div style="width:200px">`)
* - There are one or more initial `style`s on an element (e.g. `<div style="width:200px;">`)
* - There are one or more initial `style`s on a directive/component host
* (e.g. `@Directive({host: {style: "width:200px;" } }`)
*/
styles: string|null;
/**
* A collection of all `style` static values for an element excluding host sources.
*
* Populated when there are one or more initial `style`s on an element
* (e.g. `<div style="width:200px;">`)
* Must be stored separately from `tNode.styles` to facilitate setting directive
* inputs that shadow the `style` property. If we used `tNode.styles` as is for shadowed inputs,
* we would feed host styles back into directives as "inputs". If we used `tNode.attrs`, we would
* have to concatenate the attributes on every template pass. Instead, we process once on first
* create pass and store here.
*/
stylesWithoutHost: string|null;
/**
* A `KeyValueArray` version of residual `styles`.
*
@ -540,14 +556,29 @@ export interface TNode {
residualStyles: KeyValueArray<any>|undefined|null;
/**
* A collection of all class static values for an element.
* A collection of all class static values for an element (including from host).
*
* This field will be populated if and when:
*
* - There are one or more initial classes on an element (e.g. `<div class="one two three">`)
* - There are one or more initial classes on an directive/component host
* (e.g. `@Directive({host: {class: "SOME_CLASS" } }`)
*/
classes: string|null;
/**
* A collection of all class static values for an element excluding host sources.
*
* Populated when there are one or more initial classes on an element
* (e.g. `<div class="SOME_CLASS">`)
* Must be stored separately from `tNode.classes` to facilitate setting directive
* inputs that shadow the `class` property. If we used `tNode.classes` as is for shadowed inputs,
* we would feed host classes back into directives as "inputs". If we used `tNode.attrs`, we would
* have to concatenate the attributes on every template pass. Instead, we process once on first
* create pass and store here.
*/
classesWithoutHost: string|null;
/**
* A `KeyValueArray` version of residual `classes`.
*

View File

@ -18,25 +18,31 @@ import {getTView} from '../state';
*
* @param tNode The `TNode` into which the styling information should be loaded.
* @param attrs `TAttributes` containing the styling information.
* @param writeToHost Where should the resulting static styles be written?
* - `false` Write to `TNode.stylesWithoutHost` / `TNode.classesWithoutHost`
* - `true` Write to `TNode.styles` / `TNode.classes`
*/
export function computeStaticStyling(tNode: TNode, attrs: TAttributes): void {
export function computeStaticStyling(
tNode: TNode, attrs: TAttributes|null, writeToHost: boolean): void {
ngDevMode &&
assertFirstCreatePass(getTView(), 'Expecting to be called in first template pass only');
let styles: string|null = tNode.styles;
let classes: string|null = tNode.classes;
let styles: string|null = writeToHost ? tNode.styles : null;
let classes: string|null = writeToHost ? tNode.classes : null;
let mode: AttributeMarker|0 = 0;
for (let i = 0; i < attrs.length; i++) {
const value = attrs[i];
if (typeof value === 'number') {
mode = value;
} else if (mode == AttributeMarker.Classes) {
classes = concatStringsWithSpace(classes, value as string);
} else if (mode == AttributeMarker.Styles) {
const style = value as string;
const styleValue = attrs[++i] as string;
styles = concatStringsWithSpace(styles, style + ': ' + styleValue + ';');
if (attrs !== null) {
for (let i = 0; i < attrs.length; i++) {
const value = attrs[i];
if (typeof value === 'number') {
mode = value;
} else if (mode == AttributeMarker.Classes) {
classes = concatStringsWithSpace(classes, value as string);
} else if (mode == AttributeMarker.Styles) {
const style = value as string;
const styleValue = attrs[++i] as string;
styles = concatStringsWithSpace(styles, style + ': ' + styleValue + ';');
}
}
}
styles !== null && (tNode.styles = styles);
classes !== null && (tNode.classes = classes);
writeToHost ? tNode.styles = styles : tNode.stylesWithoutHost = styles;
writeToHost ? tNode.classes = classes : tNode.classesWithoutHost = classes;
}