perf(ivy): apply [style]/[class] bindings directly to style/className (#33336)

This patch ensures that the `[style]` and `[class]` based bindings
are directly applied to an element's style and className attributes.

This patch optimizes the algorithm so that it...
- Doesn't construct an update an instance of `StylingMapArray` for
  `[style]` and `[class]` bindings
- Doesn't apply `[style]` and `[class]` based entries using
  `classList` and `style` (direct attributes are used instead)
- Doesn't split or iterate over all string-based tokens in a
  string value obtained from a `[class]` binding.

This patch speeds up the following cases:
- `<div [class]>` and `<div class="..." [class]>`
- `<div [style]>` and `<div style="..." [style]>`

The overall speec increase is by over 5x.

PR Close #33336
This commit is contained in:
Matias Niemelä 2019-10-22 15:18:40 -07:00 committed by Andrew Kushnir
parent ee4fc12e42
commit dcdb433b7d
13 changed files with 359 additions and 102 deletions

View File

@ -34,7 +34,7 @@ export const ngClassDirectiveDef__POST_R3__ = ɵɵdefineDirective({
selectors: null as any, selectors: null as any,
hostBindings: function(rf: ɵRenderFlags, ctx: any, elIndex: number) { hostBindings: function(rf: ɵRenderFlags, ctx: any, elIndex: number) {
if (rf & ɵRenderFlags.Create) { if (rf & ɵRenderFlags.Create) {
ɵɵallocHostVars(1); ɵɵallocHostVars(2);
} }
if (rf & ɵRenderFlags.Update) { if (rf & ɵRenderFlags.Update) {
ɵɵclassMap(ctx.getValue()); ɵɵclassMap(ctx.getValue());

View File

@ -35,7 +35,7 @@ export const ngStyleDirectiveDef__POST_R3__ = ɵɵdefineDirective({
selectors: null as any, selectors: null as any,
hostBindings: function(rf: ɵRenderFlags, ctx: any, elIndex: number) { hostBindings: function(rf: ɵRenderFlags, ctx: any, elIndex: number) {
if (rf & ɵRenderFlags.Create) { if (rf & ɵRenderFlags.Create) {
ɵɵallocHostVars(1); ɵɵallocHostVars(2);
} }
if (rf & ɵRenderFlags.Update) { if (rf & ɵRenderFlags.Update) {
ɵɵstyleMap(ctx.getValue()); ɵɵstyleMap(ctx.getValue());

View File

@ -436,7 +436,7 @@ describe('compiler compliance: styling', () => {
const template = ` const template = `
decls: 1, decls: 1,
vars: 2, vars: 3,
template: function MyComponentWithInterpolation_Template(rf, $ctx$) { template: function MyComponentWithInterpolation_Template(rf, $ctx$) {
if (rf & 1) { if (rf & 1) {
$r3$.ɵɵelement(0, "div"); $r3$.ɵɵelement(0, "div");
@ -447,7 +447,7 @@ describe('compiler compliance: styling', () => {
} }
decls: 1, decls: 1,
vars: 3, vars: 4,
template: function MyComponentWithMuchosInterpolation_Template(rf, $ctx$) { template: function MyComponentWithMuchosInterpolation_Template(rf, $ctx$) {
if (rf & 1) { if (rf & 1) {
$r3$.ɵɵelement(0, "div"); $r3$.ɵɵelement(0, "div");
@ -458,7 +458,7 @@ describe('compiler compliance: styling', () => {
} }
decls: 1, decls: 1,
vars: 1, vars: 2,
template: function MyComponentWithoutInterpolation_Template(rf, $ctx$) { template: function MyComponentWithoutInterpolation_Template(rf, $ctx$) {
if (rf & 1) { if (rf & 1) {
$r3$.ɵɵelement(0, "div"); $r3$.ɵɵelement(0, "div");
@ -506,7 +506,7 @@ describe('compiler compliance: styling', () => {
type: MyComponent, type: MyComponent,
selectors:[["my-component"]], selectors:[["my-component"]],
decls: 1, decls: 1,
vars: 4, vars: 5,
consts: [[${AttributeMarker.Styles}, "opacity", "1"]], consts: [[${AttributeMarker.Styles}, "opacity", "1"]],
template: function MyComponent_Template(rf, $ctx$) { template: function MyComponent_Template(rf, $ctx$) {
if (rf & 1) { if (rf & 1) {
@ -700,7 +700,7 @@ describe('compiler compliance: styling', () => {
type: MyComponent, type: MyComponent,
selectors:[["my-component"]], selectors:[["my-component"]],
decls: 1, decls: 1,
vars: 4, vars: 5,
consts: [[${AttributeMarker.Classes}, "grape"]], consts: [[${AttributeMarker.Classes}, "grape"]],
template: function MyComponent_Template(rf, $ctx$) { template: function MyComponent_Template(rf, $ctx$) {
if (rf & 1) { if (rf & 1) {
@ -863,8 +863,8 @@ describe('compiler compliance: styling', () => {
} }
if (rf & 2) { if (rf & 2) {
$r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer); $r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵstyleMap($r3$.ɵɵpipeBind1(1, 2, $ctx$.myStyleExp)); $r3$.ɵɵstyleMap($r3$.ɵɵpipeBind1(1, 4, $ctx$.myStyleExp));
$r3$.ɵɵclassMap($r3$.ɵɵpipeBind1(2, 4, $ctx$.myClassExp)); $r3$.ɵɵclassMap($r3$.ɵɵpipeBind1(2, 6, $ctx$.myClassExp));
} }
} }
`; `;
@ -916,11 +916,11 @@ describe('compiler compliance: styling', () => {
} }
if (rf & 2) { if (rf & 2) {
$r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer); $r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵstyleMap($r3$.ɵɵpipeBind2(1, 6, $ctx$.myStyleExp, 1000)); $r3$.ɵɵstyleMap($r3$.ɵɵpipeBind2(1, 8, $ctx$.myStyleExp, 1000));
$r3$.ɵɵclassMap($e2_styling$); $r3$.ɵɵclassMap($e2_styling$);
$r3$.ɵɵstyleProp("bar", $r3$.ɵɵpipeBind2(2, 9, $ctx$.barExp, 3000)); $r3$.ɵɵstyleProp("bar", $r3$.ɵɵpipeBind2(2, 11, $ctx$.barExp, 3000));
$r3$.ɵɵstyleProp("baz", $r3$.ɵɵpipeBind2(3, 12, $ctx$.bazExp, 4000)); $r3$.ɵɵstyleProp("baz", $r3$.ɵɵpipeBind2(3, 14, $ctx$.bazExp, 4000));
$r3$.ɵɵclassProp("foo", $r3$.ɵɵpipeBind2(4, 15, $ctx$.fooExp, 2000)); $r3$.ɵɵclassProp("foo", $r3$.ɵɵpipeBind2(4, 17, $ctx$.fooExp, 2000));
$r3$.ɵɵadvance(5); $r3$.ɵɵadvance(5);
$r3$.ɵɵtextInterpolate1(" ", $ctx$.item, ""); $r3$.ɵɵtextInterpolate1(" ", $ctx$.item, "");
} }
@ -1018,7 +1018,7 @@ describe('compiler compliance: styling', () => {
const template = ` const template = `
hostBindings: function MyComponent_HostBindings(rf, ctx, elIndex) { hostBindings: function MyComponent_HostBindings(rf, ctx, elIndex) {
if (rf & 1) { if (rf & 1) {
$r3$.ɵɵallocHostVars(4); $r3$.ɵɵallocHostVars(6);
$r3$.ɵɵelementHostAttrs($e0_attrs$); $r3$.ɵɵelementHostAttrs($e0_attrs$);
} }
if (rf & 2) { if (rf & 2) {
@ -1077,7 +1077,7 @@ describe('compiler compliance: styling', () => {
const template = ` const template = `
hostBindings: function MyComponent_HostBindings(rf, ctx, elIndex) { hostBindings: function MyComponent_HostBindings(rf, ctx, elIndex) {
if (rf & 1) { if (rf & 1) {
$r3$.ɵɵallocHostVars(6); $r3$.ɵɵallocHostVars(8);
} }
if (rf & 2) { if (rf & 2) {
$r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer); $r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer);
@ -1152,7 +1152,7 @@ describe('compiler compliance: styling', () => {
const hostBindings = ` const hostBindings = `
hostBindings: function MyComponent_HostBindings(rf, ctx, elIndex) { hostBindings: function MyComponent_HostBindings(rf, ctx, elIndex) {
if (rf & 1) { if (rf & 1) {
$r3$.ɵɵallocHostVars(4); $r3$.ɵɵallocHostVars(6);
} }
if (rf & 2) { if (rf & 2) {
$r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer); $r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer);
@ -1218,7 +1218,7 @@ describe('compiler compliance: styling', () => {
const template = ` const template = `
function ClassDirective_HostBindings(rf, ctx, elIndex) { function ClassDirective_HostBindings(rf, ctx, elIndex) {
if (rf & 1) { if (rf & 1) {
$r3$.ɵɵallocHostVars(1); $r3$.ɵɵallocHostVars(2);
} }
if (rf & 2) { if (rf & 2) {
$r3$.ɵɵclassMap(ctx.myClassMap); $r3$.ɵɵclassMap(ctx.myClassMap);
@ -1506,7 +1506,7 @@ describe('compiler compliance: styling', () => {
hostBindings: function MyComponent_HostBindings(rf, ctx, elIndex) { hostBindings: function MyComponent_HostBindings(rf, ctx, elIndex) {
if (rf & 1) { if (rf & 1) {
$r3$.ɵɵallocHostVars(4); $r3$.ɵɵallocHostVars(6);
$r3$.ɵɵelementHostAttrs($_c0$); $r3$.ɵɵelementHostAttrs($_c0$);
} }
if (rf & 2) { if (rf & 2) {

View File

@ -326,7 +326,10 @@ export class StylingBuilder {
valueConverter: ValueConverter, isClassBased: boolean, valueConverter: ValueConverter, isClassBased: boolean,
stylingInput: BoundStylingEntry): StylingInstruction { stylingInput: BoundStylingEntry): StylingInstruction {
// each styling binding value is stored in the LView // each styling binding value is stored in the LView
let totalBindingSlotsRequired = 1; // map-based bindings allocate two slots: one for the
// previous binding value and another for the previous
// className or style attribute value.
let totalBindingSlotsRequired = 2;
// these values must be outside of the update block so that they can // these values must be outside of the update block so that they can
// be evaluated (the AST visit call) during creation time so that any // be evaluated (the AST visit call) during creation time so that any

View File

@ -96,7 +96,7 @@ export function stylePropInternal(
// in this case we do not need to do anything, but the binding index // in this case we do not need to do anything, but the binding index
// still needs to be incremented because all styling binding values // still needs to be incremented because all styling binding values
// are stored inside of the lView. // are stored inside of the lView.
const bindingIndex = lView[BINDING_INDEX]++; const bindingIndex = getAndIncrementBindingIndex(lView, false);
const updated = const updated =
stylingProp(elementIndex, bindingIndex, prop, resolveStylePropValue(value, suffix), false); stylingProp(elementIndex, bindingIndex, prop, resolveStylePropValue(value, suffix), false);
@ -130,7 +130,7 @@ export function ɵɵclassProp(className: string, value: boolean | null): void {
// in this case we do not need to do anything, but the binding index // in this case we do not need to do anything, but the binding index
// still needs to be incremented because all styling binding values // still needs to be incremented because all styling binding values
// are stored inside of the lView. // are stored inside of the lView.
const bindingIndex = lView[BINDING_INDEX]++; const bindingIndex = getAndIncrementBindingIndex(lView, false);
const updated = stylingProp(getSelectedIndex(), bindingIndex, className, value, true); const updated = stylingProp(getSelectedIndex(), bindingIndex, className, value, true);
if (ngDevMode) { if (ngDevMode) {
@ -189,8 +189,7 @@ function stylingProp(
const sanitizerToUse = isClassBased ? null : sanitizer; const sanitizerToUse = isClassBased ? null : sanitizer;
const renderer = getRenderer(tNode, lView); const renderer = getRenderer(tNode, lView);
updated = applyStylingValueDirectly( updated = applyStylingValueDirectly(
renderer, context, native, lView, bindingIndex, prop, value, isClassBased, renderer, context, native, lView, bindingIndex, prop, value, isClassBased, sanitizerToUse);
isClassBased ? setClass : setStyle, sanitizerToUse);
if (sanitizerToUse) { if (sanitizerToUse) {
// it's important we remove the current style sanitizer once the // it's important we remove the current style sanitizer once the
@ -243,28 +242,23 @@ export function ɵɵstyleMap(styles: {[styleName: string]: any} | NO_CHANGE | nu
const lView = getLView(); const lView = getLView();
const tNode = getTNode(index, lView); const tNode = getTNode(index, lView);
const context = getStylesContext(tNode); const context = getStylesContext(tNode);
const hasDirectiveInput = hasStyleInput(tNode);
// if a value is interpolated then it may render a `NO_CHANGE` value. // if a value is interpolated then it may render a `NO_CHANGE` value.
// in this case we do not need to do anything, but the binding index // in this case we do not need to do anything, but the binding index
// still needs to be incremented because all styling binding values // still needs to be incremented because all styling binding values
// are stored inside of the lView. // are stored inside of the lView.
const bindingIndex = lView[BINDING_INDEX]++; const bindingIndex = getAndIncrementBindingIndex(lView, true);
// inputs are only evaluated from a template binding into a directive, therefore, // inputs are only evaluated from a template binding into a directive, therefore,
// there should not be a situation where a directive host bindings function // there should not be a situation where a directive host bindings function
// evaluates the inputs (this should only happen in the template function) // evaluates the inputs (this should only happen in the template function)
if (!isHostStyling() && hasStyleInput(tNode) && styles !== NO_CHANGE) { if (!isHostStyling() && hasDirectiveInput && styles !== NO_CHANGE) {
updateDirectiveInputValue(context, lView, tNode, bindingIndex, styles, false); updateDirectiveInputValue(context, lView, tNode, bindingIndex, styles, false);
styles = NO_CHANGE; styles = NO_CHANGE;
} }
const updated = stylingMap(index, context, bindingIndex, styles, false); stylingMap(context, tNode, lView, bindingIndex, styles, false, hasDirectiveInput);
if (ngDevMode) {
ngDevMode.styleMap++;
if (updated) {
ngDevMode.styleMapCacheMiss++;
}
}
} }
/** /**
@ -300,28 +294,23 @@ export function classMapInternal(
const lView = getLView(); const lView = getLView();
const tNode = getTNode(elementIndex, lView); const tNode = getTNode(elementIndex, lView);
const context = getClassesContext(tNode); const context = getClassesContext(tNode);
const hasDirectiveInput = hasClassInput(tNode);
// if a value is interpolated then it may render a `NO_CHANGE` value. // if a value is interpolated then it may render a `NO_CHANGE` value.
// in this case we do not need to do anything, but the binding index // in this case we do not need to do anything, but the binding index
// still needs to be incremented because all styling binding values // still needs to be incremented because all styling binding values
// are stored inside of the lView. // are stored inside of the lView.
const bindingIndex = lView[BINDING_INDEX]++; const bindingIndex = getAndIncrementBindingIndex(lView, true);
// inputs are only evaluated from a template binding into a directive, therefore, // inputs are only evaluated from a template binding into a directive, therefore,
// there should not be a situation where a directive host bindings function // there should not be a situation where a directive host bindings function
// evaluates the inputs (this should only happen in the template function) // evaluates the inputs (this should only happen in the template function)
if (!isHostStyling() && hasClassInput(tNode) && classes !== NO_CHANGE) { if (!isHostStyling() && hasDirectiveInput && classes !== NO_CHANGE) {
updateDirectiveInputValue(context, lView, tNode, bindingIndex, classes, true); updateDirectiveInputValue(context, lView, tNode, bindingIndex, classes, true);
classes = NO_CHANGE; classes = NO_CHANGE;
} }
const updated = stylingMap(elementIndex, context, bindingIndex, classes, true); stylingMap(context, tNode, lView, bindingIndex, classes, true, hasDirectiveInput);
if (ngDevMode) {
ngDevMode.classMap++;
if (updated) {
ngDevMode.classMapCacheMiss++;
}
}
} }
/** /**
@ -331,13 +320,10 @@ export function classMapInternal(
* `[class]` bindings in Angular. * `[class]` bindings in Angular.
*/ */
function stylingMap( function stylingMap(
elementIndex: number, context: TStylingContext, bindingIndex: number, context: TStylingContext, tNode: TNode, lView: LView, bindingIndex: number,
value: {[key: string]: any} | string | null, isClassBased: boolean): boolean { value: {[key: string]: any} | string | null, isClassBased: boolean,
let updated = false; hasDirectiveInput: boolean): void {
const lView = getLView();
const directiveIndex = getActiveDirectiveId(); const directiveIndex = getActiveDirectiveId();
const tNode = getTNode(elementIndex, lView);
const native = getNativeByTNode(tNode, lView) as RElement; const native = getNativeByTNode(tNode, lView) as RElement;
const oldValue = getValue(lView, bindingIndex); const oldValue = getValue(lView, bindingIndex);
const hostBindingsMode = isHostStyling(); const hostBindingsMode = isHostStyling();
@ -359,17 +345,14 @@ function stylingMap(
patchConfig(context, TStylingConfig.HasMapBindings); patchConfig(context, TStylingConfig.HasMapBindings);
} }
const stylingMapArr =
value === NO_CHANGE ? NO_CHANGE : normalizeIntoStylingMap(oldValue, value, !isClassBased);
// Direct Apply Case: bypass context resolution and apply the // Direct Apply Case: bypass context resolution and apply the
// style/class map values directly to the element // style/class map values directly to the element
if (allowDirectStyling(context, hostBindingsMode)) { if (allowDirectStyling(context, hostBindingsMode)) {
const sanitizerToUse = isClassBased ? null : sanitizer; const sanitizerToUse = isClassBased ? null : sanitizer;
const renderer = getRenderer(tNode, lView); const renderer = getRenderer(tNode, lView);
updated = applyStylingMapDirectly( applyStylingMapDirectly(
renderer, context, native, lView, bindingIndex, stylingMapArr as StylingMapArray, renderer, context, native, lView, bindingIndex, value, isClassBased, sanitizerToUse,
isClassBased, isClassBased ? setClass : setStyle, sanitizerToUse, valueHasChanged); valueHasChanged, hasDirectiveInput);
if (sanitizerToUse) { if (sanitizerToUse) {
// it's important we remove the current style sanitizer once the // it's important we remove the current style sanitizer once the
// element exits, otherwise it will be used by the next styling // element exits, otherwise it will be used by the next styling
@ -377,7 +360,9 @@ function stylingMap(
setElementExitFn(stylingApply); setElementExitFn(stylingApply);
} }
} else { } else {
updated = valueHasChanged; const stylingMapArr =
value === NO_CHANGE ? NO_CHANGE : normalizeIntoStylingMap(oldValue, value, !isClassBased);
activateStylingMapFeature(); activateStylingMapFeature();
// Context Resolution (or first update) Case: save the map value // Context Resolution (or first update) Case: save the map value
@ -396,7 +381,12 @@ function stylingMap(
setElementExitFn(stylingApply); setElementExitFn(stylingApply);
} }
return updated; if (ngDevMode) {
isClassBased ? ngDevMode.classMap : ngDevMode.styleMap++;
if (valueHasChanged) {
isClassBased ? ngDevMode.classMapCacheMiss : ngDevMode.styleMapCacheMiss++;
}
}
} }
/** /**
@ -452,8 +442,8 @@ function normalizeStylingDirectiveInputValue(
value = concatString(initialValue, forceClassesAsString(bindingValue)); value = concatString(initialValue, forceClassesAsString(bindingValue));
} else { } else {
value = concatString( value = concatString(
initialValue, forceStylesAsString(bindingValue as{[key: string]: any} | null | undefined), initialValue,
';'); forceStylesAsString(bindingValue as{[key: string]: any} | null | undefined, true), ';');
} }
} }
return value; return value;
@ -594,3 +584,11 @@ function resolveStylePropValue(
function isHostStyling(): boolean { function isHostStyling(): boolean {
return isHostStylingActive(getActiveDirectiveId()); return isHostStylingActive(getActiveDirectiveId());
} }
function getAndIncrementBindingIndex(lView: LView, isMapBased: boolean): number {
// map-based bindings use two slots because the previously constructed
// className / style value must be compared against.
const index = lView[BINDING_INDEX];
lView[BINDING_INDEX] += isMapBased ? 2 : 1;
return index;
}

View File

@ -7,14 +7,15 @@
*/ */
import {SafeValue, unwrapSafeValue} from '../../sanitization/bypass'; import {SafeValue, unwrapSafeValue} from '../../sanitization/bypass';
import {StyleSanitizeFn, StyleSanitizeMode} from '../../sanitization/style_sanitizer'; import {StyleSanitizeFn, StyleSanitizeMode} from '../../sanitization/style_sanitizer';
import {global} from '../../util/global';
import {ProceduralRenderer3, RElement, Renderer3, RendererStyleFlags3, isProceduralRenderer} from '../interfaces/renderer'; import {ProceduralRenderer3, RElement, Renderer3, RendererStyleFlags3, isProceduralRenderer} from '../interfaces/renderer';
import {ApplyStylingFn, LStylingData, StylingMapArray, StylingMapArrayIndex, StylingMapsSyncMode, SyncStylingMapsFn, TStylingConfig, TStylingContext, TStylingContextIndex, TStylingContextPropConfigFlags} from '../interfaces/styling'; import {ApplyStylingFn, LStylingData, StylingMapArray, StylingMapArrayIndex, StylingMapsSyncMode, SyncStylingMapsFn, TStylingConfig, TStylingContext, TStylingContextIndex, TStylingContextPropConfigFlags} from '../interfaces/styling';
import {NO_CHANGE} from '../tokens'; import {NO_CHANGE} from '../tokens';
import {DEFAULT_BINDING_INDEX, DEFAULT_BINDING_VALUE, DEFAULT_GUARD_MASK_VALUE, MAP_BASED_ENTRY_PROP_NAME, TEMPLATE_DIRECTIVE_INDEX, getBindingValue, getConfig, getDefaultValue, getGuardMask, getInitialStylingValue, getMapProp, getMapValue, getProp, getPropValuesStartPosition, getStylingMapArray, getTotalSources, getValue, getValuesCount, hasConfig, hasValueChanged, isContextLocked, isHostStylingActive, isSanitizationRequired, isStylingValueDefined, lockContext, patchConfig, setDefaultValue, setGuardMask, setMapAsDirty, setValue} from '../util/styling_utils'; import {DEFAULT_BINDING_INDEX, DEFAULT_BINDING_VALUE, DEFAULT_GUARD_MASK_VALUE, MAP_BASED_ENTRY_PROP_NAME, TEMPLATE_DIRECTIVE_INDEX, concatString, forceStylesAsString, getBindingValue, getConfig, getDefaultValue, getGuardMask, getInitialStylingValue, getMapProp, getMapValue, getProp, getPropValuesStartPosition, getStylingMapArray, getTotalSources, getValue, getValuesCount, hasConfig, hasValueChanged, isContextLocked, isHostStylingActive, isSanitizationRequired, isStylingMapArray, isStylingValueDefined, lockContext, normalizeIntoStylingMap, patchConfig, setDefaultValue, setGuardMask, setMapAsDirty, setValue} from '../util/styling_utils';
import {getStylingState, resetStylingState} from './state'; import {getStylingState, resetStylingState} from './state';
const VALUE_IS_EXTERNALLY_MODIFIED = {};
/** /**
* -------- * --------
@ -655,12 +656,69 @@ export function applyStylingViaContext(
*/ */
export function applyStylingMapDirectly( export function applyStylingMapDirectly(
renderer: any, context: TStylingContext, element: RElement, data: LStylingData, renderer: any, context: TStylingContext, element: RElement, data: LStylingData,
bindingIndex: number, map: StylingMapArray, isClassBased: boolean, applyFn: ApplyStylingFn, bindingIndex: number, value: {[key: string]: any} | string | null, isClassBased: boolean,
sanitizer?: StyleSanitizeFn | null, forceUpdate?: boolean): boolean { sanitizer?: StyleSanitizeFn | null, forceUpdate?: boolean,
if (forceUpdate || hasValueChanged(data[bindingIndex], map)) { bindingValueContainsInitial?: boolean): void {
setValue(data, bindingIndex, map); const oldValue = getValue(data, bindingIndex);
const initialStyles = if (forceUpdate || hasValueChanged(oldValue, value)) {
hasConfig(context, TStylingConfig.HasInitialStyling) ? getStylingMapArray(context) : null; const config = getConfig(context);
const hasInitial = config & TStylingConfig.HasInitialStyling;
const initialValue =
hasInitial && !bindingValueContainsInitial ? getInitialStylingValue(context) : null;
setValue(data, bindingIndex, value);
// the cached value is the last snapshot of the style or class
// attribute value and is used in the if statement below to
// keep track of internal/external changes.
const cachedValueIndex = bindingIndex + 1;
let cachedValue = getValue(data, cachedValueIndex);
if (cachedValue === NO_CHANGE) {
cachedValue = initialValue;
}
cachedValue = typeof cachedValue !== 'string' ? '' : cachedValue;
// If a class/style value was modified externally then the styling
// fast pass cannot guarantee that the external values are retained.
// When this happens, the algorithm will bail out and not write to
// the style or className attribute directly.
let writeToAttrDirectly = !(config & TStylingConfig.HasPropBindings);
if (writeToAttrDirectly &&
checkIfExternallyModified(element as HTMLElement, cachedValue, isClassBased)) {
writeToAttrDirectly = false;
if (oldValue !== VALUE_IS_EXTERNALLY_MODIFIED) {
// direct styling will reset the attribute entirely each time,
// and, for this reason, if the algorithm decides it cannot
// write to the class/style attributes directly then it must
// reset all the previous style/class values before it starts
// to apply values in the non-direct way.
removeStylingValues(renderer, element, oldValue, isClassBased);
// this will instruct the algorithm not to apply class or style
// values directly anymore.
setValue(data, cachedValueIndex, VALUE_IS_EXTERNALLY_MODIFIED);
}
}
if (writeToAttrDirectly) {
let valueToApply: string;
if (isClassBased) {
valueToApply = typeof value === 'string' ? value : objectToClassName(value);
if (initialValue !== null) {
valueToApply = concatString(initialValue, valueToApply, ' ');
}
setClassName(renderer, element, valueToApply);
} else {
valueToApply = forceStylesAsString(value as{[key: string]: any}, true);
if (initialValue !== null) {
valueToApply = initialValue + ';' + valueToApply;
}
setStyleAttr(renderer, element, valueToApply);
}
setValue(data, cachedValueIndex, valueToApply || null);
} else {
const applyFn = isClassBased ? setClass : setStyle;
const map = normalizeIntoStylingMap(oldValue, value, !isClassBased);
const initialStyles = hasInitial ? getStylingMapArray(context) : null;
for (let i = StylingMapArrayIndex.ValuesStartPosition; i < map.length; for (let i = StylingMapArrayIndex.ValuesStartPosition; i < map.length;
i += StylingMapArrayIndex.TupleSize) { i += StylingMapArrayIndex.TupleSize) {
@ -689,10 +747,8 @@ export function applyStylingMapDirectly(
} else { } else {
state.lastDirectStyleMap = map; state.lastDirectStyleMap = map;
} }
return true;
} }
return false; }
} }
/** /**
@ -727,11 +783,12 @@ export function applyStylingMapDirectly(
*/ */
export function applyStylingValueDirectly( export function applyStylingValueDirectly(
renderer: any, context: TStylingContext, element: RElement, data: LStylingData, renderer: any, context: TStylingContext, element: RElement, data: LStylingData,
bindingIndex: number, prop: string, value: any, isClassBased: boolean, applyFn: ApplyStylingFn, bindingIndex: number, prop: string, value: any, isClassBased: boolean,
sanitizer?: StyleSanitizeFn | null): boolean { sanitizer?: StyleSanitizeFn | null): boolean {
let applied = false; let applied = false;
if (hasValueChanged(data[bindingIndex], value)) { if (hasValueChanged(data[bindingIndex], value)) {
setValue(data, bindingIndex, value); setValue(data, bindingIndex, value);
const applyFn = isClassBased ? setClass : setStyle;
// case 1: apply the provided value (if it exists) // case 1: apply the provided value (if it exists)
applied = applyStylingValue(renderer, element, prop, value, applyFn, bindingIndex, sanitizer); applied = applyStylingValue(renderer, element, prop, value, applyFn, bindingIndex, sanitizer);
@ -888,6 +945,26 @@ export const setClass: ApplyStylingFn =
} }
}; };
export const setClassName = (renderer: Renderer3 | null, native: RElement, className: string) => {
if (renderer !== null) {
if (isProceduralRenderer(renderer)) {
renderer.setAttribute(native, 'class', className);
} else {
native.className = className;
}
}
};
export const setStyleAttr = (renderer: Renderer3 | null, native: RElement, value: string) => {
if (renderer !== null) {
if (isProceduralRenderer(renderer)) {
renderer.setAttribute(native, 'style', value);
} else {
native.setAttribute('style', value);
}
}
};
/** /**
* Iterates over all provided styling entries and renders them on the element. * Iterates over all provided styling entries and renders them on the element.
* *
@ -914,3 +991,63 @@ export function renderStylingMap(
} }
} }
} }
function objectToClassName(obj: {[key: string]: any} | null): string {
let str = '';
if (obj) {
for (let key in obj) {
const value = obj[key];
if (value) {
str += (str.length ? ' ' : '') + key;
}
}
}
return str;
}
/**
* Determines whether or not an element style/className value has changed since the last update.
*
* This function helps Angular determine if a style or class attribute value was
* modified by an external plugin or API outside of the style binding code. This
* means any JS code that adds/removes class/style values on an element outside
* of Angular's styling binding algorithm.
*
* @returns true when the value was modified externally.
*/
function checkIfExternallyModified(element: HTMLElement, cachedValue: any, isClassBased: boolean) {
// this means it was checked before and there is no reason
// to compare the style/class values again. Either that or
// web workers are being used.
if (global.Node === 'undefined' || cachedValue === VALUE_IS_EXTERNALLY_MODIFIED) return true;
// comparing the DOM value against the cached value is the best way to
// see if something has changed.
const currentValue =
(isClassBased ? element.className : (element.style && element.style.cssText)) || '';
return currentValue !== (cachedValue || '');
}
/**
* Removes provided styling values from the element
*/
function removeStylingValues(
renderer: any, element: RElement, values: string | {[key: string]: any} | StylingMapArray,
isClassBased: boolean) {
let arr: StylingMapArray;
if (isStylingMapArray(values)) {
arr = values as StylingMapArray;
} else {
arr = normalizeIntoStylingMap(null, values, !isClassBased);
}
const applyFn = isClassBased ? setClass : setStyle;
for (let i = StylingMapArrayIndex.ValuesStartPosition; i < arr.length;
i += StylingMapArrayIndex.TupleSize) {
const value = getMapValue(arr, i);
if (value) {
const prop = getMapProp(arr, i);
applyFn(renderer, element, prop, false);
}
}
}

View File

@ -5,12 +5,13 @@
* Use of this source code is governed by an MIT-style license that can be * Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {createProxy} from '../../debug/proxy';
import {StyleSanitizeFn} from '../../sanitization/style_sanitizer'; import {StyleSanitizeFn} from '../../sanitization/style_sanitizer';
import {RElement} from '../interfaces/renderer'; import {RElement} from '../interfaces/renderer';
import {ApplyStylingFn, LStylingData, TStylingConfig, TStylingContext, TStylingContextIndex} from '../interfaces/styling'; import {ApplyStylingFn, LStylingData, TStylingConfig, TStylingContext, TStylingContextIndex} from '../interfaces/styling';
import {getCurrentStyleSanitizer} from '../state'; import {getCurrentStyleSanitizer} from '../state';
import {attachDebugObject} from '../util/debug_utils'; import {attachDebugObject} from '../util/debug_utils';
import {MAP_BASED_ENTRY_PROP_NAME, TEMPLATE_DIRECTIVE_INDEX, allowDirectStyling as _allowDirectStyling, getBindingValue, getDefaultValue, getGuardMask, getProp, getPropValuesStartPosition, getValuesCount, hasConfig, isContextLocked, isSanitizationRequired, isStylingContext} from '../util/styling_utils'; import {MAP_BASED_ENTRY_PROP_NAME, TEMPLATE_DIRECTIVE_INDEX, allowDirectStyling as _allowDirectStyling, getBindingValue, getDefaultValue, getGuardMask, getProp, getPropValuesStartPosition, getValue, getValuesCount, hasConfig, isContextLocked, isSanitizationRequired, isStylingContext, normalizeIntoStylingMap, setValue} from '../util/styling_utils';
import {applyStylingViaContext} from './bindings'; import {applyStylingViaContext} from './bindings';
import {activateStylingMapFeature} from './map_based_bindings'; import {activateStylingMapFeature} from './map_based_bindings';
@ -374,10 +375,52 @@ export class NodeStylingDebug implements DebugNodeStyling {
*/ */
get summary(): {[key: string]: DebugNodeStylingEntry} { get summary(): {[key: string]: DebugNodeStylingEntry} {
const entries: {[key: string]: DebugNodeStylingEntry} = {}; const entries: {[key: string]: DebugNodeStylingEntry} = {};
this._mapValues((prop: string, value: any, bindingIndex: number | null) => { const config = this.config;
const isClassBased = this._isClassBased;
let data = this._data;
// the direct pass code doesn't convert [style] or [class] values
// into StylingMapArray instances. For this reason, the values
// need to be converted ahead of time since the styling debug
// relies on context resolution to figure out what styling
// values have been added/removed on the element.
if (config.allowDirectStyling && config.hasMapBindings) {
data = data.concat([]); // make a copy
this._convertMapBindingsToStylingMapArrays(data);
}
this._mapValues(data, (prop: string, value: any, bindingIndex: number | null) => {
entries[prop] = {prop, value, bindingIndex}; entries[prop] = {prop, value, bindingIndex};
}); });
return entries;
// because the styling algorithm runs into two different
// modes: direct and context-resolution, the output of the entries
// object is different because the removed values are not
// saved between updates. For this reason a proxy is created
// so that the behavior is the same when examining values
// that are no longer active on the element.
return createProxy({
get(target: {}, prop: string): DebugNodeStylingEntry{
let value: DebugNodeStylingEntry = entries[prop]; if (!value) {
value = {
prop,
value: isClassBased ? false : null,
bindingIndex: null,
};
} return value;
},
set(target: {}, prop: string, value: any) { return false; },
ownKeys() { return Object.keys(entries); },
getOwnPropertyDescriptor(k: any) {
// we use a special property descriptor here so that enumeration operations
// such as `Object.keys` will work on this proxy.
return {
enumerable: true,
configurable: true,
};
},
});
} }
get config() { return buildConfig(this.context.context); } get config() { return buildConfig(this.context.context); }
@ -387,11 +430,41 @@ export class NodeStylingDebug implements DebugNodeStyling {
*/ */
get values(): {[key: string]: any} { get values(): {[key: string]: any} {
const entries: {[key: string]: any} = {}; const entries: {[key: string]: any} = {};
this._mapValues((prop: string, value: any) => { entries[prop] = value; }); const config = this.config;
let data = this._data;
// the direct pass code doesn't convert [style] or [class] values
// into StylingMapArray instances. For this reason, the values
// need to be converted ahead of time since the styling debug
// relies on context resolution to figure out what styling
// values have been added/removed on the element.
if (config.allowDirectStyling && config.hasMapBindings) {
data = data.concat([]); // make a copy
this._convertMapBindingsToStylingMapArrays(data);
}
this._mapValues(data, (prop: string, value: any) => { entries[prop] = value; });
return entries; return entries;
} }
private _mapValues(fn: (prop: string, value: string|null, bindingIndex: number|null) => any) { private _convertMapBindingsToStylingMapArrays(data: LStylingData) {
const context = this.context.context;
const limit = getPropValuesStartPosition(context);
for (let i =
TStylingContextIndex.ValuesStartPosition + TStylingContextIndex.BindingsStartOffset;
i < limit; i++) {
const bindingIndex = context[i] as number;
const bindingValue = bindingIndex !== 0 ? getValue(data, bindingIndex) : null;
if (bindingValue && !Array.isArray(bindingValue)) {
const stylingMapArray = normalizeIntoStylingMap(null, bindingValue, !this._isClassBased);
setValue(data, bindingIndex, stylingMapArray);
}
}
}
private _mapValues(
data: LStylingData,
fn: (prop: string, value: string|null, bindingIndex: number|null) => any) {
// there is no need to store/track an element instance. The // there is no need to store/track an element instance. The
// element is only used when the styling algorithm attempts to // element is only used when the styling algorithm attempts to
// style the value (and we mock out the stylingApplyFn anyway). // style the value (and we mock out the stylingApplyFn anyway).
@ -409,11 +482,11 @@ export class NodeStylingDebug implements DebugNodeStyling {
// run the template bindings // run the template bindings
applyStylingViaContext( applyStylingViaContext(
this.context.context, null, mockElement, this._data, true, mapFn, sanitizer, false); this.context.context, null, mockElement, data, true, mapFn, sanitizer, false);
// and also the host bindings // and also the host bindings
applyStylingViaContext( applyStylingViaContext(
this.context.context, null, mockElement, this._data, true, mapFn, sanitizer, true); this.context.context, null, mockElement, data, true, mapFn, sanitizer, true);
} }
} }

View File

@ -247,7 +247,7 @@ export function isStylingContext(value: any): boolean {
typeof value[1] !== 'string'; typeof value[1] !== 'string';
} }
export function isStylingMapArray(value: TStylingContext | StylingMapArray | null): boolean { export function isStylingMapArray(value: any): boolean {
// the StylingMapArray is in the format of [initial, prop, string, prop, string] // the StylingMapArray is in the format of [initial, prop, string, prop, string]
// and this is the defining value to distinguish between arrays // and this is the defining value to distinguish between arrays
return Array.isArray(value) && return Array.isArray(value) &&
@ -295,13 +295,18 @@ export function forceClassesAsString(classes: string | {[key: string]: any} | nu
return (classes as string) || ''; return (classes as string) || '';
} }
export function forceStylesAsString(styles: {[key: string]: any} | null | undefined): string { export function forceStylesAsString(
styles: {[key: string]: any} | null | undefined, hyphenateProps: boolean): string {
let str = ''; let str = '';
if (styles) { if (styles) {
const props = Object.keys(styles); const props = Object.keys(styles);
for (let i = 0; i < props.length; i++) { for (let i = 0; i < props.length; i++) {
const prop = props[i]; const prop = props[i];
str = concatString(str, `${prop}:${styles[prop]}`, ';'); const propLabel = hyphenateProps ? hyphenate(prop) : prop;
const value = styles[prop];
if (value !== null) {
str = concatString(str, `${propLabel}:${value}`, ';');
}
} }
} }
return str; return str;

View File

@ -1268,7 +1268,7 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
fixture.detectChanges(); fixture.detectChanges();
expect(fixture.nativeElement.innerHTML) expect(fixture.nativeElement.innerHTML)
.toEqual( .toEqual(
`<div test="" title="début 2 milieu 1 fin" class="foo"> traduction: un <i>email</i><!--ICU 20--> ` + `<div test="" title="début 2 milieu 1 fin" class="foo"> traduction: un <i>email</i><!--ICU 21--> ` +
`</div><div test="" class="foo"></div>`); `</div><div test="" class="foo"></div>`);
directiveInstances.forEach(instance => instance.klass = 'bar'); directiveInstances.forEach(instance => instance.klass = 'bar');
@ -1277,7 +1277,7 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
fixture.detectChanges(); fixture.detectChanges();
expect(fixture.nativeElement.innerHTML) expect(fixture.nativeElement.innerHTML)
.toEqual( .toEqual(
`<div test="" title="début 3 milieu 2 fin" class="bar"> traduction: 2 emails<!--ICU 20--> ` + `<div test="" title="début 3 milieu 2 fin" class="bar"> traduction: 2 emails<!--ICU 21--> ` +
`</div><div test="" class="bar"></div>`); `</div><div test="" class="bar"></div>`);
}); });

View File

@ -2309,6 +2309,34 @@ describe('styling', () => {
expect(fixture.debugElement.nativeElement.innerHTML).toContain('two'); expect(fixture.debugElement.nativeElement.innerHTML).toContain('two');
expect(fixture.debugElement.nativeElement.innerHTML).toContain('three'); expect(fixture.debugElement.nativeElement.innerHTML).toContain('three');
}); });
onlyInIvy('only ivy treats [class] in concert with other class bindings')
.it('should retain classes added externally', () => {
@Component({template: `<div [class]="exp"></div>`})
class MyComp {
exp = '';
}
const fixture =
TestBed.configureTestingModule({declarations: [MyComp]}).createComponent(MyComp);
fixture.detectChanges();
const div = fixture.nativeElement.querySelector('div') !;
div.className += ' abc';
expect(splitSortJoin(div.className)).toEqual('abc');
fixture.componentInstance.exp = '1 2 3';
fixture.detectChanges();
expect(splitSortJoin(div.className)).toEqual('1 2 3 abc');
fixture.componentInstance.exp = '4 5 6 7';
fixture.detectChanges();
expect(splitSortJoin(div.className)).toEqual('4 5 6 7 abc');
function splitSortJoin(s: string) { return s.split(/\s+/).sort().join(' ').trim(); }
});
}); });
function assertStyleCounters(countForSet: number, countForRemove: number) { function assertStyleCounters(countForSet: number, countForRemove: number) {

View File

@ -608,6 +608,9 @@
{ {
"name": "getActiveDirectiveId" "name": "getActiveDirectiveId"
}, },
{
"name": "getAndIncrementBindingIndex"
},
{ {
"name": "getBeforeNodeForView" "name": "getBeforeNodeForView"
}, },

View File

@ -49,6 +49,9 @@ export class NoopRenderer implements ProceduralRenderer3 {
export class NoopRendererFactory implements RendererFactory3 { export class NoopRendererFactory implements RendererFactory3 {
createRenderer(hostElement: RElement|null, rendererType: null): Renderer3 { createRenderer(hostElement: RElement|null, rendererType: null): Renderer3 {
if (typeof global !== 'undefined') {
(global as any).Node = WebWorkerRenderNode;
}
return new NoopRenderer(); return new NoopRenderer();
} }
} }

View File

@ -13,6 +13,8 @@ describe('styling debugging tools', () => {
describe('NodeStylingDebug', () => { describe('NodeStylingDebug', () => {
it('should list out each of the values in the context paired together with the provided data', it('should list out each of the values in the context paired together with the provided data',
() => { () => {
if (isIE()) return;
const debug = makeContextWithDebug(false); const debug = makeContextWithDebug(false);
const context = debug.context; const context = debug.context;
const data: any[] = []; const data: any[] = [];
@ -67,3 +69,8 @@ function makeContextWithDebug(isClassBased: boolean) {
const ctx = allocTStylingContext(null, false); const ctx = allocTStylingContext(null, false);
return attachStylingDebugObject(ctx, isClassBased); return attachStylingDebugObject(ctx, isClassBased);
} }
function isIE() {
// note that this only applies to older IEs (not edge)
return typeof window !== 'undefined' && (window as any).document['documentMode'] ? true : false;
}