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:
parent
ee4fc12e42
commit
dcdb433b7d
|
@ -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());
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -608,6 +608,9 @@
|
||||||
{
|
{
|
||||||
"name": "getActiveDirectiveId"
|
"name": "getActiveDirectiveId"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "getAndIncrementBindingIndex"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "getBeforeNodeForView"
|
"name": "getBeforeNodeForView"
|
||||||
},
|
},
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue