From fe8301c46291eec3632812ca65c6e96c9d8a8e73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Fri, 8 Feb 2019 15:03:54 -0800 Subject: [PATCH] feat(ivy): provide support for map-based host bindings for [style] and [class] (#28246) Up until now, `[style]` and `[class]` bindings (the map-based ones) have only worked as template bindings and have not been supported at all inside of host bindings. This patch ensures that multiple host binding sources (components and directives) all properly assign style values and merge them correctly in terms of priority. Jira: FW-882 PR Close #28246 --- .../r3_view_compiler_styling_spec.ts | 78 + .../compiler/src/render3/view/compiler.ts | 31 +- .../src/render3/view/styling_builder.ts | 143 +- .../compiler/src/render3/view/template.ts | 3 +- .../src/template_parser/template_parser.ts | 2 +- packages/core/src/render3/instructions.ts | 132 +- packages/core/src/render3/interfaces/node.ts | 28 +- .../core/src/render3/interfaces/styling.ts | 256 +- .../styling/class_and_style_bindings.ts | 871 +++++-- packages/core/src/render3/styling/util.ts | 59 +- .../test/bundling/animation_world/index.ts | 47 +- .../cyclic_import/bundle.golden_symbols.json | 14 +- .../hello_world/bundle.golden_symbols.json | 6 +- .../bundling/todo/bundle.golden_symbols.json | 37 +- .../core/test/render3/integration_spec.ts | 2129 +++++++++-------- .../styling/class_and_style_bindings_spec.ts | 982 +++++--- 16 files changed, 3104 insertions(+), 1714 deletions(-) diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts index 203ffd0b54..98bfd8595f 100644 --- a/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts @@ -1062,6 +1062,84 @@ describe('compiler compliance: styling', () => { expectEmit(result.source, template, 'Incorrect template'); }); + it('should generate override instructions for only single-level styling bindings when !important is present', + () => { + const files = { + app: { + 'spec.ts': ` + import {Component, NgModule, HostBinding} from '@angular/core'; + + @Component({ + selector: 'my-component', + template: \` +
+ \`, + host: { + '[style!important]': 'myStyleExp', + '[class!important]': 'myClassExp' + } + }) + export class MyComponent { + @HostBinding('class.foo!important') + myFooClassExp = true; + + @HostBinding('style.width!important') + myWidthExp = '100px'; + + myBarClassExp = true; + myHeightExp = '200px'; + } + + @NgModule({declarations: [MyComponent]}) + export class MyModule {} + ` + } + }; + + const template = ` + const _c2 = ["bar"]; + const _c3 = ["height"]; + … + function MyComponent_Template(rf, ctx) { + if (rf & 1) { + $r3$.ɵelementStart(0, "div"); + $r3$.ɵelementStyling(_c2, _c3, $r3$.ɵdefaultStyleSanitizer); + $r3$.ɵelementEnd(); + } + if (rf & 2) { + $r3$.ɵelementStylingMap(0, ctx.myClassExp, ctx.myStyleExp); + $r3$.ɵelementStyleProp(0, 0, ctx.myHeightExp, null, true); + $r3$.ɵelementClassProp(0, 0, ctx.myBarClassExp, null, true); + $r3$.ɵelementStylingApply(0); + } + }, + `; + + const hostBindings = ` + const _c0 = ["foo"]; + const _c1 = ["width"]; + … + hostBindings: function MyComponent_HostBindings(rf, ctx, elIndex) { + if (rf & 1) { + $r3$.ɵelementStyling(_c0, _c1, $r3$.ɵdefaultStyleSanitizer, ctx); + } + if (rf & 2) { + $r3$.ɵelementStylingMap(elIndex, ctx.myClassExp, ctx.myStyleExp, ctx); + $r3$.ɵelementStyleProp(elIndex, 0, ctx.myWidthExp, null, ctx, true); + $r3$.ɵelementClassProp(elIndex, 0, ctx.myFooClassExp, ctx, true); + $r3$.ɵelementStylingApply(elIndex, ctx); + } + }, + `; + + const result = compile(files, angularFiles); + expectEmit(result.source, hostBindings, 'Incorrect template'); + expectEmit(result.source, template, 'Incorrect template'); + }); + it('should generate styling instructions for multiple directives that contain host binding definitions', () => { const files = { diff --git a/packages/compiler/src/render3/view/compiler.ts b/packages/compiler/src/render3/view/compiler.ts index f66f5ad873..bfc2b7aeca 100644 --- a/packages/compiler/src/render3/view/compiler.ts +++ b/packages/compiler/src/render3/view/compiler.ts @@ -39,8 +39,8 @@ const EMPTY_ARRAY: any[] = []; // If there is a match, the first matching group will contain the attribute name to bind. const ATTR_REGEX = /attr\.([^\]]+)/; -function getStylingPrefix(propName: string): string { - return propName.substring(0, 5).toLowerCase(); +function getStylingPrefix(name: string): string { + return name.substring(0, 5); // style or class } function baseDirectiveFields( @@ -672,14 +672,9 @@ function createHostBindingsFunction( const bindings = bindingParser.createBoundHostProperties(directiveSummary, hostBindingSourceSpan); (bindings || []).forEach((binding: ParsedProperty) => { const name = binding.name; - const stylePrefix = getStylingPrefix(name); - if (stylePrefix === 'style') { - const {propertyName, unit} = parseNamedProperty(name); - styleBuilder.registerStyleInput(propertyName, binding.expression, unit, binding.sourceSpan); - } else if (stylePrefix === 'class') { - styleBuilder.registerClassInput( - parseNamedProperty(name).propertyName, binding.expression, binding.sourceSpan); - } else { + const stylingInputWasSet = + styleBuilder.registerInputBasedOnName(name, binding.expression, binding.sourceSpan); + if (!stylingInputWasSet) { // resolve literal arrays and literal objects const value = binding.expression.visit(getValueConverter()); const bindingExpr = bindingFn(bindingContext, value); @@ -923,19 +918,3 @@ function compileStyles(styles: string[], selector: string, hostSelector: string) const shadowCss = new ShadowCss(); return styles.map(style => { return shadowCss !.shimCssText(style, selector, hostSelector); }); } - -function parseNamedProperty(name: string): {propertyName: string, unit: string} { - let unit = ''; - let propertyName = ''; - const index = name.indexOf('.'); - if (index > 0) { - const unitIndex = name.lastIndexOf('.'); - if (unitIndex !== index) { - unit = name.substring(unitIndex + 1, name.length); - propertyName = name.substring(index + 1, unitIndex); - } else { - propertyName = name.substring(index + 1, name.length); - } - } - return {propertyName, unit}; -} diff --git a/packages/compiler/src/render3/view/styling_builder.ts b/packages/compiler/src/render3/view/styling_builder.ts index 0b2c322f91..774214a062 100644 --- a/packages/compiler/src/render3/view/styling_builder.ts +++ b/packages/compiler/src/render3/view/styling_builder.ts @@ -16,6 +16,7 @@ import {Identifiers as R3} from '../r3_identifiers'; import {parse as parseStyle} from './style_parser'; import {ValueConverter} from './template'; +const IMPORTANT_FLAG = '!important'; /** * A styling expression summary that is to be processed by the compiler @@ -31,7 +32,8 @@ export interface Instruction { * An internal record of the input data for a styling binding */ interface BoundStylingEntry { - name: string; + hasOverrideFlag: boolean; + name: string|null; unit: string|null; sourceSpan: ParseSourceSpan; value: AST; @@ -123,51 +125,70 @@ export class StylingBuilder { // will therefore skip all style/class resolution that is present // with style="", [style]="" and [style.prop]="", class="", // [class.prop]="". [class]="" assignments - const name = input.name; let binding: BoundStylingEntry|null = null; + let name = input.name; switch (input.type) { case BindingType.Property: - if (name == 'style') { - binding = this.registerStyleInput(null, input.value, '', input.sourceSpan); - } else if (isClassBinding(input.name)) { - binding = this.registerClassInput(null, input.value, input.sourceSpan); - } + binding = this.registerInputBasedOnName(name, input.value, input.sourceSpan); break; case BindingType.Style: - binding = this.registerStyleInput(input.name, input.value, input.unit, input.sourceSpan); + binding = this.registerStyleInput(name, false, input.value, input.sourceSpan, input.unit); break; case BindingType.Class: - binding = this.registerClassInput(input.name, input.value, input.sourceSpan); + binding = this.registerClassInput(name, false, input.value, input.sourceSpan); break; } return binding ? true : false; } + registerInputBasedOnName(name: string, expression: AST, sourceSpan: ParseSourceSpan) { + let binding: BoundStylingEntry|null = null; + const nameToMatch = name.substring(0, 5); // class | style + const isStyle = nameToMatch === 'style'; + const isClass = isStyle ? false : (nameToMatch === 'class'); + if (isStyle || isClass) { + const isMapBased = name.charAt(5) !== '.'; // style.prop or class.prop makes this a no + const property = name.substr(isMapBased ? 5 : 6); // the dot explains why there's a +1 + if (isStyle) { + binding = this.registerStyleInput(property, isMapBased, expression, sourceSpan); + } else { + binding = this.registerClassInput(property, isMapBased, expression, sourceSpan); + } + } + return binding; + } + registerStyleInput( - propertyName: string|null, value: AST, unit: string|null, - sourceSpan: ParseSourceSpan): BoundStylingEntry { - const entry = { name: propertyName, unit, value, sourceSpan } as BoundStylingEntry; - if (propertyName) { - (this._singleStyleInputs = this._singleStyleInputs || []).push(entry); - this._useDefaultSanitizer = this._useDefaultSanitizer || isStyleSanitizable(propertyName); - registerIntoMap(this._stylesIndex, propertyName); - } else { + name: string, isMapBased: boolean, value: AST, sourceSpan: ParseSourceSpan, + unit?: string|null): BoundStylingEntry { + const {property, hasOverrideFlag, unit: bindingUnit} = parseProperty(name); + const entry: BoundStylingEntry = { + name: property, + unit: unit || bindingUnit, value, sourceSpan, hasOverrideFlag + }; + if (isMapBased) { this._useDefaultSanitizer = true; this._styleMapInput = entry; + } else { + (this._singleStyleInputs = this._singleStyleInputs || []).push(entry); + this._useDefaultSanitizer = this._useDefaultSanitizer || isStyleSanitizable(name); + registerIntoMap(this._stylesIndex, property); } this._lastStylingInput = entry; this.hasBindings = true; return entry; } - registerClassInput(className: string|null, value: AST, sourceSpan: ParseSourceSpan): + registerClassInput(name: string, isMapBased: boolean, value: AST, sourceSpan: ParseSourceSpan): BoundStylingEntry { - const entry = { name: className, value, sourceSpan } as BoundStylingEntry; - if (className) { - (this._singleClassInputs = this._singleClassInputs || []).push(entry); - registerIntoMap(this._classesIndex, className); - } else { + const {property, hasOverrideFlag} = parseProperty(name); + const entry: + BoundStylingEntry = {name: property, value, sourceSpan, hasOverrideFlag, unit: null}; + if (isMapBased) { this._classMapInput = entry; + } else { + (this._singleClassInputs = this._singleClassInputs || []).push(entry); + registerIntoMap(this._classesIndex, property); } this._lastStylingInput = entry; this.hasBindings = true; @@ -235,6 +256,7 @@ export class StylingBuilder { reference: R3.elementHostAttrs, allocateBindingSlots: 0, buildParams: () => { + // params => elementHostAttrs(directive, attrs) this.populateInitialStylingAttrs(attrs); return [this._directiveExpr !, getConstantLiteralFromArray(constantPool, attrs)]; } @@ -337,24 +359,26 @@ export class StylingBuilder { reference: R3.elementStylingMap, allocateBindingSlots: totalBindingSlotsRequired, buildParams: (convertFn: (value: any) => o.Expression) => { - const params: o.Expression[] = [this._elementIndexExpr]; - - if (mapBasedClassValue) { - params.push(convertFn(mapBasedClassValue)); - } else if (this._styleMapInput) { - params.push(o.NULL_EXPR); - } - - if (mapBasedStyleValue) { - params.push(convertFn(mapBasedStyleValue)); - } else if (this._directiveExpr) { - params.push(o.NULL_EXPR); - } - + // min params => elementStylingMap(index, classMap) + // max params => elementStylingMap(index, classMap, styleMap, directive) + let expectedNumberOfArgs = 0; if (this._directiveExpr) { - params.push(this._directiveExpr); + expectedNumberOfArgs = 4; + } else if (mapBasedStyleValue) { + expectedNumberOfArgs = 3; + } else if (mapBasedClassValue) { + // index and class = 2 + expectedNumberOfArgs = 2; } + const params: o.Expression[] = [this._elementIndexExpr]; + addParam( + params, mapBasedClassValue, mapBasedClassValue ? convertFn(mapBasedClassValue) : null, + 2, expectedNumberOfArgs); + addParam( + params, mapBasedStyleValue, mapBasedStyleValue ? convertFn(mapBasedStyleValue) : null, + 3, expectedNumberOfArgs); + addParam(params, this._directiveExpr, this._directiveExpr, 4, expectedNumberOfArgs); return params; } }; @@ -367,14 +391,18 @@ export class StylingBuilder { allowUnits: boolean, valueConverter: ValueConverter): Instruction[] { let totalBindingSlotsRequired = 0; return inputs.map(input => { - const bindingIndex: number = mapIndex.get(input.name) !; + const bindingIndex: number = mapIndex.get(input.name !) !; const value = input.value.visit(valueConverter); totalBindingSlotsRequired += (value instanceof Interpolation) ? value.expressions.length : 0; return { sourceSpan: input.sourceSpan, allocateBindingSlots: totalBindingSlotsRequired, reference, buildParams: (convertFn: (value: any) => o.Expression) => { + // min params => elementStlyingProp(elmIndex, bindingIndex, value) + // max params => elementStlyingProp(elmIndex, bindingIndex, value, overrideFlag) + const params = [this._elementIndexExpr, o.literal(bindingIndex), convertFn(value)]; + if (allowUnits) { if (input.unit) { params.push(o.literal(input.unit)); @@ -385,7 +413,14 @@ export class StylingBuilder { if (this._directiveExpr) { params.push(this._directiveExpr); + } else if (input.hasOverrideFlag) { + params.push(o.NULL_EXPR); } + + if (input.hasOverrideFlag) { + params.push(o.literal(true)); + } + return params; } }; @@ -414,6 +449,8 @@ export class StylingBuilder { reference: R3.elementStylingApply, allocateBindingSlots: 0, buildParams: () => { + // min params => elementStylingApply(elmIndex) + // max params => elementStylingApply(elmIndex, directive) const params: o.Expression[] = [this._elementIndexExpr]; if (this._directiveExpr) { params.push(this._directiveExpr); @@ -442,10 +479,6 @@ export class StylingBuilder { } } -function isClassBinding(name: string): boolean { - return name == 'className' || name == 'class'; -} - function registerIntoMap(map: Map, key: string) { if (!map.has(key)) { map.set(key, map.size); @@ -471,11 +504,31 @@ function getConstantLiteralFromArray( * predicate and totalExpectedArgs values */ function addParam( - params: o.Expression[], predicate: boolean, value: o.Expression, argNumber: number, + params: o.Expression[], predicate: any, value: o.Expression | null, argNumber: number, totalExpectedArgs: number) { - if (predicate) { + if (predicate && value) { params.push(value); } else if (argNumber < totalExpectedArgs) { params.push(o.NULL_EXPR); } } + +export function parseProperty(name: string): + {property: string, unit: string, hasOverrideFlag: boolean} { + let hasOverrideFlag = false; + const overrideIndex = name.indexOf(IMPORTANT_FLAG); + if (overrideIndex !== -1) { + name = overrideIndex > 0 ? name.substring(0, overrideIndex) : ''; + hasOverrideFlag = true; + } + + let unit = ''; + let property = name; + const unitIndex = name.lastIndexOf('.'); + if (unitIndex > 0) { + unit = name.substr(unitIndex + 1); + property = name.substring(0, unitIndex); + } + + return {property, unit, hasOverrideFlag}; +} diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts index 2483fa571c..3563b8718f 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -549,7 +549,8 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver const allOtherInputs: t.BoundAttribute[] = []; element.inputs.forEach((input: t.BoundAttribute) => { - if (!stylingBuilder.registerBoundInput(input)) { + const stylingInputWasSet = stylingBuilder.registerBoundInput(input); + if (!stylingInputWasSet) { if (input.type === BindingType.Property) { if (input.i18n) { i18nAttrs.push(input); diff --git a/packages/compiler/src/template_parser/template_parser.ts b/packages/compiler/src/template_parser/template_parser.ts index a646856cc8..0b71697782 100644 --- a/packages/compiler/src/template_parser/template_parser.ts +++ b/packages/compiler/src/template_parser/template_parser.ts @@ -897,4 +897,4 @@ function isEmptyExpression(ast: AST): boolean { ast = ast.ast; } return ast instanceof EmptyExpr; -} \ No newline at end of file +} diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index f65583b0e2..ccf59d8c32 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -37,9 +37,9 @@ import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert'; import {appendChild, appendProjectedNode, createTextNode, getLViewChild, insertView, removeView} from './node_manipulation'; import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher'; import {decreaseElementDepthCount, enterView, getBindingsEnabled, getCheckNoChangesMode, getContextLView, getCurrentDirectiveDef, getElementDepthCount, getIsParent, getLView, getPreviousOrParentTNode, increaseElementDepthCount, isCreationMode, leaveView, nextContextImpl, resetComponentState, setBindingRoot, setCheckNoChangesMode, setCurrentDirectiveDef, setCurrentQueryIndex, setIsParent, setPreviousOrParentTNode} from './state'; -import {getInitialClassNameValue, initializeStaticContext as initializeStaticStylingContext, patchContextWithStaticAttrs, renderInitialStylesAndClasses, renderStyling, updateClassProp as updateElementClassProp, updateContextWithBindings, updateStyleProp as updateElementStyleProp, updateStylingMap} from './styling/class_and_style_bindings'; +import {getInitialClassNameValue, getInitialStyleStringValue, initializeStaticContext as initializeStaticStylingContext, patchContextWithStaticAttrs, renderInitialClasses, renderInitialStyles, renderStyling, updateClassProp as updateElementClassProp, updateContextWithBindings, updateStyleProp as updateElementStyleProp, updateStylingMap} from './styling/class_and_style_bindings'; import {BoundPlayerFactory} from './styling/player_factory'; -import {ANIMATION_PROP_PREFIX, createEmptyStylingContext, getStylingContext, hasClassInput, hasStyling, isAnimationProp} from './styling/util'; +import {ANIMATION_PROP_PREFIX, allocateDirectiveIntoContext, createEmptyStylingContext, forceClassesAsString, forceStylesAsString, getStylingContext, hasClassInput, hasStyleInput, hasStyling, isAnimationProp} from './styling/util'; import {NO_CHANGE} from './tokens'; import {INTERPOLATION_DELIMITER, findComponentView, getComponentViewByIndex, getNativeByIndex, getNativeByTNode, getRootContext, getRootView, getTNode, isComponent, isComponentDef, isContentQueryHost, loadInternal, readElementValue, readPatchedLView, renderStringify} from './util'; @@ -633,12 +633,16 @@ export function elementStart( if (inputData && inputData.hasOwnProperty('class')) { tNode.flags |= TNodeFlags.hasClassInput; } + if (inputData && inputData.hasOwnProperty('style')) { + tNode.flags |= TNodeFlags.hasStyleInput; + } } // There is no point in rendering styles when a class directive is present since // it will take that over for us (this will be removed once #FW-882 is in). - if (tNode.stylingTemplate && (tNode.flags & TNodeFlags.hasClassInput) === 0) { - renderInitialStylesAndClasses(native, tNode.stylingTemplate, lView[RENDERER]); + if (tNode.stylingTemplate) { + renderInitialClasses(native, tNode.stylingTemplate, lView[RENDERER]); + renderInitialStyles(native, tNode.stylingTemplate, lView[RENDERER]); } const currentQueries = lView[QUERIES]; @@ -1072,6 +1076,20 @@ export function elementEnd(): void { previousOrParentTNode = previousOrParentTNode.parent !; setPreviousOrParentTNode(previousOrParentTNode); } + + // there may be some instructions that need to run in a specific + // order because the CREATE block in a directive runs before the + // CREATE block in a template. To work around this instructions + // can get access to the function array below and defer any code + // to run after the element is created. + let fns: Function[]|null; + if (fns = previousOrParentTNode.onElementCreationFns) { + for (let i = 0; i < fns.length; i++) { + fns[i](); + } + previousOrParentTNode.onElementCreationFns = null; + } + ngDevMode && assertNodeType(previousOrParentTNode, TNodeType.Element); const lView = getLView(); const currentQueries = lView[QUERIES]; @@ -1090,6 +1108,12 @@ export function elementEnd(): void { setInputsForProperty( lView, previousOrParentTNode.inputs !['class'] !, getInitialClassNameValue(stylingContext)); } + if (hasStyleInput(previousOrParentTNode)) { + const stylingContext = getStylingContext(previousOrParentTNode.index, lView); + setInputsForProperty( + lView, previousOrParentTNode.inputs !['style'] !, + getInitialStyleStringValue(stylingContext)); + } } /** @@ -1297,7 +1321,8 @@ export function createTNode( child: null, parent: tParent, stylingTemplate: null, - projection: null + projection: null, + onElementCreationFns: null, }; } @@ -1412,9 +1437,33 @@ export function elementStyling( if (!tNode.stylingTemplate) { tNode.stylingTemplate = createEmptyStylingContext(); } + + if (directive) { + // this will ALWAYS happen first before the bindings are applied so that the ordering + // of directives is correct (otherwise if a follow-up directive contains static styling, + // which is applied through elementHostAttrs, then it may end up being listed in the + // context directive array before a former one (because the former one didn't contain + // any static styling values)) + allocateDirectiveIntoContext(tNode.stylingTemplate, directive); + + const fns = tNode.onElementCreationFns = tNode.onElementCreationFns || []; + fns.push( + () => initElementStyling( + tNode, classBindingNames, styleBindingNames, styleSanitizer, directive)); + } else { + // this will make sure that the root directive (the template) will always be + // run FIRST before all the other styling properties are populated into the + // context... + initElementStyling(tNode, classBindingNames, styleBindingNames, styleSanitizer, directive); + } +} + +function initElementStyling( + tNode: TNode, classBindingNames?: string[] | null, styleBindingNames?: string[] | null, + styleSanitizer?: StyleSanitizeFn | null, directive?: {}): void { updateContextWithBindings( tNode.stylingTemplate !, directive || null, classBindingNames, styleBindingNames, - styleSanitizer, hasClassInput(tNode)); + styleSanitizer); } /** @@ -1521,7 +1570,7 @@ components */ export function elementStyleProp( index: number, styleIndex: number, value: string | number | String | PlayerFactory | null, - suffix?: string | null, directive?: {}): void { + suffix?: string | null, directive?: {}, forceOverride?: boolean): void { let valueToAdd: string|null = null; if (value !== null) { if (suffix) { @@ -1537,7 +1586,8 @@ export function elementStyleProp( } } updateElementStyleProp( - getStylingContext(index + HEADER_OFFSET, getLView()), styleIndex, valueToAdd, directive); + getStylingContext(index + HEADER_OFFSET, getLView()), styleIndex, valueToAdd, directive, + forceOverride); } /** @@ -1555,26 +1605,36 @@ export function elementStyleProp( * @param value A true/false value which will turn the class on or off. * @param directive Directive instance that is attempting to change styling. (Defaults to the * component of the current view). -components + * @param forceOverride Whether or not this value will be applied regardless of where it is being + * set within the directive priority structure. * * @publicApi */ export function elementClassProp( - index: number, classIndex: number, value: boolean | PlayerFactory, directive?: {}): void { - const onOrOffClassValue = - (value instanceof BoundPlayerFactory) ? (value as BoundPlayerFactory) : (!!value); + index: number, classIndex: number, value: boolean | PlayerFactory, directive?: {}, + forceOverride?: boolean): void { + const input = (value instanceof BoundPlayerFactory) ? + (value as BoundPlayerFactory) : + booleanOrNull(value); updateElementClassProp( - getStylingContext(index + HEADER_OFFSET, getLView()), classIndex, onOrOffClassValue, - directive); + getStylingContext(index + HEADER_OFFSET, getLView()), classIndex, input, directive, + forceOverride); +} + +function booleanOrNull(value: any): boolean|null { + if (typeof value === 'boolean') return value; + return value ? true : null; } /** * Update style and/or class bindings using object literal. * * This instruction is meant apply styling via the `[style]="exp"` and `[class]="exp"` template - * bindings. When styles are applied to the Element they will then be placed with respect to + * bindings. When styles are applied to the element they will then be placed with respect to * any styles set with `elementStyleProp`. If any styles are set to `null` then they will be - * removed from the element. + * removed from the element. This instruction is also called for host bindings that write to + * `[style]` and `[class]` (the directive param helps the instruction code determine where the + * binding values come from). * * (Note that the styling instruction will not be applied until `elementStylingApply` is called.) * @@ -1593,29 +1653,33 @@ export function elementClassProp( export function elementStylingMap( index: number, classes: {[key: string]: any} | string | NO_CHANGE | null, styles?: {[styleName: string]: any} | NO_CHANGE | null, directive?: {}): void { - if (directive != undefined) - return hackImplementationOfElementStylingMap( - index, classes, styles, directive); // supported in next PR const lView = getLView(); const tNode = getTNode(index, lView); const stylingContext = getStylingContext(index + HEADER_OFFSET, lView); - if (hasClassInput(tNode) && classes !== NO_CHANGE) { - const initialClasses = getInitialClassNameValue(stylingContext); - const classInputVal = - (initialClasses.length ? (initialClasses + ' ') : '') + (classes as string); - setInputsForProperty(lView, tNode.inputs !['class'] !, classInputVal); - } else { - updateStylingMap(stylingContext, classes, styles); - } -} -/* START OF HACK BLOCK */ -function hackImplementationOfElementStylingMap( - index: number, classes: {[key: string]: any} | string | NO_CHANGE | null, - styles?: {[styleName: string]: any} | NO_CHANGE | null, directive?: {}): void { - throw new Error('unimplemented. Should not be needed by ViewEngine compatibility'); + // inputs are only evaluated from a template binding into a directive, therefore, + // there should not be a situation where a directive host bindings function + // evaluates the inputs (this should only happen in the template function) + if (!directive) { + if (hasClassInput(tNode) && classes !== NO_CHANGE) { + const initialClasses = getInitialClassNameValue(stylingContext); + const classInputVal = + (initialClasses.length ? (initialClasses + ' ') : '') + forceClassesAsString(classes); + setInputsForProperty(lView, tNode.inputs !['class'] !, classInputVal); + classes = NO_CHANGE; + } + + if (hasStyleInput(tNode) && styles !== NO_CHANGE) { + const initialStyles = getInitialClassNameValue(stylingContext); + const styleInputVal = + (initialStyles.length ? (initialStyles + ' ') : '') + forceStylesAsString(styles); + setInputsForProperty(lView, tNode.inputs !['style'] !, styleInputVal); + styles = NO_CHANGE; + } + } + + updateStylingMap(stylingContext, classes, styles, directive); } -/* END OF HACK BLOCK */ ////////////////////////// //// Text diff --git a/packages/core/src/render3/interfaces/node.ts b/packages/core/src/render3/interfaces/node.ts index ed0ee4786a..4f8ac12766 100644 --- a/packages/core/src/render3/interfaces/node.ts +++ b/packages/core/src/render3/interfaces/node.ts @@ -30,16 +30,19 @@ export const enum TNodeType { */ export const enum TNodeFlags { /** This bit is set if the node is a component */ - isComponent = 0b0001, + isComponent = 0b00001, /** This bit is set if the node has been projected */ - isProjected = 0b0010, + isProjected = 0b00010, /** This bit is set if any directive on this node has content queries */ - hasContentQuery = 0b0100, + hasContentQuery = 0b00100, - /** This bit is set if the node has any directives that contain [class properties */ - hasClassInput = 0b1000, + /** This bit is set if the node has any "class" inputs */ + hasClassInput = 0b01000, + + /** This bit is set if the node has any "style" inputs */ + hasStyleInput = 0b10000, } /** @@ -182,7 +185,7 @@ export interface TNode { propertyMetadataEndIndex: number; /** - * Stores if Node isComponent, isProjected, hasContentQuery and hasClassInput + * Stores if Node isComponent, isProjected, hasContentQuery, hasClassInput and hasStyleInput */ flags: TNodeFlags; @@ -345,6 +348,19 @@ export interface TNode { * projectable nodes during dynamic component creation. */ projection: (TNode|RNode[])[]|number|null; + + /** + * A buffer of functions that will be called once `elementEnd` (or `element`) completes. + * + * Due to the nature of how directives work in Angular, some directive code may + * need to fire after any template-level code runs. If present, this array will + * be flushed (each function will be invoked) once the associated element is + * created. + * + * If an element is created multiple times then this function will be populated + * with functions each time the creation block is called. + */ + onElementCreationFns: Function[]|null; } /** Static data for an element */ diff --git a/packages/core/src/render3/interfaces/styling.ts b/packages/core/src/render3/interfaces/styling.ts index fa2091b342..c8d90677da 100644 --- a/packages/core/src/render3/interfaces/styling.ts +++ b/packages/core/src/render3/interfaces/styling.ts @@ -30,7 +30,7 @@ import {PlayerContext} from './player'; * * Say for example we have this: * ``` - * *
* ``` @@ -44,9 +44,9 @@ import {PlayerContext} from './player'; * 1. elementStart or element (within the template function of a component) * 2. elementHostAttrs (for directive host bindings) * - * In either case, a styling context will be created and stored within an element's LViewData. Once - * the styling context is created then single and multi properties can stored within it. For this to - * happen, the following function needs to be called: + * In either case, a styling context will be created and stored within an element's `LViewData`. + * Once the styling context is created then single and multi properties can be stored within it. + * For this to happen, the following function needs to be called: * * `elementStyling` (called with style properties, class properties and a sanitizer + a directive * instance). @@ -73,8 +73,8 @@ import {PlayerContext} from './player'; * * The context generated from these values will look like this (note that * for each binding name (the class and style bindings) the values will - * be inserted twice into the array (once for single property entries) and - * another for multi property entries). + * be inserted twice into the array (once for single property entries and + * again for multi property entries). * * context = [ * // 0-8: header values (about 8 entries of configuration data) @@ -147,9 +147,10 @@ import {PlayerContext} from './player'; * have changed. * * ## Directives - * Directives style values (which are provided through host bindings) are also supported and - * housed within the same styling context as are template-level style/class properties/bindings. - * Both directive-level and template-level styling bindings share the same context. + * Directive style/class values (which are provided through host bindings) are also supported and + * housed within the same styling context as are template-level style/class properties/bindings + * So long as they are all assigned to the same element, both directive-level and template-level + * styling bindings share the same context. * * Each of the following instructions supports accepting a directive instance as an input parameter: * @@ -160,22 +161,40 @@ import {PlayerContext} from './player'; * - `elementStylingMap` * - `elementStylingApply` * - * Each time a directiveRef is passed in, it will be converted into an index by examining the + * Each time a directive value is passed in, it will be converted into an index by examining the * directive registry (which lives in the context configuration area). The index is then used * to help single style properties figure out where a value is located in the context. * + * + * ## Single-level styling bindings (`[style.prop]` and `[class.name]`) + * + * Both `[style.prop]` and `[class.name]` bindings are run through the `updateStyleProp` + * and `updateClassProp` functions respectively. They work by examining the provided + * `offset` value and are able to locate the exact spot in the context where the + * matching style is located. + * + * Both `[style.prop]` and `[class.name]` bindings are able to process these values + * from directive host bindings. When evaluated (from the host binding function) the + * `directiveRef` value is then passed in. + * * If two directives or a directive + a template binding both write to the same style/class * binding then the styling context code will decide which one wins based on the following * rule: * * 1. If the template binding has a value then it always wins - * 2. If not then whichever first-registered directive that has that value first will win + * 2. Otherwise whichever first-registered directive that has that value first will win * * The code example helps make this clear: * * ``` - *
- * @Directive({ selector: '[my-width-directive' ]}) + * + * + * @Directive({ + * selector: '[my-width-directive'] + * }) * class MyWidthDirective { * @Input('my-width-directive') * @HostBinding('style.width') @@ -187,7 +206,8 @@ import {PlayerContext} from './player'; * it will always win over the width binding that is present as a host binding within * the `MyWidthDirective`. However, if `[style.width]` renders as `null` (so `myWidth=null`) * then the `MyWidthDirective` will be able to write to the `width` style within the context. - * Simply put, whichever directive writes to a value ends up having ownership of it. + * Simply put, whichever directive writes to a value first ends up having ownership of it as + * long as the template didn't set anything. * * The way in which the ownership is facilitated is through index value. The earliest directives * get the smallest index values (with 0 being reserved for the template element bindings). Each @@ -195,10 +215,48 @@ import {PlayerContext} from './player'; * assigned the directive index value in its data. If another directive writes a value again then * its directive index gets compared against the directive index that exists on the element. Only * when the new value's directive index is less than the existing directive index then the new - * value will be written to the context. + * value will be written to the context. But, if the existing value is null then the new value is + * written by the less important directive. * * Each directive also has its own sanitizer and dirty flags. These values are consumed within the * rendering function. + * + * + * ## Multi-level styling bindings (`[style]` and `[class]`) + * + * Multi-level styling bindings are treated as less important (less specific) as single-level + * bindings (things like `[style.prop]` and `[class.name]`). + * + * Multi-level bindings are still applied to the context in a similar way as are single level + * bindings, but this process works by diffing the new multi-level values (which are key/value + * maps) against the existing set of styles that live in the context. Each time a new map value + * is detected (via identity check) then it will loop through the values and figure out what + * has changed and reorder the context array to match the ordering of the keys. This reordering + * of the context makes sure that follow-up traversals of the context when updated against the + * key/value map are as close as possible to o(n) (where "n" is the size of the key/value map). + * + * If a `directiveRef` value is passed in then the styling algorithm code will take the directive's + * prioritization index into account and update the values with respect to more important + * directives. This means that if a value such as `width` is updated in two different `[style]` + * bindings (say one on the template and another within a directive that sits on the same element) + * then the algorithm will decide how to update the value based on the following heuristic: + * + * 1. If the template binding has a value then it always wins + * 2. If not then whichever first-registered directive that has that value first will win + * + * It will also update the value if it was set to `null` by a previous directive (or the template). + * + * Each time a value is updated (or removed) then the context will change shape to better match + * the ordering of the styling data as well as the ordering of each directive that contains styling + * data. (See `patchStylingMapIntoContext` inside of class_and_style_bindings.ts to better + * understand how this works.) + * + * ## Rendering + * The rendering mechanism (when the styling data is applied on screen) occurs via the + * `elementStylingApply` function and is designed to run after **all** styling functions have been + * evaluated. The rendering algorithm will loop over the context and only apply the styles that are + * flagged as dirty (either because they are new, updated or have been removed via multi or + * single bindings). */ export interface StylingContext extends Array<{[key: string]: any}|number|string|boolean|RElement|StyleSanitizeFn|PlayerContext|null> { @@ -240,13 +298,13 @@ export interface StylingContext extends * The last class value that was interpreted by elementStylingMap. This is cached * So that the algorithm can exit early incase the value has not changed. */ - [StylingIndex.CachedClassValueOrInitialClassString]: {[key: string]: any}|string|(string)[]|null; + [StylingIndex.CachedMultiClasses]: any|MapBasedOffsetValues; /** * The last style value that was interpreted by elementStylingMap. This is cached * So that the algorithm can exit early incase the value has not changed. */ - [StylingIndex.CachedStyleValue]: {[key: string]: any}|(string)[]|null; + [StylingIndex.CachedMultiStyles]: any|MapBasedOffsetValues; /** * Location of animation context (which contains the active players) for this element styling @@ -262,7 +320,10 @@ export interface StylingContext extends * * See [InitialStylingValuesIndex] for a breakdown of how all this works. */ -export interface InitialStylingValues extends Array { [0]: null; } +export interface InitialStylingValues extends Array { + [InitialStylingValuesIndex.DefaultNullValuePosition]: null; + [InitialStylingValuesIndex.InitialClassesStringPosition]: string|null; +} /** * Used as an offset/position index to figure out where initial styling @@ -270,13 +331,16 @@ export interface InitialStylingValues extends Array { [0]: * * Used as a reference point to provide markers to all static styling * values (the initial style and class values on an element) within an - * array within the StylingContext. This array contains key/value pairs + * array within the `StylingContext`. This array contains key/value pairs * where the key is the style property name or className and the value is * the style value or whether or not a class is present on the elment. * - * The first value is also always null so that a initial index value of + * The first value is always null so that a initial index value of * `0` will always point to a null value. * + * The second value is also always null unless a string-based representation + * of the styling data was constructed (it gets cached in this slot). + * * If a
elements contains a list of static styling values like so: * *
@@ -284,14 +348,18 @@ export interface InitialStylingValues extends Array { [0]: * Then the initial styles for that will look like so: * * Styles: + * ``` * StylingContext[InitialStylesIndex] = [ - * null, 'width', '100px', height, '200px' + * null, null, 'width', '100px', height, '200px' * ] + * ``` * * Classes: - * StylingContext[InitialStylesIndex] = [ - * null, 'foo', true, 'bar', true, 'baz', true + * ``` + * StylingContext[InitialClassesIndex] = [ + * null, null, 'foo', true, 'bar', true, 'baz', true * ] + * ``` * * Initial style and class entries have their own arrays. This is because * it's easier to add to the end of one array and not then have to update @@ -300,26 +368,32 @@ export interface InitialStylingValues extends Array { [0]: * When property bindinds are added to a context then initial style/class * values will also be inserted into the array. This is to create a space * in the situation when a follow-up directive inserts static styling into - * the array. By default style values are `null` and class values are + * the array. By default, style values are `null` and class values are * `false` when inserted by property bindings. * * For example: + * ``` *
+ * ``` * * Will construct initial styling values that look like: * * Styles: + * ``` * StylingContext[InitialStylesIndex] = [ - * null, 'width', '100px', height, '200px', 'opacity', null + * null, null, 'width', '100px', height, '200px', 'opacity', null * ] + * ``` * * Classes: - * StylingContext[InitialStylesIndex] = [ - * null, 'foo', true, 'bar', true, 'baz', true, 'car', false + * ``` + * StylingContext[InitialClassesIndex] = [ + * null, null, 'foo', true, 'bar', true, 'baz', true, 'car', false * ] + * ``` * * Now if a directive comes along and introduces `car` as a static * class value or `opacity` then those values will be filled into @@ -327,6 +401,7 @@ export interface InitialStylingValues extends Array { [0]: * * For example: * + * ``` * @Directive({ * selector: 'opacity-car-directive', * host: { @@ -335,21 +410,28 @@ export interface InitialStylingValues extends Array { [0]: * } * }) * class OpacityCarDirective {} + * ``` * * This will render itself as: * * Styles: + * ``` * StylingContext[InitialStylesIndex] = [ - * null, 'width', '100px', height, '200px', 'opacity', null + * null, null, 'width', '100px', height, '200px', 'opacity', '0.5' * ] + * ``` * * Classes: - * StylingContext[InitialStylesIndex] = [ - * null, 'foo', true, 'bar', true, 'baz', true, 'car', false + * ``` + * StylingContext[InitialClassesIndex] = [ + * null, null, 'foo', true, 'bar', true, 'baz', true, 'car', true * ] + * ``` */ export const enum InitialStylingValuesIndex { - KeyValueStartPosition = 1, + DefaultNullValuePosition = 0, + InitialClassesStringPosition = 1, + KeyValueStartPosition = 2, PropOffset = 0, ValueOffset = 1, Size = 2 @@ -361,28 +443,27 @@ export const enum InitialStylingValuesIndex { * * Each entry in this array represents a source of where style/class binding values could * come from. By default, there is always at least one directive here with a null value and - * that represents bindings that live directly on an element (not host bindings). + * that represents bindings that live directly on an element in the template (not host bindings). * - * Each successive entry in the array is an actual instance of an array as well as some - * additional info. + * Each successive entry in the array is an actual instance of a directive as well as some + * additional info about that entry. * * An entry within this array has the following values: - * [0] = The instance of the directive (or null when it is not a directive, but a template binding - * source) + * [0] = The instance of the directive (the first entry is null because its reserved for the + * template) * [1] = The pointer that tells where the single styling (stuff like [class.foo] and [style.prop]) * offset values are located. This value will allow for a binding instruction to find exactly * where a style is located. * [2] = Whether or not the directive has any styling values that are dirty. This is used as - * reference within the renderClassAndStyleBindings function to decide whether to skip - * iterating through the context when rendering is executed. + * reference within the `renderStyling` function to decide whether to skip iterating + * through the context when rendering is executed. * [3] = The styleSanitizer instance that is assigned to the directive. Although it's unlikely, * a directive could introduce its own special style sanitizer and for this reach each * directive will get its own space for it (if null then the very first sanitizer is used). * * Each time a new directive is added it will insert these four values at the end of the array. - * When this array is examined (using indexOf) then the resulting directiveIndex will be resolved - * by dividing the index value by the size of the array entries (so if DirA is at spot 8 then its - * index will be 2). + * When this array is examined then the resulting directiveIndex will be resolved by dividing the + * index value by the size of the array entries (so if DirA is at spot 8 then its index will be 2). */ export interface DirectiveRegistryValues extends Array { [DirectiveRegistryValuesIndex.DirectiveValueOffset]: null; @@ -441,29 +522,94 @@ export const enum SinglePropOffsetValuesIndex { ValueStartPosition = 2 } +/** + * Used a reference for all multi styling values (values that are assigned via the + * `[style]` and `[class]` bindings). + * + * Single-styling properties (things set via `[style.prop]` and `[class.name]` bindings) + * are not handled using the same approach as multi-styling bindings (such as `[style]` + * `[class]` bindings). + * + * Multi-styling bindings rely on a diffing algorithm to figure out what properties have been added, + * removed and modified. Multi-styling properties are also evaluated across directives--which means + * that Angular supports having multiple directives all write to the same `[style]` and `[class]` + * bindings (using host bindings) even if the `[style]` and/or `[class]` bindings are being written + * to on the template element. + * + * All multi-styling values that are written to an element (whether it be from the template or any + * directives attached to the element) are all written into the `MapBasedOffsetValues` array. (Note + * that there are two arrays: one for styles and another for classes.) + * + * This array is shaped in the following way: + * + * [0] = The total amount of unique multi-style or multi-class entries that exist currently in the + * context. + * [1+] = Contains an entry of four values ... Each entry is a value assigned by a + * `[style]`/`[class]` + * binding (we call this a **source**). + * + * An example entry looks like so (at a given `i` index): + * [i + 0] = Whether or not the value is dirty + * + * [i + 1] = The index of where the map-based values + * (for this **source**) start within the context + * + * [i + 2] = The untouched, last set value of the binding + * + * [i + 3] = The total amount of unqiue binding values that were + * extracted and set into the context. (Note that this value does + * not reflect the total amount of values within the binding + * value (since it's a map), but instead reflects the total values + * that were not used by another directive). + * + * Each time a directive (or template) writes a value to a `[class]`/`[style]` binding then the + * styling diffing algorithm code will decide whether or not to update the value based on the + * following rules: + * + * 1. If a more important directive (either the template or a directive that was registered + * beforehand) has written a specific styling value into the context then any follow-up styling + * values (set by another directive via its `[style]` and/or `[class]` host binding) will not be + * able to set it. This is because the former directive has priorty. + * 2. Only if a former directive has set a specific styling value to null (whether by actually + * setting it to null or not including it in is map value) then a less imporatant directive can + * set its own value. + * + * ## How the map-based styling algorithm updates itself + */ +export interface MapBasedOffsetValues extends Array { + [MapBasedOffsetValuesIndex.EntriesCountPosition]: number; +} + +export const enum MapBasedOffsetValuesIndex { + EntriesCountPosition = 0, + ValuesStartPosition = 1, + DirtyFlagOffset = 0, + PositionStartOffset = 1, + ValueOffset = 2, + ValueCountOffset = 3, + Size = 4 +} + /** * Used to set the context to be dirty or not both on the master flag (position 1) * or for each single/multi property that exists in the context. */ export const enum StylingFlags { // Implies no configurations - None = 0b000000, + None = 0b00000, // Whether or not the entry or context itself is dirty - Dirty = 0b000001, + Dirty = 0b00001, // Whether or not this is a class-based assignment - Class = 0b000010, + Class = 0b00010, // Whether or not a sanitizer was applied to this property - Sanitize = 0b000100, + Sanitize = 0b00100, // Whether or not any player builders within need to produce new players - PlayerBuildersDirty = 0b001000, - // If NgClass is present (or some other class handler) then it will handle the map expressions and - // initial classes - OnlyProcessSingleClasses = 0b010000, + PlayerBuildersDirty = 0b01000, // The max amount of bits used to represent these configuration values - BindingAllocationLocked = 0b100000, - BitCountSize = 6, - // There are only six bits here - BitMask = 0b111111 + BindingAllocationLocked = 0b10000, + BitCountSize = 5, + // There are only five bits here + BitMask = 0b11111 } /** Used as numeric pointer values to determine what cells to update in the `StylingContext` */ @@ -482,9 +628,9 @@ export const enum StylingIndex { ElementPosition = 5, // Position of where the last string-based CSS class value was stored (or a cached version of the // initial styles when a [class] directive is present) - CachedClassValueOrInitialClassString = 6, + CachedMultiClasses = 6, // Position of where the last string-based CSS class value was stored - CachedStyleValue = 7, + CachedMultiStyles = 7, // Multi and single entries are stored in `StylingContext` as: Flag; PropertyName; PropertyValue // Position of where the initial styles are stored in the styling context PlayerContext = 8, diff --git a/packages/core/src/render3/styling/class_and_style_bindings.ts b/packages/core/src/render3/styling/class_and_style_bindings.ts index e1d5325ea9..3123eceec4 100644 --- a/packages/core/src/render3/styling/class_and_style_bindings.ts +++ b/packages/core/src/render3/styling/class_and_style_bindings.ts @@ -11,13 +11,13 @@ import {EMPTY_ARRAY, EMPTY_OBJ} from '../empty'; import {AttributeMarker, TAttributes} from '../interfaces/node'; import {BindingStore, BindingType, Player, PlayerBuilder, PlayerFactory, PlayerIndex} from '../interfaces/player'; import {RElement, Renderer3, RendererStyleFlags3, isProceduralRenderer} from '../interfaces/renderer'; -import {DirectiveOwnerAndPlayerBuilderIndex, DirectiveRegistryValues, DirectiveRegistryValuesIndex, InitialStylingValues, InitialStylingValuesIndex, SinglePropOffsetValues, SinglePropOffsetValuesIndex, StylingContext, StylingFlags, StylingIndex} from '../interfaces/styling'; +import {DirectiveOwnerAndPlayerBuilderIndex, DirectiveRegistryValues, DirectiveRegistryValuesIndex, InitialStylingValues, InitialStylingValuesIndex, MapBasedOffsetValues, MapBasedOffsetValuesIndex, SinglePropOffsetValues, SinglePropOffsetValuesIndex, StylingContext, StylingFlags, StylingIndex} from '../interfaces/styling'; import {LView, RootContext} from '../interfaces/view'; import {NO_CHANGE} from '../tokens'; import {getRootContext} from '../util'; import {BoundPlayerFactory} from './player_factory'; -import {addPlayerInternal, allocPlayerContext, createEmptyStylingContext, getPlayerContext} from './util'; +import {addPlayerInternal, allocPlayerContext, allocateDirectiveIntoContext, createEmptyStylingContext, getPlayerContext} from './util'; @@ -30,8 +30,11 @@ import {addPlayerInternal, allocPlayerContext, createEmptyStylingContext, getPla * [style.prop]="myPropValue" * [class.name]="myClassValue" * + * It also includes code that will allow style binding code to operate within host + * bindings for components/directives. + * * There are many different ways in which these functions below are called. Please see - * `interfaces/styles.ts` to get a better idea of how the styling algorithm works. + * `render3/interfaces/styling.ts` to get a better idea of how the styling algorithm works. */ @@ -39,12 +42,12 @@ import {addPlayerInternal, allocPlayerContext, createEmptyStylingContext, getPla /** * Creates a new StylingContext an fills it with the provided static styling attribute values. */ -export function initializeStaticContext(attrs: TAttributes) { +export function initializeStaticContext(attrs: TAttributes): StylingContext { const context = createEmptyStylingContext(); const initialClasses: InitialStylingValues = context[StylingIndex.InitialClassValuesPosition] = - [null]; + [null, null]; const initialStyles: InitialStylingValues = context[StylingIndex.InitialStyleValuesPosition] = - [null]; + [null, null]; // The attributes array has marker values (numbers) indicating what the subsequent // values represent. When we encounter a number, we set the mode to that type of attribute. @@ -72,18 +75,18 @@ export function initializeStaticContext(attrs: TAttributes) { * @param context the existing styling context * @param attrs an array of new static styling attributes that will be * assigned to the context - * @param directive the directive instance with which static data is associated with. + * @param directiveRef the directive instance with which static data is associated with. */ export function patchContextWithStaticAttrs( - context: StylingContext, attrs: TAttributes, startingIndex: number, directive: any): void { + context: StylingContext, attrs: TAttributes, startingIndex: number, directiveRef: any): void { // If the styling context has already been patched with the given directive's bindings, // then there is no point in doing it again. The reason why this may happen (the directive // styling being patched twice) is because the `stylingBinding` function is called each time // an element is created (both within a template function and within directive host bindings). const directives = context[StylingIndex.DirectiveRegistryPosition]; - if (getDirectiveRegistryValuesIndexOf(directives, directive) == -1) { + if (getDirectiveRegistryValuesIndexOf(directives, directiveRef) == -1) { // this is a new directive which we have not seen yet. - directives.push(directive, -1, false, null); + allocateDirectiveIntoContext(context, directiveRef); let initialClasses: InitialStylingValues|null = null; let initialStyles: InitialStylingValues|null = null; @@ -134,16 +137,23 @@ function patchInitialStylingValue( } /** - * Runs through the initial styling data present in the context and renders + * Runs through the initial style data present in the context and renders * them via the renderer on the element. */ -export function renderInitialStylesAndClasses( +export function renderInitialStyles( + element: RElement, context: StylingContext, renderer: Renderer3) { + const initialStyles = context[StylingIndex.InitialStyleValuesPosition]; + renderInitialStylingValues(element, renderer, initialStyles, false); +} + +/** + * Runs through the initial class data present in the context and renders + * them via the renderer on the element. + */ +export function renderInitialClasses( element: RElement, context: StylingContext, renderer: Renderer3) { const initialClasses = context[StylingIndex.InitialClassValuesPosition]; renderInitialStylingValues(element, renderer, initialClasses, true); - - const initialStyles = context[StylingIndex.InitialStyleValuesPosition]; - renderInitialStylingValues(element, renderer, initialStyles, false); } /** @@ -190,8 +200,7 @@ export function allowNewBindingsForStylingContext(context: StylingContext): bool */ export function updateContextWithBindings( context: StylingContext, directiveRef: any | null, classBindingNames?: string[] | null, - styleBindingNames?: string[] | null, styleSanitizer?: StyleSanitizeFn | null, - onlyProcessSingleClasses?: boolean) { + styleBindingNames?: string[] | null, styleSanitizer?: StyleSanitizeFn | null) { if (context[StylingIndex.MasterFlagPosition] & StylingFlags.BindingAllocationLocked) return; // this means the context has already been patched with the directive's bindings @@ -201,6 +210,10 @@ export function updateContextWithBindings( return; } + if (styleBindingNames) { + styleBindingNames = hyphenateEntries(styleBindingNames); + } + // there are alot of variables being used below to track where in the context the new // binding values will be placed. Because the context consists of multiple types of // entries (single classes/styles and multi classes/styles) alot of the index positions @@ -212,6 +225,9 @@ export function updateContextWithBindings( const totalCurrentStyleBindings = singlePropOffsetValues[SinglePropOffsetValuesIndex.StylesCountPosition]; + const cachedClassMapValues = context[StylingIndex.CachedMultiClasses]; + const cachedStyleMapValues = context[StylingIndex.CachedMultiStyles]; + const classesOffset = totalCurrentClassBindings * StylingIndex.Size; const stylesOffset = totalCurrentStyleBindings * StylingIndex.Size; @@ -390,27 +406,90 @@ export function updateContextWithBindings( singlePropOffsetValues[SinglePropOffsetValuesIndex.StylesCountPosition] = totalCurrentStyleBindings + filteredStyleBindingNames.length; + // the map-based values also need to know how many entries got inserted + cachedClassMapValues[MapBasedOffsetValuesIndex.EntriesCountPosition] += + filteredClassBindingNames.length; + cachedStyleMapValues[MapBasedOffsetValuesIndex.EntriesCountPosition] += + filteredStyleBindingNames.length; + const newStylesSpaceAllocationSize = filteredStyleBindingNames.length * StylingIndex.Size; + const newClassesSpaceAllocationSize = filteredClassBindingNames.length * StylingIndex.Size; + + // update the multi styles cache with a reference for the directive that was just inserted + const directiveMultiStylesStartIndex = + multiStylesStartIndex + totalCurrentStyleBindings * StylingIndex.Size; + const cachedStyleMapIndex = cachedStyleMapValues.length; + + // this means that ONLY directive style styling (like ngStyle) was used + // therefore the root directive will still need to be filled in + if (directiveIndex > 0 && + cachedStyleMapValues.length <= MapBasedOffsetValuesIndex.ValuesStartPosition) { + cachedStyleMapValues.push(0, directiveMultiStylesStartIndex, null, 0); + } + + cachedStyleMapValues.push( + 0, directiveMultiStylesStartIndex, null, filteredStyleBindingNames.length); + + for (let i = MapBasedOffsetValuesIndex.ValuesStartPosition; i < cachedStyleMapIndex; + i += MapBasedOffsetValuesIndex.Size) { + // multi values start after all the single values (which is also where classes are) in the + // context therefore the new class allocation size should be taken into account + cachedStyleMapValues[i + MapBasedOffsetValuesIndex.PositionStartOffset] += + newClassesSpaceAllocationSize + newStylesSpaceAllocationSize; + } + + // update the multi classes cache with a reference for the directive that was just inserted + const directiveMultiClassesStartIndex = + multiClassesStartIndex + totalCurrentClassBindings * StylingIndex.Size; + const cachedClassMapIndex = cachedClassMapValues.length; + + // this means that ONLY directive class styling (like ngClass) was used + // therefore the root directive will still need to be filled in + if (directiveIndex > 0 && + cachedClassMapValues.length <= MapBasedOffsetValuesIndex.ValuesStartPosition) { + cachedClassMapValues.push(0, directiveMultiClassesStartIndex, null, 0); + } + + cachedClassMapValues.push( + 0, directiveMultiClassesStartIndex, null, filteredClassBindingNames.length); + + for (let i = MapBasedOffsetValuesIndex.ValuesStartPosition; i < cachedClassMapIndex; + i += MapBasedOffsetValuesIndex.Size) { + // the reason why both the styles + classes space is allocated to the existing offsets is + // because the styles show up before the classes in the context and any new inserted + // styles will offset any existing class entries in the context (even if there are no + // new class entries added) also the reason why it's *2 is because both single + multi + // entries for each new style have been added in the context before the multi class values + // actually start + cachedClassMapValues[i + MapBasedOffsetValuesIndex.PositionStartOffset] += + (newStylesSpaceAllocationSize * 2) + newClassesSpaceAllocationSize; + } + // there is no initial value flag for the master index since it doesn't // reference an initial style value - const masterFlag = pointers(0, 0, multiStylesStartIndex) | - (onlyProcessSingleClasses ? StylingFlags.OnlyProcessSingleClasses : 0); + const masterFlag = pointers(0, 0, multiStylesStartIndex); setFlag(context, StylingIndex.MasterFlagPosition, masterFlag); } /** * Searches through the existing registry of directives */ -function findOrPatchDirectiveIntoRegistry( +export function findOrPatchDirectiveIntoRegistry( context: StylingContext, directiveRef: any, styleSanitizer?: StyleSanitizeFn | null) { const directiveRefs = context[StylingIndex.DirectiveRegistryPosition]; const nextOffsetInsertionIndex = context[StylingIndex.SinglePropOffsetPositions].length; let directiveIndex: number; - const detectedIndex = getDirectiveRegistryValuesIndexOf(directiveRefs, directiveRef); + let detectedIndex = getDirectiveRegistryValuesIndexOf(directiveRefs, directiveRef); if (detectedIndex === -1) { + detectedIndex = directiveRefs.length; directiveIndex = directiveRefs.length / DirectiveRegistryValuesIndex.Size; - directiveRefs.push(directiveRef, nextOffsetInsertionIndex, false, styleSanitizer || null); + + allocateDirectiveIntoContext(context, directiveRef); + directiveRefs[detectedIndex + DirectiveRegistryValuesIndex.SinglePropValuesIndexOffset] = + nextOffsetInsertionIndex; + directiveRefs[detectedIndex + DirectiveRegistryValuesIndex.StyleSanitizerOffset] = + styleSanitizer || null; } else { const singlePropStartPosition = detectedIndex + DirectiveRegistryValuesIndex.SinglePropValuesIndexOffset; @@ -446,27 +525,53 @@ function getMatchingBindingIndex( } /** - * Sets and resolves all `multi` styling on an `StylingContext` so that they can be - * applied to the element once `renderStyling` is called. + * Registers the provided multi styling (`[style]` and `[class]`) values to the context. * - * All missing styles/class (any values that are not provided in the new `styles` - * or `classes` params) will resolve to `null` within their respective positions - * in the context. + * This function will iterate over the provided `classesInput` and `stylesInput` map + * values and insert/update or remove them from the context at exactly the right + * spot. + * + * This function also takes in a directive which implies that the styling values will + * be evaluated for that directive with respect to any other styling that already exists + * on the context. When there are styles that conflict (e.g. say `ngStyle` and `[style]` + * both update the `width` property at the same time) then the styling algorithm code below + * will decide which one wins based on the directive styling prioritization mechanism. This + * mechanism is better explained in render3/interfaces/styling.ts#directives). + * + * This function will not render any styling values on screen, but is rather designed to + * prepare the context for that. `renderStyling` must be called afterwards to render any + * styling data that was set in this function (note that `updateClassProp` and + * `updateStyleProp` are designed to be run after this function is run). * * @param context The styling context that will be updated with the * newly provided style values. * @param classesInput The key/value map of CSS class names that will be used for the update. * @param stylesInput The key/value map of CSS styles that will be used for the update. + * @param directiveRef an optional reference to the directive responsible + * for this binding change. If present then style binding will only + * actualize if the directive has ownership over this binding + * (see styling.ts#directives for more information about the algorithm). */ export function updateStylingMap( context: StylingContext, classesInput: {[key: string]: any} | string | - BoundPlayerFactory| NO_CHANGE | null, - stylesInput?: {[key: string]: any} | BoundPlayerFactory| NO_CHANGE | - null, + BoundPlayerFactory| null, + stylesInput?: {[key: string]: any} | BoundPlayerFactory| null, directiveRef?: any): void { - stylesInput = stylesInput || null; - const directiveIndex = getDirectiveIndexFromRegistry(context, directiveRef || null); + + classesInput = classesInput || null; + stylesInput = stylesInput || null; + const ignoreAllClassUpdates = isMultiValueCacheHit(context, true, directiveIndex, classesInput); + const ignoreAllStyleUpdates = isMultiValueCacheHit(context, false, directiveIndex, stylesInput); + + // early exit (this is what's done to avoid using ctx.bind() to cache the value) + if (ignoreAllClassUpdates && ignoreAllStyleUpdates) return; + + classesInput = + classesInput === NO_CHANGE ? readCachedMapValue(context, true, directiveIndex) : classesInput; + stylesInput = + stylesInput === NO_CHANGE ? readCachedMapValue(context, false, directiveIndex) : stylesInput; + const element = context[StylingIndex.ElementPosition] !as HTMLElement; const classesPlayerBuilder = classesInput instanceof BoundPlayerFactory ? new ClassAndStylePlayerBuilder(classesInput as any, element, BindingType.Class) : @@ -479,15 +584,6 @@ export function updateStylingMap( (classesInput as BoundPlayerFactory<{[key: string]: any}|string>) !.value : classesInput; const stylesValue = stylesPlayerBuilder ? stylesInput !.value : stylesInput; - // early exit (this is what's done to avoid using ctx.bind() to cache the value) - const ignoreAllClassUpdates = limitToSingleClasses(context) || classesValue === NO_CHANGE || - classesValue === context[StylingIndex.CachedClassValueOrInitialClassString]; - const ignoreAllStyleUpdates = - stylesValue === NO_CHANGE || stylesValue === context[StylingIndex.CachedStyleValue]; - if (ignoreAllClassUpdates && ignoreAllStyleUpdates) return; - - context[StylingIndex.CachedClassValueOrInitialClassString] = classesValue; - context[StylingIndex.CachedStyleValue] = stylesValue; let classNames: string[] = EMPTY_ARRAY; let applyAllClasses = false; @@ -522,150 +618,27 @@ export function updateStylingMap( } } - const classes = (classesValue || EMPTY_OBJ) as{[key: string]: any}; - const styleProps = stylesValue ? Object.keys(stylesValue) : EMPTY_ARRAY; - const styles = stylesValue || EMPTY_OBJ; + const multiStylesStartIndex = getMultiStylesStartIndex(context); + let multiClassesStartIndex = getMultiClassStartIndex(context); + let multiClassesEndIndex = context.length; - const classesStartIndex = styleProps.length; - let multiStartIndex = getMultiStartIndex(context); - - let dirty = false; - let ctxIndex = multiStartIndex; - - let propIndex = 0; - const propLimit = styleProps.length + classNames.length; - - // the main loop here will try and figure out how the shape of the provided - // styles differ with respect to the context. Later if the context/styles/classes - // are off-balance then they will be dealt in another loop after this one - while (ctxIndex < context.length && propIndex < propLimit) { - const isClassBased = propIndex >= classesStartIndex; - const processValue = - (!isClassBased && !ignoreAllStyleUpdates) || (isClassBased && !ignoreAllClassUpdates); - - // when there is a cache-hit for a string-based class then we should - // avoid doing any work diffing any of the changes - if (processValue) { - const adjustedPropIndex = isClassBased ? propIndex - classesStartIndex : propIndex; - const newProp: string = - isClassBased ? classNames[adjustedPropIndex] : styleProps[adjustedPropIndex]; - const newValue: string|boolean = - isClassBased ? (applyAllClasses ? true : classes[newProp]) : styles[newProp]; - const playerBuilderIndex = - isClassBased ? classesPlayerBuilderIndex : stylesPlayerBuilderIndex; - - const prop = getProp(context, ctxIndex); - if (prop === newProp) { - const value = getValue(context, ctxIndex); - const flag = getPointers(context, ctxIndex); - setPlayerBuilderIndex(context, ctxIndex, playerBuilderIndex, directiveIndex); - - if (hasValueChanged(flag, value, newValue)) { - setValue(context, ctxIndex, newValue); - playerBuildersAreDirty = playerBuildersAreDirty || !!playerBuilderIndex; - - const initialValue = getInitialValue(context, flag); - - // SKIP IF INITIAL CHECK - // If the former `value` is `null` then it means that an initial value - // could be being rendered on screen. If that is the case then there is - // no point in updating the value incase it matches. In other words if the - // new value is the exact same as the previously rendered value (which - // happens to be the initial value) then do nothing. - if (value != null || hasValueChanged(flag, initialValue, newValue)) { - setDirty(context, ctxIndex, true); - dirty = true; - } - } - } else { - const indexOfEntry = findEntryPositionByProp(context, newProp, ctxIndex); - if (indexOfEntry > 0) { - // it was found at a later point ... just swap the values - const valueToCompare = getValue(context, indexOfEntry); - const flagToCompare = getPointers(context, indexOfEntry); - swapMultiContextEntries(context, ctxIndex, indexOfEntry); - if (hasValueChanged(flagToCompare, valueToCompare, newValue)) { - const initialValue = getInitialValue(context, flagToCompare); - setValue(context, ctxIndex, newValue); - - // same if statement logic as above (look for SKIP IF INITIAL CHECK). - if (valueToCompare != null || hasValueChanged(flagToCompare, initialValue, newValue)) { - setDirty(context, ctxIndex, true); - playerBuildersAreDirty = playerBuildersAreDirty || !!playerBuilderIndex; - dirty = true; - } - } - } else { - // we only care to do this if the insertion is in the middle - const newFlag = prepareInitialFlag( - context, newProp, isClassBased, getStyleSanitizer(context, directiveIndex)); - playerBuildersAreDirty = playerBuildersAreDirty || !!playerBuilderIndex; - insertNewMultiProperty( - context, ctxIndex, isClassBased, newProp, newFlag, newValue, directiveIndex, - playerBuilderIndex); - dirty = true; - } - } + if (!ignoreAllStyleUpdates) { + const styleProps = stylesValue ? Object.keys(stylesValue) : EMPTY_ARRAY; + const styles = stylesValue || EMPTY_OBJ; + const totalNewEntries = patchStylingMapIntoContext( + context, directiveIndex, stylesPlayerBuilderIndex, multiStylesStartIndex, + multiClassesStartIndex, styleProps, styles, stylesInput, false); + if (totalNewEntries) { + multiClassesStartIndex += totalNewEntries * StylingIndex.Size; + multiClassesEndIndex += totalNewEntries * StylingIndex.Size; } - - ctxIndex += StylingIndex.Size; - propIndex++; } - // this means that there are left-over values in the context that - // were not included in the provided styles/classes and in this - // case the goal is to "remove" them from the context (by nullifying) - while (ctxIndex < context.length) { - const flag = getPointers(context, ctxIndex); - const isClassBased = (flag & StylingFlags.Class) === StylingFlags.Class; - const processValue = - (!isClassBased && !ignoreAllStyleUpdates) || (isClassBased && !ignoreAllClassUpdates); - if (processValue) { - const value = getValue(context, ctxIndex); - const doRemoveValue = valueExists(value, isClassBased); - if (doRemoveValue) { - setDirty(context, ctxIndex, true); - setValue(context, ctxIndex, null); - - // we keep the player factory the same so that the `nulled` value can - // be instructed into the player because removing a style and/or a class - // is a valid animation player instruction. - const playerBuilderIndex = - isClassBased ? classesPlayerBuilderIndex : stylesPlayerBuilderIndex; - setPlayerBuilderIndex(context, ctxIndex, playerBuilderIndex, directiveIndex); - dirty = true; - } - } - ctxIndex += StylingIndex.Size; - } - - // this means that there are left-over properties in the context that - // were not detected in the context during the loop above. In that - // case we want to add the new entries into the list - const sanitizer = getStyleSanitizer(context, directiveIndex); - while (propIndex < propLimit) { - const isClassBased = propIndex >= classesStartIndex; - const processValue = - (!isClassBased && !ignoreAllStyleUpdates) || (isClassBased && !ignoreAllClassUpdates); - if (processValue) { - const adjustedPropIndex = isClassBased ? propIndex - classesStartIndex : propIndex; - const prop = isClassBased ? classNames[adjustedPropIndex] : styleProps[adjustedPropIndex]; - const value: string|boolean = - isClassBased ? (applyAllClasses ? true : classes[prop]) : styles[prop]; - const flag = prepareInitialFlag(context, prop, isClassBased, sanitizer) | StylingFlags.Dirty; - const playerBuilderIndex = - isClassBased ? classesPlayerBuilderIndex : stylesPlayerBuilderIndex; - const ctxIndex = context.length; - context.push(flag, prop, value, 0); - setPlayerBuilderIndex(context, ctxIndex, playerBuilderIndex, directiveIndex); - dirty = true; - } - propIndex++; - } - - if (dirty) { - setContextDirty(context, true); - setDirectiveDirty(context, directiveIndex, true); + if (!ignoreAllClassUpdates) { + const classes = (classesValue || EMPTY_OBJ) as{[key: string]: any}; + patchStylingMapIntoContext( + context, directiveIndex, classesPlayerBuilderIndex, multiClassesStartIndex, + multiClassesEndIndex, classNames, applyAllClasses || classes, classesInput, true); } if (playerBuildersAreDirty) { @@ -674,18 +647,275 @@ export function updateStylingMap( } /** - * This method will toggle the referenced CSS class (by the provided index) - * within the given context. + * Applies the given multi styling (styles or classes) values to the context. + * + * The styling algorithm code that applies multi-level styling (things like `[style]` and `[class]` + * values) resides here. + * + * Because this function understands that multiple directives may all write to the `[style]` and + * `[class]` bindings (through host bindings), it relies of each directive applying its binding + * value in order. This means that a directive like `classADirective` will always fire before + * `classBDirective` and therefore its styling values (classes and styles) will always be evaluated + * in the same order. Because of this consistent ordering, the first directive has a higher priority + * than the second one. It is with this prioritzation mechanism that the styling algorithm knows how + * to merge and apply redudant styling properties. + * + * The function itself applies the key/value entries (or an array of keys) to + * the context in the following steps. + * + * STEP 1: + * First check to see what properties are already set and in use by another directive in the + * context (e.g. `ngClass` set the `width` value and `[style.width]="w"` in a directive is + * attempting to set it as well). + * + * STEP 2: + * All remaining properties (that were not set prior to this directive) are now updated in + * the context. Any new properties are inserted exactly at their spot in the context and any + * previously set properties are shifted to exactly where the cursor sits while iterating over + * the context. The end result is a balanced context that includes the exact ordering of the + * styling properties/values for the provided input from the directive. + * + * STEP 3: + * Any unmatched properties in the context that belong to the directive are set to null + * + * Once the updating phase is done, then the algorithm will decide whether or not to flag the + * follow-up directives (the directives that will pass in their styling values) depending on if + * the "shape" of the multi-value map has changed (either if any keys are removed or added or + * if there are any new `null` values). If any follow-up directives are flagged as dirty then the + * algorithm will run again for them. Otherwise if the shape did not change then any follow-up + * directives will not run (so long as their binding values stay the same). + * + * @returns the total amount of new slots that were allocated into the context due to new styling + * properties that were detected. + */ +function patchStylingMapIntoContext( + context: StylingContext, directiveIndex: number, playerBuilderIndex: number, ctxStart: number, + ctxEnd: number, props: (string | null)[], values: {[key: string]: any} | true, cacheValue: any, + entryIsClassBased: boolean): number { + let dirty = false; + + const cacheIndex = MapBasedOffsetValuesIndex.ValuesStartPosition + + directiveIndex * MapBasedOffsetValuesIndex.Size; + + // the cachedValues array is the registry of all multi style values (map values). Each + // value is stored (cached) each time is updated. + const cachedValues = + context[entryIsClassBased ? StylingIndex.CachedMultiClasses : StylingIndex.CachedMultiStyles]; + + // this is the index in which this directive has ownership access to write to this + // value (anything before is owned by a previous directive that is more important) + const ownershipValuesStartIndex = + cachedValues[cacheIndex + MapBasedOffsetValuesIndex.PositionStartOffset]; + + const existingCachedValue = cachedValues[cacheIndex + MapBasedOffsetValuesIndex.ValueOffset]; + const existingCachedValueCount = + cachedValues[cacheIndex + MapBasedOffsetValuesIndex.ValueCountOffset]; + const existingCachedValueIsDirty = + cachedValues[cacheIndex + MapBasedOffsetValuesIndex.DirtyFlagOffset] === 1; + + // A shape change means the provided map value has either removed or added new properties + // compared to what were in the last time. If a shape change occurs then it means that all + // follow-up multi-styling entries are obsolete and will be examined again when CD runs + // them. If a shape change has not occurred then there is no reason to check any other + // directive values if their identity has not changed. If a previous directive set this + // value as dirty (because its own shape changed) then this means that the object has been + // offset to a different area in the context. Because its value has been offset then it + // can't write to a region that it wrote to before (which may have been apart of another + // directive) and therefore its shape changes too. + let valuesEntryShapeChange = + existingCachedValueIsDirty || ((!existingCachedValue && cacheValue) ? true : false); + + let totalUniqueValues = 0; + let totalNewAllocatedSlots = 0; + + // this is a trick to avoid building {key:value} map where all the values + // are `true` (this happens when a className string is provided instead of a + // map as an input value to this styling algorithm) + const applyAllProps = values === true; + + // STEP 1: + // loop through the earlier directives and figure out if any properties here will be placed + // in their area (this happens when the value is null because the earlier directive erased it). + let ctxIndex = ctxStart; + let totalRemainingProperties = props.length; + while (ctxIndex < ownershipValuesStartIndex) { + const currentProp = getProp(context, ctxIndex); + if (totalRemainingProperties) { + for (let i = 0; i < props.length; i++) { + const mapProp = props[i]; + const normalizedProp = mapProp ? (entryIsClassBased ? mapProp : hyphenate(mapProp)) : null; + if (normalizedProp && currentProp === normalizedProp) { + const currentValue = getValue(context, ctxIndex); + const currentDirectiveIndex = getDirectiveIndexFromEntry(context, ctxIndex); + const value = applyAllProps ? true : (values as{[key: string]: any})[normalizedProp]; + const currentFlag = getPointers(context, ctxIndex); + if (hasValueChanged(currentFlag, currentValue, value) && + allowValueChange(currentValue, value, currentDirectiveIndex, directiveIndex)) { + setValue(context, ctxIndex, value); + setPlayerBuilderIndex(context, ctxIndex, playerBuilderIndex, directiveIndex); + if (hasInitialValueChanged(context, currentFlag, value)) { + setDirty(context, ctxIndex, true); + dirty = true; + } + } + props[i] = null; + totalRemainingProperties--; + break; + } + } + } + ctxIndex += StylingIndex.Size; + } + + // STEP 2: + // apply the left over properties to the context in the correct order. + if (totalRemainingProperties) { + const sanitizer = entryIsClassBased ? null : getStyleSanitizer(context, directiveIndex); + propertiesLoop: for (let i = 0; i < props.length; i++) { + const mapProp = props[i]; + + if (!mapProp) { + // this is an early exit incase a value was already encountered above in the + // previous loop (which means that the property was applied or rejected) + continue; + } + + const value = applyAllProps ? true : (values as{[key: string]: any})[mapProp]; + const normalizedProp = entryIsClassBased ? mapProp : hyphenate(mapProp); + const isInsideOwnershipArea = ctxIndex >= ownershipValuesStartIndex; + + for (let j = ctxIndex; j < ctxEnd; j += StylingIndex.Size) { + const distantCtxProp = getProp(context, j); + if (distantCtxProp === normalizedProp) { + const distantCtxDirectiveIndex = getDirectiveIndexFromEntry(context, j); + const distantCtxPlayerBuilderIndex = getPlayerBuilderIndex(context, j); + const distantCtxValue = getValue(context, j); + const distantCtxFlag = getPointers(context, j); + + if (allowValueChange(distantCtxValue, value, distantCtxDirectiveIndex, directiveIndex)) { + // even if the entry isn't updated (by value or directiveIndex) then + // it should still be moved over to the correct spot in the array so + // the iteration loop is tighter. + if (isInsideOwnershipArea) { + swapMultiContextEntries(context, ctxIndex, j); + totalUniqueValues++; + } + + if (hasValueChanged(distantCtxFlag, distantCtxValue, value)) { + if (value === null || value === undefined && value !== distantCtxValue) { + valuesEntryShapeChange = true; + } + + setValue(context, ctxIndex, value); + + // SKIP IF INITIAL CHECK + // If the former `value` is `null` then it means that an initial value + // could be being rendered on screen. If that is the case then there is + // no point in updating the value incase it matches. In other words if the + // new value is the exact same as the previously rendered value (which + // happens to be the initial value) then do nothing. + if (distantCtxValue !== null || + hasInitialValueChanged(context, distantCtxFlag, value)) { + setDirty(context, ctxIndex, true); + dirty = true; + } + } + + if (distantCtxDirectiveIndex !== directiveIndex || + playerBuilderIndex !== distantCtxPlayerBuilderIndex) { + setPlayerBuilderIndex(context, ctxIndex, playerBuilderIndex, directiveIndex); + } + } + + ctxIndex += StylingIndex.Size; + continue propertiesLoop; + } + } + + // fallback case ... value not found at all in the context + if (value != null) { + valuesEntryShapeChange = true; + totalUniqueValues++; + const flag = prepareInitialFlag(context, normalizedProp, entryIsClassBased, sanitizer) | + StylingFlags.Dirty; + + const insertionIndex = isInsideOwnershipArea ? + ctxIndex : + (ownershipValuesStartIndex + totalNewAllocatedSlots * StylingIndex.Size); + insertNewMultiProperty( + context, insertionIndex, entryIsClassBased, normalizedProp, flag, value, directiveIndex, + playerBuilderIndex); + + totalNewAllocatedSlots++; + ctxEnd += StylingIndex.Size; + ctxIndex += StylingIndex.Size; + + dirty = true; + } + } + } + + // STEP 3: + // Remove (nullify) any existing entries in the context that were not apart of the + // map input value that was passed into this algorithm for this directive. + while (ctxIndex < ctxEnd) { + valuesEntryShapeChange = true; // some values are missing + const ctxValue = getValue(context, ctxIndex); + const ctxFlag = getPointers(context, ctxIndex); + if (ctxValue != null) { + valuesEntryShapeChange = true; + } + if (hasValueChanged(ctxFlag, ctxValue, null)) { + setValue(context, ctxIndex, null); + // only if the initial value is falsy then + if (hasInitialValueChanged(context, ctxFlag, ctxValue)) { + setDirty(context, ctxIndex, true); + dirty = true; + } + setPlayerBuilderIndex(context, ctxIndex, playerBuilderIndex, directiveIndex); + } + ctxIndex += StylingIndex.Size; + } + + // Because the object shape has changed, this means that all follow-up directives will need to + // reapply their values into the object. For this to happen, the cached array needs to be updated + // with dirty flags so that follow-up calls to `updateStylingMap` will reapply their styling code. + // the reapplication of styling code within the context will reshape it and update the offset + // values (also follow-up directives can write new values incase earlier directives set anything + // to null due to removals or falsy values). + valuesEntryShapeChange = valuesEntryShapeChange || existingCachedValueCount !== totalUniqueValues; + updateCachedMapValue( + context, directiveIndex, entryIsClassBased, cacheValue, ownershipValuesStartIndex, ctxEnd, + totalUniqueValues, valuesEntryShapeChange); + + if (dirty) { + setContextDirty(context, true); + setDirectiveDirty(context, directiveIndex, true); + } + + return totalNewAllocatedSlots; +} + +/** + * Sets and resolves a single class value on the provided `StylingContext` so + * that they can be applied to the element once `renderStyling` is called. * * @param context The styling context that will be updated with the * newly provided class value. * @param offset The index of the CSS class which is being updated. * @param addOrRemove Whether or not to add or remove the CSS class + * @param directiveRef an optional reference to the directive responsible + * for this binding change. If present then style binding will only + * actualize if the directive has ownership over this binding + * (see styling.ts#directives for more information about the algorithm). + * @param forceOverride whether or not to skip all directive prioritization + * and just apply the value regardless. */ export function updateClassProp( - context: StylingContext, offset: number, addOrRemove: boolean | BoundPlayerFactory, - directiveRef?: any): void { - _updateSingleStylingValue(context, offset, addOrRemove, true, directiveRef); + context: StylingContext, offset: number, + input: boolean | BoundPlayerFactory| null, directiveRef?: any, + forceOverride?: boolean): void { + updateSingleStylingValue(context, offset, input, true, directiveRef, forceOverride); } /** @@ -705,18 +935,20 @@ export function updateClassProp( * for this binding change. If present then style binding will only * actualize if the directive has ownership over this binding * (see styling.ts#directives for more information about the algorithm). + * @param forceOverride whether or not to skip all directive prioritization + * and just apply the value regardless. */ export function updateStyleProp( context: StylingContext, offset: number, - input: string | boolean | null | BoundPlayerFactory, - directiveRef?: any): void { - _updateSingleStylingValue(context, offset, input, false, directiveRef); + input: string | boolean | null | BoundPlayerFactory, directiveRef?: any, + forceOverride?: boolean): void { + updateSingleStylingValue(context, offset, input, false, directiveRef, forceOverride); } -function _updateSingleStylingValue( +function updateSingleStylingValue( context: StylingContext, offset: number, input: string | boolean | null | BoundPlayerFactory, isClassBased: boolean, - directiveRef: any): void { + directiveRef: any, forceOverride?: boolean): void { const directiveIndex = getDirectiveIndexFromRegistry(context, directiveRef || null); const singleIndex = getSinglePropIndexValue(context, directiveIndex, offset, isClassBased); const currValue = getValue(context, singleIndex); @@ -725,7 +957,7 @@ function _updateSingleStylingValue( const value: string|boolean|null = (input instanceof BoundPlayerFactory) ? input.value : input; if (hasValueChanged(currFlag, currValue, value) && - allowValueChange(currValue, value, currDirective, directiveIndex)) { + (forceOverride || allowValueChange(currValue, value, currDirective, directiveIndex))) { const isClassBased = (currFlag & StylingFlags.Class) === StylingFlags.Class; const element = context[StylingIndex.ElementPosition] !as HTMLElement; const playerBuilder = input instanceof BoundPlayerFactory ? @@ -816,8 +1048,7 @@ export function renderStyling( const flushPlayerBuilders: any = context[StylingIndex.MasterFlagPosition] & StylingFlags.PlayerBuildersDirty; const native = context[StylingIndex.ElementPosition] !; - const multiStartIndex = getMultiStartIndex(context); - const onlySingleClasses = limitToSingleClasses(context); + const multiStartIndex = getMultiStylesStartIndex(context); let stillDirty = false; for (let i = StylingIndex.SingleStylesStartPosition; i < context.length; @@ -838,7 +1069,6 @@ export function renderStyling( const playerBuilder = getPlayerBuilder(context, i); const isClassBased = flag & StylingFlags.Class ? true : false; const isInSingleRegion = i < multiStartIndex; - const readInitialValue = !isClassBased || !onlySingleClasses; let valueToApply: string|boolean|null = value; @@ -859,7 +1089,7 @@ export function renderStyling( // classes are turned off and should therefore defer to their initial values) // Note that we ignore class-based deferals because otherwise a class can never // be removed in the case that it exists as true in the initial classes list... - if (!isClassBased && !valueExists(valueToApply, isClassBased) && readInitialValue) { + if (!valueExists(valueToApply, isClassBased)) { valueToApply = getInitialValue(context, flag); } @@ -922,6 +1152,8 @@ export function renderStyling( } /** + * Assigns a style value to a style property for the given element. + * * This function renders a given CSS prop/value entry using the * provided renderer. If a `store` value is provided then * that will be used a render context instead of the provided @@ -947,20 +1179,22 @@ export function setStyle( } } else if (value) { value = value.toString(); // opacity, z-index and flexbox all have number values which may not - // assign as numbers + // assign as numbers ngDevMode && ngDevMode.rendererSetStyle++; isProceduralRenderer(renderer) ? renderer.setStyle(native, prop, value, RendererStyleFlags3.DashCase) : - native.style[prop] = value; + native.style.setProperty(prop, value); } else { ngDevMode && ngDevMode.rendererRemoveStyle++; isProceduralRenderer(renderer) ? renderer.removeStyle(native, prop, RendererStyleFlags3.DashCase) : - native.style[prop] = ''; + native.style.removeProperty(prop); } } /** + * Adds/removes the provided className value to the provided element. + * * This function renders a given CSS class value using the provided * renderer (by adding or removing it from the provided element). * If a `store` value is provided then that will be used a render @@ -1059,6 +1293,20 @@ function getMultiStartIndex(context: StylingContext): number { return getMultiOrSingleIndex(context[StylingIndex.MasterFlagPosition]) as number; } +function getMultiClassStartIndex(context: StylingContext): number { + const classCache = context[StylingIndex.CachedMultiClasses]; + return classCache + [MapBasedOffsetValuesIndex.ValuesStartPosition + + MapBasedOffsetValuesIndex.PositionStartOffset]; +} + +function getMultiStylesStartIndex(context: StylingContext): number { + const stylesCache = context[StylingIndex.CachedMultiStyles]; + return stylesCache + [MapBasedOffsetValuesIndex.ValuesStartPosition + + MapBasedOffsetValuesIndex.PositionStartOffset]; +} + function setProp(context: StylingContext, index: number, prop: string) { context[index + StylingIndex.PropertyOffset] = prop; } @@ -1148,10 +1396,6 @@ export function isContextDirty(context: StylingContext): boolean { return isDirty(context, StylingIndex.MasterFlagPosition); } -export function limitToSingleClasses(context: StylingContext) { - return context[StylingIndex.MasterFlagPosition] & StylingFlags.OnlyProcessSingleClasses; -} - export function setContextDirty(context: StylingContext, isDirtyYes: boolean): void { setDirty(context, StylingIndex.MasterFlagPosition, isDirtyYes); } @@ -1164,23 +1408,14 @@ export function setContextPlayersDirty(context: StylingContext, isDirtyYes: bool } } -function findEntryPositionByProp( - context: StylingContext, prop: string, startIndex?: number): number { - for (let i = (startIndex || 0) + StylingIndex.PropertyOffset; i < context.length; - i += StylingIndex.Size) { - const thisProp = context[i]; - if (thisProp == prop) { - return i - StylingIndex.PropertyOffset; - } - } - return -1; -} - function swapMultiContextEntries(context: StylingContext, indexA: number, indexB: number) { + if (indexA === indexB) return; + const tmpValue = getValue(context, indexA); const tmpProp = getProp(context, indexA); const tmpFlag = getPointers(context, indexA); const tmpPlayerBuilderIndex = getPlayerBuilderIndex(context, indexA); + const tmpDirectiveIndex = getDirectiveIndexFromEntry(context, indexA); let flagA = tmpFlag; let flagB = getPointers(context, indexB); @@ -1203,13 +1438,13 @@ function swapMultiContextEntries(context: StylingContext, indexA: number, indexB setProp(context, indexA, getProp(context, indexB)); setFlag(context, indexA, getPointers(context, indexB)); const playerIndexA = getPlayerBuilderIndex(context, indexB); - const directiveIndexA = 0; + const directiveIndexA = getDirectiveIndexFromEntry(context, indexB); setPlayerBuilderIndex(context, indexA, playerIndexA, directiveIndexA); setValue(context, indexB, tmpValue); setProp(context, indexB, tmpProp); setFlag(context, indexB, tmpFlag); - setPlayerBuilderIndex(context, indexB, tmpPlayerBuilderIndex, directiveIndexA); + setPlayerBuilderIndex(context, indexB, tmpPlayerBuilderIndex, tmpDirectiveIndex); } function updateSinglePointerValues(context: StylingContext, indexStartPosition: number) { @@ -1248,9 +1483,6 @@ function insertNewMultiProperty( } function valueExists(value: string | null | boolean, isClassBased?: boolean) { - if (isClassBased) { - return value ? true : false; - } return value !== null; } @@ -1273,6 +1505,11 @@ function prepareInitialFlag( return pointers(flag, initialIndex, 0); } +function hasInitialValueChanged(context: StylingContext, flag: number, newValue: any) { + const initialValue = getInitialValue(context, flag); + return !initialValue || hasValueChanged(flag, initialValue, newValue); +} + function hasValueChanged( flag: number, a: string | boolean | null, b: string | boolean | null): boolean { const isClassBased = flag & StylingFlags.Class; @@ -1336,12 +1573,11 @@ export interface LogSummary { dynamicIndex: number; // value: number; // flags: { - dirty: boolean; // - class: boolean; // - sanitize: boolean; // - playerBuildersDirty: boolean; // - onlyProcessSingleClasses: boolean; // - bindingAllocationLocked: boolean; // + dirty: boolean; // + class: boolean; // + sanitize: boolean; // + playerBuildersDirty: boolean; // + bindingAllocationLocked: boolean; // }; } @@ -1379,7 +1615,6 @@ export function generateConfigSummary(source: number | StylingContext, index?: n class: flag & StylingFlags.Class ? true : false, sanitize: flag & StylingFlags.Sanitize ? true : false, playerBuildersDirty: flag & StylingFlags.PlayerBuildersDirty ? true : false, - onlyProcessSingleClasses: flag & StylingFlags.OnlyProcessSingleClasses ? true : false, bindingAllocationLocked: flag & StylingFlags.BindingAllocationLocked ? true : false, } }; @@ -1502,9 +1737,9 @@ function allowValueChange( // prioritization of directives enables the styling algorithm to decide if a style // or class should be allowed to be updated/replaced incase an earlier directive // already wrote to the exact same style-property or className value. In other words - // ... this decides what to do if and when there is a collision. - if (currentValue) { - if (newValue) { + // this decides what to do if and when there is a collision. + if (currentValue != null) { + if (newValue != null) { // if a directive index is lower than it always has priority over the // previous directive's value... return newDirectiveOwner <= currentDirectiveOwner; @@ -1520,16 +1755,21 @@ function allowValueChange( } /** - * This function is only designed to be called for `[class]` bindings when - * `[ngClass]` (or something that uses `class` as an input) is present. Once - * directive host bindings fully work for `[class]` and `[style]` inputs - * then this can be deleted. + * Returns the className string of all the initial classes for the element. + * + * This function is designed to populate and cache all the static class + * values into a className string. The caching mechanism works by placing + * the completed className string into the initial values array into a + * dedicated slot. This will prevent the function from having to populate + * the string each time an element is created or matched. + * + * @returns the className string (e.g. `on active red`) */ export function getInitialClassNameValue(context: StylingContext): string { - let className = context[StylingIndex.CachedClassValueOrInitialClassString] as string; - if (className == null) { + const initialClassValues = context[StylingIndex.InitialClassValuesPosition]; + let className = initialClassValues[InitialStylingValuesIndex.InitialClassesStringPosition]; + if (className === null) { className = ''; - const initialClassValues = context[StylingIndex.InitialClassValuesPosition]; for (let i = InitialStylingValuesIndex.KeyValueStartPosition; i < initialClassValues.length; i += InitialStylingValuesIndex.Size) { const isPresent = initialClassValues[i + 1]; @@ -1537,7 +1777,162 @@ export function getInitialClassNameValue(context: StylingContext): string { className += (className.length ? ' ' : '') + initialClassValues[i]; } } - context[StylingIndex.CachedClassValueOrInitialClassString] = className; + initialClassValues[InitialStylingValuesIndex.InitialClassesStringPosition] = className; } return className; } + +/** + * Returns the style string of all the initial styles for the element. + * + * This function is designed to populate and cache all the static style + * values into a style string. The caching mechanism works by placing + * the completed style string into the initial values array into a + * dedicated slot. This will prevent the function from having to populate + * the string each time an element is created or matched. + * + * @returns the style string (e.g. `width:100px;height:200px`) + */ +export function getInitialStyleStringValue(context: StylingContext): string { + const initialStyleValues = context[StylingIndex.InitialStyleValuesPosition]; + let styleString = initialStyleValues[InitialStylingValuesIndex.InitialClassesStringPosition]; + if (styleString === null) { + styleString = ''; + for (let i = InitialStylingValuesIndex.KeyValueStartPosition; i < initialStyleValues.length; + i += InitialStylingValuesIndex.Size) { + const value = initialStyleValues[i + 1]; + if (value !== null) { + styleString += (styleString.length ? ';' : '') + `${initialStyleValues[i]}:${value}`; + } + } + initialStyleValues[InitialStylingValuesIndex.InitialClassesStringPosition] = styleString; + } + return styleString; +} + +/** + * Returns the current cached mutli-value for a given directiveIndex within the provided context. + */ +function readCachedMapValue( + context: StylingContext, entryIsClassBased: boolean, directiveIndex: number) { + const values: MapBasedOffsetValues = + context[entryIsClassBased ? StylingIndex.CachedMultiClasses : StylingIndex.CachedMultiStyles]; + const index = MapBasedOffsetValuesIndex.ValuesStartPosition + + directiveIndex * MapBasedOffsetValuesIndex.Size; + return values[index + MapBasedOffsetValuesIndex.ValueOffset] || null; +} + +/** + * Determines whether the provided multi styling value should be updated or not. + * + * Because `[style]` and `[class]` bindings rely on an identity change to occur before + * applying new values, the styling algorithm may not update an existing entry into + * the context if a previous directive's entry changed shape. + * + * This function will decide whether or not a value should be applied (if there is a + * cache miss) to the context based on the following rules: + * + * - If there is an identity change between the existing value and new value + * - If there is no existing value cached (first write) + * - If a previous directive flagged the existing cached value as dirty + */ +function isMultiValueCacheHit( + context: StylingContext, entryIsClassBased: boolean, directiveIndex: number, + newValue: any): boolean { + const indexOfCachedValues = + entryIsClassBased ? StylingIndex.CachedMultiClasses : StylingIndex.CachedMultiStyles; + const cachedValues = context[indexOfCachedValues] as MapBasedOffsetValues; + const index = MapBasedOffsetValuesIndex.ValuesStartPosition + + directiveIndex * MapBasedOffsetValuesIndex.Size; + if (cachedValues[index + MapBasedOffsetValuesIndex.DirtyFlagOffset]) return false; + return newValue === NO_CHANGE || + readCachedMapValue(context, entryIsClassBased, directiveIndex) === newValue; +} + +/** + * Updates the cached status of a multi-styling value in the context. + * + * The cached map array (which exists in the context) contains a manifest of + * each multi-styling entry (`[style]` and `[class]` entries) for the template + * as well as all directives. + * + * This function will update the cached status of the provided multi-style + * entry within the cache. + * + * When called, this function will update the following information: + * - The actual cached value (the raw value that was passed into `[style]` or `[class]`) + * - The total amount of unique styling entries that this value has written into the context + * - The exact position of where the multi styling entries start in the context for this binding + * - The dirty flag will be set to true + * + * If the `dirtyFutureValues` param is provided then it will update all future entries (binding + * entries that exist as apart of other directives) to be dirty as well. This will force the + * styling algorithm to reapply those values once change detection checks them (which will in + * turn cause the styling context to update itself and the correct styling values will be + * rendered on screen). + */ +function updateCachedMapValue( + context: StylingContext, directiveIndex: number, entryIsClassBased: boolean, cacheValue: any, + startPosition: number, endPosition: number, totalValues: number, dirtyFutureValues: boolean) { + const values = + context[entryIsClassBased ? StylingIndex.CachedMultiClasses : StylingIndex.CachedMultiStyles]; + + const index = MapBasedOffsetValuesIndex.ValuesStartPosition + + directiveIndex * MapBasedOffsetValuesIndex.Size; + + // in the event that this is true we assume that future values are dirty and therefore + // will be checked again in the next CD cycle + if (dirtyFutureValues) { + const nextStartPosition = startPosition + totalValues * MapBasedOffsetValuesIndex.Size; + for (let i = index + MapBasedOffsetValuesIndex.Size; i < values.length; + i += MapBasedOffsetValuesIndex.Size) { + values[i + MapBasedOffsetValuesIndex.PositionStartOffset] = nextStartPosition; + values[i + MapBasedOffsetValuesIndex.DirtyFlagOffset] = 1; + } + } + + values[index + MapBasedOffsetValuesIndex.DirtyFlagOffset] = 0; + values[index + MapBasedOffsetValuesIndex.PositionStartOffset] = startPosition; + values[index + MapBasedOffsetValuesIndex.ValueOffset] = cacheValue; + values[index + MapBasedOffsetValuesIndex.ValueCountOffset] = totalValues; + + // the code below counts the total amount of styling values that exist in + // the context up until this directive. This value will be later used to + // update the cached value map's total counter value. + let totalStylingEntries = totalValues; + for (let i = MapBasedOffsetValuesIndex.ValuesStartPosition; i < index; + i += MapBasedOffsetValuesIndex.Size) { + totalStylingEntries += values[i + MapBasedOffsetValuesIndex.ValueCountOffset]; + } + + // because style values come before class values in the context this means + // that if any new values were inserted then the cache values array for + // classes is out of sync. The code below will update the offsets to point + // to their new values. + if (!entryIsClassBased) { + const classCache = context[StylingIndex.CachedMultiClasses]; + const classesStartPosition = classCache + [MapBasedOffsetValuesIndex.ValuesStartPosition + + MapBasedOffsetValuesIndex.PositionStartOffset]; + const diffInStartPosition = endPosition - classesStartPosition; + for (let i = MapBasedOffsetValuesIndex.ValuesStartPosition; i < classCache.length; + i += MapBasedOffsetValuesIndex.Size) { + classCache[i + MapBasedOffsetValuesIndex.PositionStartOffset] += diffInStartPosition; + } + } + + values[MapBasedOffsetValuesIndex.EntriesCountPosition] = totalStylingEntries; +} + +function hyphenateEntries(entries: string[]): string[] { + const newEntries: string[] = []; + for (let i = 0; i < entries.length; i++) { + newEntries.push(hyphenate(entries[i])); + } + return newEntries; +} + +function hyphenate(value: string): string { + return value.replace( + /[a-z][A-Z]/g, match => `${match.charAt(0)}-${match.charAt(1).toLowerCase()}`); +} diff --git a/packages/core/src/render3/styling/util.ts b/packages/core/src/render3/styling/util.ts index 5bc2c9441f..4d9ea60449 100644 --- a/packages/core/src/render3/styling/util.ts +++ b/packages/core/src/render3/styling/util.ts @@ -26,17 +26,24 @@ export function createEmptyStylingContext( element?: RElement | null, sanitizer?: StyleSanitizeFn | null, initialStyles?: InitialStylingValues | null, initialClasses?: InitialStylingValues | null): StylingContext { - return [ - 0, // MasterFlags - [null, -1, false, sanitizer || null], // DirectiveRefs - initialStyles || [null], // InitialStyles - initialClasses || [null], // InitialClasses - [0, 0], // SinglePropOffsets - element || null, // Element - null, // PreviousMultiClassValue - null, // PreviousMultiStyleValue - null, // PlayerContext + const context: StylingContext = [ + 0, // MasterFlags + [] as any, // DirectiveRefs (this gets filled below) + initialStyles || [null, null], // InitialStyles + initialClasses || [null, null], // InitialClasses + [0, 0], // SinglePropOffsets + element || null, // Element + [0], // CachedMultiClassValue + [0], // CachedMultiStyleValue + null, // PlayerContext ]; + allocateDirectiveIntoContext(context, null); + return context; +} + +export function allocateDirectiveIntoContext(context: StylingContext, directiveRef: any | null) { + // this is a new directive which we have not seen yet. + context[StylingIndex.DirectiveRegistryPosition].push(directiveRef, -1, false, null); } /** @@ -103,6 +110,34 @@ export function isAnimationProp(name: string): boolean { return name[0] === ANIMATION_PROP_PREFIX; } +export function hasClassInput(tNode: TNode) { + return (tNode.flags & TNodeFlags.hasClassInput) !== 0; +} + +export function hasStyleInput(tNode: TNode) { + return (tNode.flags & TNodeFlags.hasStyleInput) !== 0; +} + +export function forceClassesAsString(classes: string | {[key: string]: any} | null | undefined): + string { + if (classes && typeof classes !== 'string') { + classes = Object.keys(classes).join(' '); + } + return (classes as string) || ''; +} + +export function forceStylesAsString(styles: {[key: string]: any} | null | undefined): string { + let str = ''; + if (styles) { + const props = Object.keys(styles); + for (let i = 0; i < props.length; i++) { + const prop = props[i]; + str += (i ? ';' : '') + `${prop}:${styles[prop]}`; + } + } + return str; +} + export function addPlayerInternal( playerContext: PlayerContext, rootContext: RootContext, element: HTMLElement, player: Player | null, playerContextIndex: number, ref?: any): boolean { @@ -196,7 +231,3 @@ export function hasStyling(attrs: TAttributes): boolean { } return false; } - -export function hasClassInput(tNode: TNode) { - return tNode.flags & TNodeFlags.hasClassInput ? true : false; -} diff --git a/packages/core/test/bundling/animation_world/index.ts b/packages/core/test/bundling/animation_world/index.ts index 001c8b3ea7..f4b3ae5f69 100644 --- a/packages/core/test/bundling/animation_world/index.ts +++ b/packages/core/test/bundling/animation_world/index.ts @@ -9,7 +9,7 @@ import '@angular/core/test/bundling/util/src/reflect_metadata'; import {CommonModule} from '@angular/common'; -import {Component, Directive, ElementRef, HostBinding, NgModule, ɵPlayState as PlayState, ɵPlayer as Player, ɵPlayerHandler as PlayerHandler, ɵaddPlayer as addPlayer, ɵbindPlayerFactory as bindPlayerFactory, ɵmarkDirty as markDirty, ɵrenderComponent as renderComponent} from '@angular/core'; +import {Component, Directive, ElementRef, HostBinding, HostListener, NgModule, ɵPlayState as PlayState, ɵPlayer as Player, ɵPlayerHandler as PlayerHandler, ɵaddPlayer as addPlayer, ɵbindPlayerFactory as bindPlayerFactory, ɵmarkDirty as markDirty, ɵrenderComponent as renderComponent} from '@angular/core'; @Directive({ selector: '[make-color-grey]', @@ -33,6 +33,36 @@ class MakeColorGreyDirective { toggle() { this._backgroundColor ? this.off() : this.on(); } } +@Component({selector: 'box-with-overridden-styles', template: '...'}) +class BoxWithOverriddenStylesComponent { + public active = false; + + @HostBinding('style') + styles = {}; + + constructor() { this.onInActive(); } + + @HostListener('click', ['$event']) + toggle() { + if (this.active) { + this.onInActive(); + } else { + this.onActive(); + } + markDirty(this); + } + + onActive() { + this.active = true; + this.styles = {height: '500px', 'font-size': '200px', background: 'red'}; + } + + onInActive() { + this.active = false; + this.styles = {width: '200px', height: '500px', border: '10px solid black', background: 'grey'}; + } +} + @Component({ selector: 'animation-world', template: ` @@ -48,7 +78,7 @@ class MakeColorGreyDirective { class="record" [style.transform]="item.active ? 'scale(1.5)' : 'none'" [class]="makeClass(item)" - style="border-radius: 10px" + style="border-radius: 10px" [style]="styles" [style.color]="item.value == 4 ? 'red' : null" [style.background-color]="item.value == 4 ? 'white' : null" @@ -56,6 +86,13 @@ class MakeColorGreyDirective { {{ item.value }}
+ +
+ + + `, }) class AnimationWorldComponent { @@ -93,8 +130,10 @@ class AnimationWorldComponent { } } -@NgModule( - {declarations: [AnimationWorldComponent, MakeColorGreyDirective], imports: [CommonModule]}) +@NgModule({ + declarations: [AnimationWorldComponent, MakeColorGreyDirective, BoxWithOverriddenStylesComponent], + imports: [CommonModule] +}) class AnimationWorldModule { } diff --git a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json index 9cd68b01b9..2d81d2aa30 100644 --- a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json +++ b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json @@ -170,6 +170,9 @@ { "name": "allocStylingContext" }, + { + "name": "allocateDirectiveIntoContext" + }, { "name": "appendChild" }, @@ -335,6 +338,9 @@ { "name": "getInitialClassNameValue" }, + { + "name": "getInitialStyleStringValue" + }, { "name": "getInjectorIndex" }, @@ -407,6 +413,9 @@ { "name": "hasParentInjector" }, + { + "name": "hasStyleInput" + }, { "name": "hasStyling" }, @@ -555,7 +564,10 @@ "name": "renderEmbeddedTemplate" }, { - "name": "renderInitialStylesAndClasses" + "name": "renderInitialClasses" + }, + { + "name": "renderInitialStyles" }, { "name": "renderInitialStylingValues" diff --git a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json index 5045c31c3a..896de5c568 100644 --- a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json @@ -44,9 +44,6 @@ { "name": "HOST" }, - { - "name": "T_HOST" - }, { "name": "INJECTOR" }, @@ -107,6 +104,9 @@ { "name": "TVIEW" }, + { + "name": "T_HOST" + }, { "name": "UnsubscriptionErrorImpl" }, diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 65fb980ad8..a4db8b2a37 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -362,9 +362,6 @@ { "name": "_symbolIterator" }, - { - "name": "_updateSingleStylingValue" - }, { "name": "addComponentLogic" }, @@ -386,6 +383,9 @@ { "name": "allocStylingContext" }, + { + "name": "allocateDirectiveIntoContext" + }, { "name": "allowValueChange" }, @@ -419,6 +419,9 @@ { "name": "bloomHashBitOrFactory" }, + { + "name": "booleanOrNull" + }, { "name": "cacheMatchingLocalNames" }, @@ -689,6 +692,9 @@ { "name": "getInitialIndex" }, + { + "name": "getInitialStyleStringValue" + }, { "name": "getInitialStylingValuesIndexOf" }, @@ -720,7 +726,7 @@ "name": "getMultiOrSingleIndex" }, { - "name": "getMultiStartIndex" + "name": "getMultiStylesStartIndex" }, { "name": "getNativeAnchorNode" @@ -839,6 +845,9 @@ { "name": "hasPlayerBuilderChanged" }, + { + "name": "hasStyleInput" + }, { "name": "hasStyling" }, @@ -848,12 +857,21 @@ { "name": "hasValueChanged" }, + { + "name": "hyphenate" + }, + { + "name": "hyphenateEntries" + }, { "name": "includeViewProviders" }, { "name": "increaseElementDepthCount" }, + { + "name": "initElementStyling" + }, { "name": "initNodeFlags" }, @@ -965,9 +983,6 @@ { "name": "leaveView" }, - { - "name": "limitToSingleClasses" - }, { "name": "listener" }, @@ -1104,7 +1119,10 @@ "name": "renderEmbeddedTemplate" }, { - "name": "renderInitialStylesAndClasses" + "name": "renderInitialClasses" + }, + { + "name": "renderInitialStyles" }, { "name": "renderInitialStylingValues" @@ -1256,6 +1274,9 @@ { "name": "updateContextWithBindings" }, + { + "name": "updateSingleStylingValue" + }, { "name": "valueExists" }, diff --git a/packages/core/test/render3/integration_spec.ts b/packages/core/test/render3/integration_spec.ts index 4550a91a47..21371db81e 100644 --- a/packages/core/test/render3/integration_spec.ts +++ b/packages/core/test/render3/integration_spec.ts @@ -1656,6 +1656,19 @@ describe('render3 integration test', () => { set klass(value: string) { this.classesVal = value; } } + let mockStyleDirective: DirWithStyleDirective; + class DirWithStyleDirective { + static ngDirectiveDef = defineDirective({ + type: DirWithStyleDirective, + selectors: [['', 'DirWithStyle', '']], + factory: () => mockStyleDirective = new DirWithStyleDirective(), + inputs: {'style': 'style'} + }); + + public stylesVal: string = ''; + set style(value: string) { this.stylesVal = value; } + } + it('should delegate initial classes to a [class] input binding if present on a directive on the same element', () => { /** @@ -1678,6 +1691,28 @@ describe('render3 integration test', () => { expect(mockClassDirective !.classesVal).toEqual('apple orange banana'); }); + it('should delegate initial styles to a [style] input binding if present on a directive on the same element', + () => { + /** + *
+ */ + const App = createComponent('app', function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + elementStart( + 0, 'div', + ['DirWithStyle', AttributeMarker.Styles, 'width', '100px', 'height', '200px']); + elementStyling(); + elementEnd(); + } + if (rf & RenderFlags.Update) { + elementStylingApply(0); + } + }, 1, 0, [DirWithStyleDirective]); + + const fixture = new ComponentFixture(App); + expect(mockStyleDirective !.stylesVal).toEqual('width:100px;height:200px'); + }); + it('should update `[class]` and bindings in the provided directive if the input is matched', () => { /** @@ -1699,6 +1734,27 @@ describe('render3 integration test', () => { expect(mockClassDirective !.classesVal).toEqual('cucumber grape'); }); + it('should update `[style]` and bindings in the provided directive if the input is matched', + () => { + /** + *
+ */ + const App = createComponent('app', function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + elementStart(0, 'div', ['DirWithStyle']); + elementStyling(); + elementEnd(); + } + if (rf & RenderFlags.Update) { + elementStylingMap(0, null, {width: '200px', height: '500px'}); + elementStylingApply(0); + } + }, 1, 0, [DirWithStyleDirective]); + + const fixture = new ComponentFixture(App); + expect(mockStyleDirective !.stylesVal).toEqual('width:200px;height:500px'); + }); + it('should apply initial styling to the element that contains the directive with host styling', () => { class DirWithInitialStyling { @@ -1723,7 +1779,7 @@ describe('render3 integration test', () => { /** *
+ * style="color:black; font-size:200px">
*/ const App = createComponent('app', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { @@ -1822,7 +1878,7 @@ describe('render3 integration test', () => { expect(target.classList.contains('xyz')).toBeTruthy(); }); - it('should properly prioritize style binding collision when they exist on multiple directives', + it('should properly prioritize single style binding collisions when they exist on multiple directives', () => { let dir1Instance: Dir1WithStyle; /** @@ -1873,7 +1929,8 @@ describe('render3 integration test', () => { } /** - *
+ * Component with the following template: + *
*/ const App = createComponent('app', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { @@ -1917,1048 +1974,1180 @@ describe('render3 integration test', () => { expect(target.style.getPropertyValue('width')).toEqual('777px'); }); - it('should properly handle and render interpolation for class attribute bindings', () => { - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { + it('should properly prioritize multi style binding collisions when they exist on multiple directives', + () => { + let dir1Instance: Dir1WithStyling; + /** + * Directive with host props: + * [style] + * [class] + */ + class Dir1WithStyling { + static ngDirectiveDef = defineDirective({ + type: Dir1WithStyling, + selectors: [['', 'Dir1WithStyling', '']], + factory: () => dir1Instance = new Dir1WithStyling(), + hostBindings: function(rf: RenderFlags, ctx: Dir1WithStyling, elementIndex: number) { + if (rf & RenderFlags.Create) { + elementStyling(null, null, null, ctx); + } + if (rf & RenderFlags.Update) { + elementStylingMap(elementIndex, ctx.classesExp, ctx.stylesExp, ctx); + elementStylingApply(elementIndex, ctx); + } + } + }); + + classesExp: any = {}; + stylesExp: any = {}; + } + + let dir2Instance: Dir2WithStyling; + /** + * Directive with host props: + * [style] + * style="width:111px" + */ + class Dir2WithStyling { + static ngDirectiveDef = defineDirective({ + type: Dir2WithStyling, + selectors: [['', 'Dir2WithStyling', '']], + factory: () => dir2Instance = new Dir2WithStyling(), + hostBindings: function(rf: RenderFlags, ctx: Dir2WithStyling, elementIndex: number) { + if (rf & RenderFlags.Create) { + elementHostAttrs(ctx, [AttributeMarker.Styles, 'width', '111px']); + elementStyling(null, null, null, ctx); + } + if (rf & RenderFlags.Update) { + elementStylingMap(elementIndex, null, ctx.stylesExp, ctx); + elementStylingApply(elementIndex, ctx); + } + } + }); + + stylesExp: any = {}; + } + + /** + * Component with the following template: + *
+ */ + const App = createComponent('app', function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + element(0, 'div', ['Dir1WithStyling', '', 'Dir2WithStyling', '']); + elementStyling(); + } + if (rf & RenderFlags.Update) { + elementStylingMap(0, ctx.classesExp, ctx.stylesExp); + elementStylingApply(0); + } + }, 1, 0, [Dir1WithStyling, Dir2WithStyling]); + + const fixture = new ComponentFixture(App); + const target = fixture.hostElement.querySelector('div') !; + expect(target.style.getPropertyValue('width')).toEqual('111px'); + + const compInstance = fixture.component; + compInstance.stylesExp = {width: '999px', height: null}; + compInstance.classesExp = {one: true, two: false}; + dir1Instance !.stylesExp = {width: '222px'}; + dir1Instance !.classesExp = {two: true, three: false}; + dir2Instance !.stylesExp = {width: '333px', height: '100px'}; + fixture.update(); + expect(target.style.getPropertyValue('width')).toEqual('999px'); + expect(target.style.getPropertyValue('height')).toEqual('100px'); + expect(target.classList.contains('one')).toBeTruthy(); + expect(target.classList.contains('two')).toBeFalsy(); + expect(target.classList.contains('three')).toBeFalsy(); + + compInstance.stylesExp = {}; + compInstance !.classesExp = {}; + dir1Instance !.stylesExp = {width: '222px', height: '200px'}; + fixture.update(); + expect(target.style.getPropertyValue('width')).toEqual('222px'); + expect(target.style.getPropertyValue('height')).toEqual('200px'); + expect(target.classList.contains('one')).toBeFalsy(); + expect(target.classList.contains('two')).toBeTruthy(); + expect(target.classList.contains('three')).toBeFalsy(); + + dir1Instance !.stylesExp = {}; + dir1Instance !.classesExp = {}; + fixture.update(); + expect(target.style.getPropertyValue('width')).toEqual('333px'); + expect(target.style.getPropertyValue('height')).toEqual('100px'); + expect(target.classList.contains('one')).toBeFalsy(); + expect(target.classList.contains('two')).toBeFalsy(); + expect(target.classList.contains('three')).toBeFalsy(); + + dir2Instance !.stylesExp = {}; + compInstance.stylesExp = {height: '900px'}; + fixture.update(); + expect(target.style.getPropertyValue('width')).toEqual('111px'); + expect(target.style.getPropertyValue('height')).toEqual('900px'); + + dir1Instance !.stylesExp = {width: '666px', height: '600px'}; + dir1Instance !.classesExp = {four: true, one: true}; + fixture.update(); + expect(target.style.getPropertyValue('width')).toEqual('666px'); + expect(target.style.getPropertyValue('height')).toEqual('900px'); + expect(target.classList.contains('one')).toBeTruthy(); + expect(target.classList.contains('two')).toBeFalsy(); + expect(target.classList.contains('three')).toBeFalsy(); + expect(target.classList.contains('four')).toBeTruthy(); + + compInstance.stylesExp = {width: '777px'}; + compInstance.classesExp = {four: false}; + fixture.update(); + expect(target.style.getPropertyValue('width')).toEqual('777px'); + expect(target.style.getPropertyValue('height')).toEqual('600px'); + expect(target.classList.contains('one')).toBeTruthy(); + expect(target.classList.contains('two')).toBeFalsy(); + expect(target.classList.contains('three')).toBeFalsy(); + expect(target.classList.contains('four')).toBeFalsy(); + }); + }); + + it('should properly handle and render interpolation for class attribute bindings', () => { + const App = createComponent('app', function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + elementStart(0, 'div'); + elementStyling(); + elementEnd(); + } + if (rf & RenderFlags.Update) { + elementStylingMap(0, interpolation2('-', ctx.name, '-', ctx.age, '-')); + elementStylingApply(0); + } + }, 1, 2); + + const fixture = new ComponentFixture(App); + const target = fixture.hostElement.querySelector('div') !; + expect(target.classList.contains('-fred-36-')).toBeFalsy(); + + fixture.component.name = 'fred'; + fixture.component.age = '36'; + fixture.update(); + + expect(target.classList.contains('-fred-36-')).toBeTruthy(); + }); + }); +}); + +describe('template data', () => { + + it('should re-use template data and node data', () => { + /** + * % if (condition) { + *
+ * % } + */ + function Template(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + container(0); + } + if (rf & RenderFlags.Update) { + containerRefreshStart(0); + { + if (ctx.condition) { + let rf1 = embeddedViewStart(0, 1, 0); + if (rf1 & RenderFlags.Create) { + element(0, 'div'); + } + embeddedViewEnd(); + } + } + containerRefreshEnd(); + } + } + + expect((Template as any).ngPrivateData).toBeUndefined(); + + renderToHtml(Template, {condition: true}, 1); + + const oldTemplateData = (Template as any).ngPrivateData; + const oldContainerData = (oldTemplateData as any).data[HEADER_OFFSET]; + const oldElementData = oldContainerData.tViews[0][HEADER_OFFSET]; + expect(oldContainerData).not.toBeNull(); + expect(oldElementData).not.toBeNull(); + + renderToHtml(Template, {condition: false}, 1); + renderToHtml(Template, {condition: true}, 1); + + const newTemplateData = (Template as any).ngPrivateData; + const newContainerData = (oldTemplateData as any).data[HEADER_OFFSET]; + const newElementData = oldContainerData.tViews[0][HEADER_OFFSET]; + expect(newTemplateData === oldTemplateData).toBe(true); + expect(newContainerData === oldContainerData).toBe(true); + expect(newElementData === oldElementData).toBe(true); + }); + +}); + +describe('component styles', () => { + it('should pass in the component styles directly into the underlying renderer', () => { + class StyledComp { + static ngComponentDef = defineComponent({ + type: StyledComp, + styles: ['div { color: red; }'], + consts: 1, + vars: 0, + encapsulation: 100, + selectors: [['foo']], + factory: () => new StyledComp(), + template: (rf: RenderFlags, ctx: StyledComp) => { + if (rf & RenderFlags.Create) { + element(0, 'div'); + } + } + }); + } + const rendererFactory = new ProxyRenderer3Factory(); + new ComponentFixture(StyledComp, {rendererFactory}); + expect(rendererFactory.lastCapturedType !.styles).toEqual(['div { color: red; }']); + expect(rendererFactory.lastCapturedType !.encapsulation).toEqual(100); + }); +}); + +describe('component animations', () => { + it('should pass in the component styles directly into the underlying renderer', () => { + const animA = {name: 'a'}; + const animB = {name: 'b'}; + + class AnimComp { + static ngComponentDef = defineComponent({ + type: AnimComp, + consts: 0, + vars: 0, + data: { + animation: [ + animA, + animB, + ], + }, + selectors: [['foo']], + factory: () => new AnimComp(), + template: (rf: RenderFlags, ctx: AnimComp) => {} + }); + } + const rendererFactory = new ProxyRenderer3Factory(); + new ComponentFixture(AnimComp, {rendererFactory}); + + const capturedAnimations = rendererFactory.lastCapturedType !.data !['animation']; + expect(Array.isArray(capturedAnimations)).toBeTruthy(); + expect(capturedAnimations.length).toEqual(2); + expect(capturedAnimations).toContain(animA); + expect(capturedAnimations).toContain(animB); + }); + + it('should include animations in the renderType data array even if the array is empty', () => { + class AnimComp { + static ngComponentDef = defineComponent({ + type: AnimComp, + consts: 0, + vars: 0, + data: { + animation: [], + }, + selectors: [['foo']], + factory: () => new AnimComp(), + template: (rf: RenderFlags, ctx: AnimComp) => {} + }); + } + const rendererFactory = new ProxyRenderer3Factory(); + new ComponentFixture(AnimComp, {rendererFactory}); + const data = rendererFactory.lastCapturedType !.data; + expect(data.animation).toEqual([]); + }); + + it('should allow [@trigger] bindings to be picked up by the underlying renderer', () => { + class AnimComp { + static ngComponentDef = defineComponent({ + type: AnimComp, + consts: 1, + vars: 1, + selectors: [['foo']], + factory: () => new AnimComp(), + template: (rf: RenderFlags, ctx: AnimComp) => { + if (rf & RenderFlags.Create) { + element(0, 'div', [AttributeMarker.SelectOnly, '@fooAnimation']); + } + if (rf & RenderFlags.Update) { + elementAttribute(0, '@fooAnimation', bind(ctx.animationValue)); + } + } + }); + + animationValue = '123'; + } + + const rendererFactory = new MockRendererFactory(['setAttribute']); + const fixture = new ComponentFixture(AnimComp, {rendererFactory}); + + const renderer = rendererFactory.lastRenderer !; + fixture.component.animationValue = '456'; + fixture.update(); + + const spy = renderer.spies['setAttribute']; + const [elm, attr, value] = spy.calls.mostRecent().args; + + expect(attr).toEqual('@fooAnimation'); + expect(value).toEqual('456'); + }); + + it('should allow creation-level [@trigger] properties to be picked up by the underlying renderer', + () => { + class AnimComp { + static ngComponentDef = defineComponent({ + type: AnimComp, + consts: 1, + vars: 1, + selectors: [['foo']], + factory: () => new AnimComp(), + template: (rf: RenderFlags, ctx: AnimComp) => { + if (rf & RenderFlags.Create) { + element(0, 'div', ['@fooAnimation', '']); + } + } + }); + } + + const rendererFactory = new MockRendererFactory(['setProperty']); + const fixture = new ComponentFixture(AnimComp, {rendererFactory}); + + const renderer = rendererFactory.lastRenderer !; + fixture.update(); + + const spy = renderer.spies['setProperty']; + const [elm, attr, value] = spy.calls.mostRecent().args; + expect(attr).toEqual('@fooAnimation'); + }); + + it('should allow host binding animations to be picked up and rendered', () => { + class ChildCompWithAnim { + static ngDirectiveDef = defineDirective({ + type: ChildCompWithAnim, + factory: () => new ChildCompWithAnim(), + selectors: [['child-comp-with-anim']], + hostBindings: function(rf: RenderFlags, ctx: any, elementIndex: number): void { + if (rf & RenderFlags.Update) { + elementProperty(0, '@fooAnim', ctx.exp); + } + }, + }); + + exp = 'go'; + } + + class ParentComp { + static ngComponentDef = defineComponent({ + type: ParentComp, + consts: 1, + vars: 1, + selectors: [['foo']], + factory: () => new ParentComp(), + template: (rf: RenderFlags, ctx: ParentComp) => { + if (rf & RenderFlags.Create) { + element(0, 'child-comp-with-anim'); + } + }, + directives: [ChildCompWithAnim] + }); + } + + const rendererFactory = new MockRendererFactory(['setProperty']); + const fixture = new ComponentFixture(ParentComp, {rendererFactory}); + + const renderer = rendererFactory.lastRenderer !; + fixture.update(); + + const spy = renderer.spies['setProperty']; + const [elm, attr, value] = spy.calls.mostRecent().args; + expect(attr).toEqual('@fooAnim'); + }); +}); + +describe('element discovery', () => { + it('should only monkey-patch immediate child nodes in a component', () => { + class StructuredComp { + static ngComponentDef = defineComponent({ + type: StructuredComp, + selectors: [['structured-comp']], + factory: () => new StructuredComp(), + consts: 2, + vars: 0, + template: (rf: RenderFlags, ctx: StructuredComp) => { if (rf & RenderFlags.Create) { elementStart(0, 'div'); - elementStyling(); + elementStart(1, 'p'); + elementEnd(); elementEnd(); } if (rf & RenderFlags.Update) { - elementStylingMap(0, interpolation2('-', ctx.name, '-', ctx.age, '-')); - elementStylingApply(0); } - }, 1, 2); - - const fixture = new ComponentFixture(App); - const target = fixture.hostElement.querySelector('div') !; - expect(target.classList.contains('-fred-36-')).toBeFalsy(); - - fixture.component.name = 'fred'; - fixture.component.age = '36'; - fixture.update(); - - expect(target.classList.contains('-fred-36-')).toBeTruthy(); + } }); - }); + } + + const fixture = new ComponentFixture(StructuredComp); + fixture.update(); + + const host = fixture.hostElement; + const parent = host.querySelector('div') as any; + const child = host.querySelector('p') as any; + + expect(parent[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); + expect(child[MONKEY_PATCH_KEY_NAME]).toBeFalsy(); }); - describe('template data', () => { - - it('should re-use template data and node data', () => { - /** - * % if (condition) { - *
- * % } - */ - function Template(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - container(0); + it('should only monkey-patch immediate child nodes in a sub component', () => { + class ChildComp { + static ngComponentDef = defineComponent({ + type: ChildComp, + selectors: [['child-comp']], + factory: () => new ChildComp(), + consts: 3, + vars: 0, + template: (rf: RenderFlags, ctx: ChildComp) => { + if (rf & RenderFlags.Create) { + element(0, 'div'); + element(1, 'div'); + element(2, 'div'); + } } - if (rf & RenderFlags.Update) { - containerRefreshStart(0); - { - if (ctx.condition) { - let rf1 = embeddedViewStart(0, 1, 0); - if (rf1 & RenderFlags.Create) { - element(0, 'div'); + }); + } + + class ParentComp { + static ngComponentDef = defineComponent({ + type: ParentComp, + selectors: [['parent-comp']], + directives: [ChildComp], + factory: () => new ParentComp(), + consts: 2, + vars: 0, + template: (rf: RenderFlags, ctx: ParentComp) => { + if (rf & RenderFlags.Create) { + elementStart(0, 'section'); + elementStart(1, 'child-comp'); + elementEnd(); + elementEnd(); + } + } + }); + } + + const fixture = new ComponentFixture(ParentComp); + fixture.update(); + + const host = fixture.hostElement; + const child = host.querySelector('child-comp') as any; + expect(child[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); + + const [kid1, kid2, kid3] = Array.from(host.querySelectorAll('child-comp > *')); + expect(kid1[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); + expect(kid2[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); + expect(kid3[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); + }); + + it('should only monkey-patch immediate child nodes in an embedded template container', () => { + class StructuredComp { + static ngComponentDef = defineComponent({ + type: StructuredComp, + selectors: [['structured-comp']], + directives: [NgIf], + factory: () => new StructuredComp(), + consts: 2, + vars: 1, + template: (rf: RenderFlags, ctx: StructuredComp) => { + if (rf & RenderFlags.Create) { + elementStart(0, 'section'); + template(1, (rf, ctx) => { + if (rf & RenderFlags.Create) { + elementStart(0, 'div'); + element(1, 'p'); + elementEnd(); + element(2, 'div'); } - embeddedViewEnd(); - } + }, 3, 0, 'ng-template', ['ngIf', '']); + elementEnd(); + } + if (rf & RenderFlags.Update) { + elementProperty(1, 'ngIf', true); } - containerRefreshEnd(); } - } + }); + } - expect((Template as any).ngPrivateData).toBeUndefined(); + const fixture = new ComponentFixture(StructuredComp); + fixture.update(); - renderToHtml(Template, {condition: true}, 1); + const host = fixture.hostElement; + const [section, div1, p, div2] = Array.from(host.querySelectorAll('section, div, p')); - const oldTemplateData = (Template as any).ngPrivateData; - const oldContainerData = (oldTemplateData as any).data[HEADER_OFFSET]; - const oldElementData = oldContainerData.tViews[0][HEADER_OFFSET]; - expect(oldContainerData).not.toBeNull(); - expect(oldElementData).not.toBeNull(); + expect(section.nodeName.toLowerCase()).toBe('section'); + expect(section[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); - renderToHtml(Template, {condition: false}, 1); - renderToHtml(Template, {condition: true}, 1); + expect(div1.nodeName.toLowerCase()).toBe('div'); + expect(div1[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); - const newTemplateData = (Template as any).ngPrivateData; - const newContainerData = (oldTemplateData as any).data[HEADER_OFFSET]; - const newElementData = oldContainerData.tViews[0][HEADER_OFFSET]; - expect(newTemplateData === oldTemplateData).toBe(true); - expect(newContainerData === oldContainerData).toBe(true); - expect(newElementData === oldElementData).toBe(true); - }); + expect(p.nodeName.toLowerCase()).toBe('p'); + expect(p[MONKEY_PATCH_KEY_NAME]).toBeFalsy(); + expect(div2.nodeName.toLowerCase()).toBe('div'); + expect(div2[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); }); - describe('component styles', () => { - it('should pass in the component styles directly into the underlying renderer', () => { - class StyledComp { - static ngComponentDef = defineComponent({ - type: StyledComp, - styles: ['div { color: red; }'], - consts: 1, - vars: 0, - encapsulation: 100, - selectors: [['foo']], - factory: () => new StyledComp(), - template: (rf: RenderFlags, ctx: StyledComp) => { - if (rf & RenderFlags.Create) { - element(0, 'div'); - } + it('should return a context object from a given dom node', () => { + class StructuredComp { + static ngComponentDef = defineComponent({ + type: StructuredComp, + selectors: [['structured-comp']], + directives: [NgIf], + factory: () => new StructuredComp(), + consts: 2, + vars: 0, + template: (rf: RenderFlags, ctx: StructuredComp) => { + if (rf & RenderFlags.Create) { + element(0, 'section'); + element(1, 'div'); } - }); - } - const rendererFactory = new ProxyRenderer3Factory(); - new ComponentFixture(StyledComp, {rendererFactory}); - expect(rendererFactory.lastCapturedType !.styles).toEqual(['div { color: red; }']); - expect(rendererFactory.lastCapturedType !.encapsulation).toEqual(100); - }); + } + }); + } + + const fixture = new ComponentFixture(StructuredComp); + fixture.update(); + + const section = fixture.hostElement.querySelector('section') !; + const sectionContext = getLContext(section) !; + const sectionLView = sectionContext.lView !; + expect(sectionContext.nodeIndex).toEqual(HEADER_OFFSET); + expect(sectionLView.length).toBeGreaterThan(HEADER_OFFSET); + expect(sectionContext.native).toBe(section); + + const div = fixture.hostElement.querySelector('div') !; + const divContext = getLContext(div) !; + const divLView = divContext.lView !; + expect(divContext.nodeIndex).toEqual(HEADER_OFFSET + 1); + expect(divLView.length).toBeGreaterThan(HEADER_OFFSET); + expect(divContext.native).toBe(div); + + expect(divLView).toBe(sectionLView); }); - describe('component animations', () => { - it('should pass in the component styles directly into the underlying renderer', () => { - const animA = {name: 'a'}; - const animB = {name: 'b'}; - - class AnimComp { - static ngComponentDef = defineComponent({ - type: AnimComp, - consts: 0, - vars: 0, - data: { - animation: [ - animA, - animB, - ], - }, - selectors: [['foo']], - factory: () => new AnimComp(), - template: (rf: RenderFlags, ctx: AnimComp) => {} - }); - } - const rendererFactory = new ProxyRenderer3Factory(); - new ComponentFixture(AnimComp, {rendererFactory}); - - const capturedAnimations = rendererFactory.lastCapturedType !.data !['animation']; - expect(Array.isArray(capturedAnimations)).toBeTruthy(); - expect(capturedAnimations.length).toEqual(2); - expect(capturedAnimations).toContain(animA); - expect(capturedAnimations).toContain(animB); - }); - - it('should include animations in the renderType data array even if the array is empty', () => { - class AnimComp { - static ngComponentDef = defineComponent({ - type: AnimComp, - consts: 0, - vars: 0, - data: { - animation: [], - }, - selectors: [['foo']], - factory: () => new AnimComp(), - template: (rf: RenderFlags, ctx: AnimComp) => {} - }); - } - const rendererFactory = new ProxyRenderer3Factory(); - new ComponentFixture(AnimComp, {rendererFactory}); - const data = rendererFactory.lastCapturedType !.data; - expect(data.animation).toEqual([]); - }); - - it('should allow [@trigger] bindings to be picked up by the underlying renderer', () => { - class AnimComp { - static ngComponentDef = defineComponent({ - type: AnimComp, - consts: 1, - vars: 1, - selectors: [['foo']], - factory: () => new AnimComp(), - template: (rf: RenderFlags, ctx: AnimComp) => { - if (rf & RenderFlags.Create) { - element(0, 'div', [AttributeMarker.SelectOnly, '@fooAnimation']); - } - if (rf & RenderFlags.Update) { - elementAttribute(0, '@fooAnimation', bind(ctx.animationValue)); - } + it('should cache the element context on a element was pre-emptively monkey-patched', () => { + class StructuredComp { + static ngComponentDef = defineComponent({ + type: StructuredComp, + selectors: [['structured-comp']], + factory: () => new StructuredComp(), + consts: 1, + vars: 0, + template: (rf: RenderFlags, ctx: StructuredComp) => { + if (rf & RenderFlags.Create) { + element(0, 'section'); } - }); + } + }); + } - animationValue = '123'; - } + const fixture = new ComponentFixture(StructuredComp); + fixture.update(); - const rendererFactory = new MockRendererFactory(['setAttribute']); - const fixture = new ComponentFixture(AnimComp, {rendererFactory}); + const section = fixture.hostElement.querySelector('section') !as any; + const result1 = section[MONKEY_PATCH_KEY_NAME]; + expect(Array.isArray(result1)).toBeTruthy(); - const renderer = rendererFactory.lastRenderer !; - fixture.component.animationValue = '456'; - fixture.update(); + const context = getLContext(section) !; + const result2 = section[MONKEY_PATCH_KEY_NAME]; + expect(Array.isArray(result2)).toBeFalsy(); - const spy = renderer.spies['setAttribute']; - const [elm, attr, value] = spy.calls.mostRecent().args; - - expect(attr).toEqual('@fooAnimation'); - expect(value).toEqual('456'); - }); - - it('should allow creation-level [@trigger] properties to be picked up by the underlying renderer', - () => { - class AnimComp { - static ngComponentDef = defineComponent({ - type: AnimComp, - consts: 1, - vars: 1, - selectors: [['foo']], - factory: () => new AnimComp(), - template: (rf: RenderFlags, ctx: AnimComp) => { - if (rf & RenderFlags.Create) { - element(0, 'div', ['@fooAnimation', '']); - } - } - }); - } - - const rendererFactory = new MockRendererFactory(['setProperty']); - const fixture = new ComponentFixture(AnimComp, {rendererFactory}); - - const renderer = rendererFactory.lastRenderer !; - fixture.update(); - - const spy = renderer.spies['setProperty']; - const [elm, attr, value] = spy.calls.mostRecent().args; - expect(attr).toEqual('@fooAnimation'); - }); - - it('should allow host binding animations to be picked up and rendered', () => { - class ChildCompWithAnim { - static ngDirectiveDef = defineDirective({ - type: ChildCompWithAnim, - factory: () => new ChildCompWithAnim(), - selectors: [['child-comp-with-anim']], - hostBindings: function(rf: RenderFlags, ctx: any, elementIndex: number): void { - if (rf & RenderFlags.Update) { - elementProperty(0, '@fooAnim', ctx.exp); - } - }, - }); - - exp = 'go'; - } - - class ParentComp { - static ngComponentDef = defineComponent({ - type: ParentComp, - consts: 1, - vars: 1, - selectors: [['foo']], - factory: () => new ParentComp(), - template: (rf: RenderFlags, ctx: ParentComp) => { - if (rf & RenderFlags.Create) { - element(0, 'child-comp-with-anim'); - } - }, - directives: [ChildCompWithAnim] - }); - } - - const rendererFactory = new MockRendererFactory(['setProperty']); - const fixture = new ComponentFixture(ParentComp, {rendererFactory}); - - const renderer = rendererFactory.lastRenderer !; - fixture.update(); - - const spy = renderer.spies['setProperty']; - const [elm, attr, value] = spy.calls.mostRecent().args; - expect(attr).toEqual('@fooAnim'); - }); + expect(result2).toBe(context); + expect(result2.lView).toBe(result1); }); - describe('element discovery', () => { - it('should only monkey-patch immediate child nodes in a component', () => { - class StructuredComp { - static ngComponentDef = defineComponent({ - type: StructuredComp, - selectors: [['structured-comp']], - factory: () => new StructuredComp(), - consts: 2, - vars: 0, - template: (rf: RenderFlags, ctx: StructuredComp) => { - if (rf & RenderFlags.Create) { - elementStart(0, 'div'); - elementStart(1, 'p'); - elementEnd(); - elementEnd(); - } - if (rf & RenderFlags.Update) { - } - } - }); - } - - const fixture = new ComponentFixture(StructuredComp); - fixture.update(); - - const host = fixture.hostElement; - const parent = host.querySelector('div') as any; - const child = host.querySelector('p') as any; - - expect(parent[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); - expect(child[MONKEY_PATCH_KEY_NAME]).toBeFalsy(); - }); - - it('should only monkey-patch immediate child nodes in a sub component', () => { - class ChildComp { - static ngComponentDef = defineComponent({ - type: ChildComp, - selectors: [['child-comp']], - factory: () => new ChildComp(), - consts: 3, - vars: 0, - template: (rf: RenderFlags, ctx: ChildComp) => { - if (rf & RenderFlags.Create) { - element(0, 'div'); - element(1, 'div'); - element(2, 'div'); - } - } - }); - } - - class ParentComp { - static ngComponentDef = defineComponent({ - type: ParentComp, - selectors: [['parent-comp']], - directives: [ChildComp], - factory: () => new ParentComp(), - consts: 2, - vars: 0, - template: (rf: RenderFlags, ctx: ParentComp) => { - if (rf & RenderFlags.Create) { - elementStart(0, 'section'); - elementStart(1, 'child-comp'); - elementEnd(); - elementEnd(); - } - } - }); - } - - const fixture = new ComponentFixture(ParentComp); - fixture.update(); - - const host = fixture.hostElement; - const child = host.querySelector('child-comp') as any; - expect(child[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); - - const [kid1, kid2, kid3] = Array.from(host.querySelectorAll('child-comp > *')); - expect(kid1[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); - expect(kid2[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); - expect(kid3[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); - }); - - it('should only monkey-patch immediate child nodes in an embedded template container', () => { - class StructuredComp { - static ngComponentDef = defineComponent({ - type: StructuredComp, - selectors: [['structured-comp']], - directives: [NgIf], - factory: () => new StructuredComp(), - consts: 2, - vars: 1, - template: (rf: RenderFlags, ctx: StructuredComp) => { - if (rf & RenderFlags.Create) { - elementStart(0, 'section'); - template(1, (rf, ctx) => { - if (rf & RenderFlags.Create) { - elementStart(0, 'div'); - element(1, 'p'); - elementEnd(); - element(2, 'div'); - } - }, 3, 0, 'ng-template', ['ngIf', '']); - elementEnd(); - } - if (rf & RenderFlags.Update) { - elementProperty(1, 'ngIf', true); - } - } - }); - } - - const fixture = new ComponentFixture(StructuredComp); - fixture.update(); - - const host = fixture.hostElement; - const [section, div1, p, div2] = Array.from(host.querySelectorAll('section, div, p')); - - expect(section.nodeName.toLowerCase()).toBe('section'); - expect(section[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); - - expect(div1.nodeName.toLowerCase()).toBe('div'); - expect(div1[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); - - expect(p.nodeName.toLowerCase()).toBe('p'); - expect(p[MONKEY_PATCH_KEY_NAME]).toBeFalsy(); - - expect(div2.nodeName.toLowerCase()).toBe('div'); - expect(div2[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); - }); - - it('should return a context object from a given dom node', () => { - class StructuredComp { - static ngComponentDef = defineComponent({ - type: StructuredComp, - selectors: [['structured-comp']], - directives: [NgIf], - factory: () => new StructuredComp(), - consts: 2, - vars: 0, - template: (rf: RenderFlags, ctx: StructuredComp) => { - if (rf & RenderFlags.Create) { - element(0, 'section'); - element(1, 'div'); - } - } - }); - } - - const fixture = new ComponentFixture(StructuredComp); - fixture.update(); - - const section = fixture.hostElement.querySelector('section') !; - const sectionContext = getLContext(section) !; - const sectionLView = sectionContext.lView !; - expect(sectionContext.nodeIndex).toEqual(HEADER_OFFSET); - expect(sectionLView.length).toBeGreaterThan(HEADER_OFFSET); - expect(sectionContext.native).toBe(section); - - const div = fixture.hostElement.querySelector('div') !; - const divContext = getLContext(div) !; - const divLView = divContext.lView !; - expect(divContext.nodeIndex).toEqual(HEADER_OFFSET + 1); - expect(divLView.length).toBeGreaterThan(HEADER_OFFSET); - expect(divContext.native).toBe(div); - - expect(divLView).toBe(sectionLView); - }); - - it('should cache the element context on a element was pre-emptively monkey-patched', () => { - class StructuredComp { - static ngComponentDef = defineComponent({ - type: StructuredComp, - selectors: [['structured-comp']], - factory: () => new StructuredComp(), - consts: 1, - vars: 0, - template: (rf: RenderFlags, ctx: StructuredComp) => { - if (rf & RenderFlags.Create) { - element(0, 'section'); - } - } - }); - } - - const fixture = new ComponentFixture(StructuredComp); - fixture.update(); - - const section = fixture.hostElement.querySelector('section') !as any; - const result1 = section[MONKEY_PATCH_KEY_NAME]; - expect(Array.isArray(result1)).toBeTruthy(); - - const context = getLContext(section) !; - const result2 = section[MONKEY_PATCH_KEY_NAME]; - expect(Array.isArray(result2)).toBeFalsy(); - - expect(result2).toBe(context); - expect(result2.lView).toBe(result1); - }); - - it('should cache the element context on an intermediate element that isn\'t pre-emptively monkey-patched', - () => { - class StructuredComp { - static ngComponentDef = defineComponent({ - type: StructuredComp, - selectors: [['structured-comp']], - factory: () => new StructuredComp(), - consts: 2, - vars: 0, - template: (rf: RenderFlags, ctx: StructuredComp) => { - if (rf & RenderFlags.Create) { - elementStart(0, 'section'); - element(1, 'p'); - elementEnd(); - } + it('should cache the element context on an intermediate element that isn\'t pre-emptively monkey-patched', + () => { + class StructuredComp { + static ngComponentDef = defineComponent({ + type: StructuredComp, + selectors: [['structured-comp']], + factory: () => new StructuredComp(), + consts: 2, + vars: 0, + template: (rf: RenderFlags, ctx: StructuredComp) => { + if (rf & RenderFlags.Create) { + elementStart(0, 'section'); + element(1, 'p'); + elementEnd(); } - }); - } + } + }); + } - const fixture = new ComponentFixture(StructuredComp); - fixture.update(); + const fixture = new ComponentFixture(StructuredComp); + fixture.update(); - const section = fixture.hostElement.querySelector('section') !as any; - expect(section[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); + const section = fixture.hostElement.querySelector('section') !as any; + expect(section[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); - const p = fixture.hostElement.querySelector('p') !as any; - expect(p[MONKEY_PATCH_KEY_NAME]).toBeFalsy(); + const p = fixture.hostElement.querySelector('p') !as any; + expect(p[MONKEY_PATCH_KEY_NAME]).toBeFalsy(); - const pContext = getLContext(p) !; - expect(pContext.native).toBe(p); - expect(p[MONKEY_PATCH_KEY_NAME]).toBe(pContext); - }); + const pContext = getLContext(p) !; + expect(pContext.native).toBe(p); + expect(p[MONKEY_PATCH_KEY_NAME]).toBe(pContext); + }); - it('should be able to pull in element context data even if the element is decorated using styling', - () => { - class StructuredComp { - static ngComponentDef = defineComponent({ - type: StructuredComp, - selectors: [['structured-comp']], - factory: () => new StructuredComp(), - consts: 1, - vars: 0, - template: (rf: RenderFlags, ctx: StructuredComp) => { - if (rf & RenderFlags.Create) { - elementStart(0, 'section'); - elementStyling(['class-foo']); - elementEnd(); - } - if (rf & RenderFlags.Update) { - elementStylingApply(0); - } + it('should be able to pull in element context data even if the element is decorated using styling', + () => { + class StructuredComp { + static ngComponentDef = defineComponent({ + type: StructuredComp, + selectors: [['structured-comp']], + factory: () => new StructuredComp(), + consts: 1, + vars: 0, + template: (rf: RenderFlags, ctx: StructuredComp) => { + if (rf & RenderFlags.Create) { + elementStart(0, 'section'); + elementStyling(['class-foo']); + elementEnd(); } - }); - } - - const fixture = new ComponentFixture(StructuredComp); - fixture.update(); - - const section = fixture.hostElement.querySelector('section') !as any; - const result1 = section[MONKEY_PATCH_KEY_NAME]; - expect(Array.isArray(result1)).toBeTruthy(); - - const elementResult = result1[HEADER_OFFSET]; // first element - expect(Array.isArray(elementResult)).toBeTruthy(); - expect(elementResult[StylingIndex.ElementPosition]).toBe(section); - - const context = getLContext(section) !; - const result2 = section[MONKEY_PATCH_KEY_NAME]; - expect(Array.isArray(result2)).toBeFalsy(); - - expect(context.native).toBe(section); - }); - - it('should monkey-patch immediate child nodes in a content-projected region with a reference to the parent component', - () => { - /* - -
- - welcome -
-

-

this content is projected

- this content is projected also -

-
-
-
- */ - class ProjectorComp { - static ngComponentDef = defineComponent({ - type: ProjectorComp, - selectors: [['projector-comp']], - factory: () => new ProjectorComp(), - consts: 4, - vars: 0, - template: (rf: RenderFlags, ctx: ProjectorComp) => { - if (rf & RenderFlags.Create) { - projectionDef(); - text(0, 'welcome'); - elementStart(1, 'header'); - elementStart(2, 'h1'); - projection(3); - elementEnd(); - elementEnd(); - } - if (rf & RenderFlags.Update) { - } + if (rf & RenderFlags.Update) { + elementStylingApply(0); } - }); - } + } + }); + } - class ParentComp { - static ngComponentDef = defineComponent({ - type: ParentComp, - selectors: [['parent-comp']], - directives: [ProjectorComp], - factory: () => new ParentComp(), - consts: 5, - vars: 0, - template: (rf: RenderFlags, ctx: ParentComp) => { - if (rf & RenderFlags.Create) { - elementStart(0, 'section'); - elementStart(1, 'projector-comp'); - elementStart(2, 'p'); - text(3, 'this content is projected'); - elementEnd(); - text(4, 'this content is projected also'); - elementEnd(); - elementEnd(); - } + const fixture = new ComponentFixture(StructuredComp); + fixture.update(); + + const section = fixture.hostElement.querySelector('section') !as any; + const result1 = section[MONKEY_PATCH_KEY_NAME]; + expect(Array.isArray(result1)).toBeTruthy(); + + const elementResult = result1[HEADER_OFFSET]; // first element + expect(Array.isArray(elementResult)).toBeTruthy(); + expect(elementResult[StylingIndex.ElementPosition]).toBe(section); + + const context = getLContext(section) !; + const result2 = section[MONKEY_PATCH_KEY_NAME]; + expect(Array.isArray(result2)).toBeFalsy(); + + expect(context.native).toBe(section); + }); + + it('should monkey-patch immediate child nodes in a content-projected region with a reference to the parent component', + () => { + /* + +
+ + welcome +
+

+

this content is projected

+ this content is projected also +

+
+
+
+ */ + class ProjectorComp { + static ngComponentDef = defineComponent({ + type: ProjectorComp, + selectors: [['projector-comp']], + factory: () => new ProjectorComp(), + consts: 4, + vars: 0, + template: (rf: RenderFlags, ctx: ProjectorComp) => { + if (rf & RenderFlags.Create) { + projectionDef(); + text(0, 'welcome'); + elementStart(1, 'header'); + elementStart(2, 'h1'); + projection(3); + elementEnd(); + elementEnd(); } - }); - } - - const fixture = new ComponentFixture(ParentComp); - fixture.update(); - - const host = fixture.hostElement; - const textNode = host.firstChild as any; - const section = host.querySelector('section') !as any; - const projectorComp = host.querySelector('projector-comp') !as any; - const header = host.querySelector('header') !as any; - const h1 = host.querySelector('h1') !as any; - const p = host.querySelector('p') !as any; - const pText = p.firstChild as any; - const projectedTextNode = p.nextSibling; - - expect(projectorComp.children).toContain(header); - expect(h1.children).toContain(p); - - expect(textNode[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); - expect(section[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); - expect(projectorComp[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); - expect(header[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); - expect(h1[MONKEY_PATCH_KEY_NAME]).toBeFalsy(); - expect(p[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); - expect(pText[MONKEY_PATCH_KEY_NAME]).toBeFalsy(); - expect(projectedTextNode[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); - - const parentContext = getLContext(section) !; - const shadowContext = getLContext(header) !; - const projectedContext = getLContext(p) !; - - const parentComponentData = parentContext.lView; - const shadowComponentData = shadowContext.lView; - const projectedComponentData = projectedContext.lView; - - expect(projectedComponentData).toBe(parentComponentData); - expect(shadowComponentData).not.toBe(parentComponentData); - }); - - it('should return `null` when an element context is retrieved that isn\'t situated in Angular', - () => { - const elm1 = document.createElement('div'); - const context1 = getLContext(elm1); - expect(context1).toBeFalsy(); - - const elm2 = document.createElement('div'); - document.body.appendChild(elm2); - const context2 = getLContext(elm2); - expect(context2).toBeFalsy(); - }); - - it('should return `null` when an element context is retrieved that is a DOM node that was not created by Angular', - () => { - class StructuredComp { - static ngComponentDef = defineComponent({ - type: StructuredComp, - selectors: [['structured-comp']], - factory: () => new StructuredComp(), - consts: 1, - vars: 0, - template: (rf: RenderFlags, ctx: StructuredComp) => { - if (rf & RenderFlags.Create) { - element(0, 'section'); - } + if (rf & RenderFlags.Update) { } - }); - } + } + }); + } - const fixture = new ComponentFixture(StructuredComp); - fixture.update(); - - const section = fixture.hostElement.querySelector('section') !as any; - const manuallyCreatedElement = document.createElement('div'); - section.appendChild(manuallyCreatedElement); - - const context = getLContext(manuallyCreatedElement); - expect(context).toBeFalsy(); - }); - - it('should by default monkey-patch the bootstrap component with context details', () => { - class StructuredComp { - static ngComponentDef = defineComponent({ - type: StructuredComp, - selectors: [['structured-comp']], - factory: () => new StructuredComp(), - consts: 0, - vars: 0, - template: (rf: RenderFlags, ctx: StructuredComp) => {} - }); - } - - const fixture = new ComponentFixture(StructuredComp); - fixture.update(); - - const hostElm = fixture.hostElement; - const component = fixture.component; - - const componentLView = (component as any)[MONKEY_PATCH_KEY_NAME]; - expect(Array.isArray(componentLView)).toBeTruthy(); - - const hostLView = (hostElm as any)[MONKEY_PATCH_KEY_NAME]; - expect(hostLView).toBe(componentLView); - - const context1 = getLContext(hostElm) !; - expect(context1.lView).toBe(hostLView); - expect(context1.native).toEqual(hostElm); - - const context2 = getLContext(component) !; - expect(context2).toBe(context1); - expect(context2.lView).toBe(hostLView); - expect(context2.native).toEqual(hostElm); - }); - - it('should by default monkey-patch the directives with LView so that they can be examined', - () => { - let myDir1Instance: MyDir1|null = null; - let myDir2Instance: MyDir2|null = null; - let myDir3Instance: MyDir2|null = null; - - class MyDir1 { - static ngDirectiveDef = defineDirective({ - type: MyDir1, - selectors: [['', 'my-dir-1', '']], - factory: () => myDir1Instance = new MyDir1() - }); - } - - class MyDir2 { - static ngDirectiveDef = defineDirective({ - type: MyDir2, - selectors: [['', 'my-dir-2', '']], - factory: () => myDir2Instance = new MyDir2() - }); - } - - class MyDir3 { - static ngDirectiveDef = defineDirective({ - type: MyDir3, - selectors: [['', 'my-dir-3', '']], - factory: () => myDir3Instance = new MyDir2() - }); - } - - class StructuredComp { - static ngComponentDef = defineComponent({ - type: StructuredComp, - selectors: [['structured-comp']], - directives: [MyDir1, MyDir2, MyDir3], - factory: () => new StructuredComp(), - consts: 2, - vars: 0, - template: (rf: RenderFlags, ctx: StructuredComp) => { - if (rf & RenderFlags.Create) { - element(0, 'div', ['my-dir-1', '', 'my-dir-2', '']); - element(1, 'div', ['my-dir-3']); - } + class ParentComp { + static ngComponentDef = defineComponent({ + type: ParentComp, + selectors: [['parent-comp']], + directives: [ProjectorComp], + factory: () => new ParentComp(), + consts: 5, + vars: 0, + template: (rf: RenderFlags, ctx: ParentComp) => { + if (rf & RenderFlags.Create) { + elementStart(0, 'section'); + elementStart(1, 'projector-comp'); + elementStart(2, 'p'); + text(3, 'this content is projected'); + elementEnd(); + text(4, 'this content is projected also'); + elementEnd(); + elementEnd(); } - }); - } + } + }); + } - const fixture = new ComponentFixture(StructuredComp); - fixture.update(); + const fixture = new ComponentFixture(ParentComp); + fixture.update(); - const hostElm = fixture.hostElement; - const div1 = hostElm.querySelector('div:first-child') !as any; - const div2 = hostElm.querySelector('div:last-child') !as any; - const context = getLContext(hostElm) !; - const componentView = context.lView[context.nodeIndex]; + const host = fixture.hostElement; + const textNode = host.firstChild as any; + const section = host.querySelector('section') !as any; + const projectorComp = host.querySelector('projector-comp') !as any; + const header = host.querySelector('header') !as any; + const h1 = host.querySelector('h1') !as any; + const p = host.querySelector('p') !as any; + const pText = p.firstChild as any; + const projectedTextNode = p.nextSibling; - expect(componentView).toContain(myDir1Instance); - expect(componentView).toContain(myDir2Instance); - expect(componentView).toContain(myDir3Instance); + expect(projectorComp.children).toContain(header); + expect(h1.children).toContain(p); - expect(Array.isArray((myDir1Instance as any)[MONKEY_PATCH_KEY_NAME])).toBeTruthy(); - expect(Array.isArray((myDir2Instance as any)[MONKEY_PATCH_KEY_NAME])).toBeTruthy(); - expect(Array.isArray((myDir3Instance as any)[MONKEY_PATCH_KEY_NAME])).toBeTruthy(); + expect(textNode[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); + expect(section[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); + expect(projectorComp[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); + expect(header[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); + expect(h1[MONKEY_PATCH_KEY_NAME]).toBeFalsy(); + expect(p[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); + expect(pText[MONKEY_PATCH_KEY_NAME]).toBeFalsy(); + expect(projectedTextNode[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); - const d1Context = getLContext(myDir1Instance) !; - const d2Context = getLContext(myDir2Instance) !; - const d3Context = getLContext(myDir3Instance) !; + const parentContext = getLContext(section) !; + const shadowContext = getLContext(header) !; + const projectedContext = getLContext(p) !; - expect(d1Context.lView).toEqual(componentView); - expect(d2Context.lView).toEqual(componentView); - expect(d3Context.lView).toEqual(componentView); + const parentComponentData = parentContext.lView; + const shadowComponentData = shadowContext.lView; + const projectedComponentData = projectedContext.lView; - expect((myDir1Instance as any)[MONKEY_PATCH_KEY_NAME]).toBe(d1Context); - expect((myDir2Instance as any)[MONKEY_PATCH_KEY_NAME]).toBe(d2Context); - expect((myDir3Instance as any)[MONKEY_PATCH_KEY_NAME]).toBe(d3Context); + expect(projectedComponentData).toBe(parentComponentData); + expect(shadowComponentData).not.toBe(parentComponentData); + }); - expect(d1Context.nodeIndex).toEqual(HEADER_OFFSET); - expect(d1Context.native).toBe(div1); - expect(d1Context.directives as any[]).toEqual([myDir1Instance, myDir2Instance]); + it('should return `null` when an element context is retrieved that isn\'t situated in Angular', + () => { + const elm1 = document.createElement('div'); + const context1 = getLContext(elm1); + expect(context1).toBeFalsy(); - expect(d2Context.nodeIndex).toEqual(HEADER_OFFSET); - expect(d2Context.native).toBe(div1); - expect(d2Context.directives as any[]).toEqual([myDir1Instance, myDir2Instance]); + const elm2 = document.createElement('div'); + document.body.appendChild(elm2); + const context2 = getLContext(elm2); + expect(context2).toBeFalsy(); + }); - expect(d3Context.nodeIndex).toEqual(HEADER_OFFSET + 1); - expect(d3Context.native).toBe(div2); - expect(d3Context.directives as any[]).toEqual([myDir3Instance]); - }); - - it('should monkey-patch the exact same context instance of the DOM node, component and any directives on the same element', - () => { - let myDir1Instance: MyDir1|null = null; - let myDir2Instance: MyDir2|null = null; - let childComponentInstance: ChildComp|null = null; - - class MyDir1 { - static ngDirectiveDef = defineDirective({ - type: MyDir1, - selectors: [['', 'my-dir-1', '']], - factory: () => myDir1Instance = new MyDir1() - }); - } - - class MyDir2 { - static ngDirectiveDef = defineDirective({ - type: MyDir2, - selectors: [['', 'my-dir-2', '']], - factory: () => myDir2Instance = new MyDir2() - }); - } - - class ChildComp { - static ngComponentDef = defineComponent({ - type: ChildComp, - selectors: [['child-comp']], - factory: () => childComponentInstance = new ChildComp(), - consts: 1, - vars: 0, - template: (rf: RenderFlags, ctx: ChildComp) => { - if (rf & RenderFlags.Create) { - element(0, 'div'); - } + it('should return `null` when an element context is retrieved that is a DOM node that was not created by Angular', + () => { + class StructuredComp { + static ngComponentDef = defineComponent({ + type: StructuredComp, + selectors: [['structured-comp']], + factory: () => new StructuredComp(), + consts: 1, + vars: 0, + template: (rf: RenderFlags, ctx: StructuredComp) => { + if (rf & RenderFlags.Create) { + element(0, 'section'); } - }); - } + } + }); + } - class ParentComp { - static ngComponentDef = defineComponent({ - type: ParentComp, - selectors: [['parent-comp']], - directives: [ChildComp, MyDir1, MyDir2], - factory: () => new ParentComp(), - consts: 1, - vars: 0, - template: (rf: RenderFlags, ctx: ParentComp) => { - if (rf & RenderFlags.Create) { - element(0, 'child-comp', ['my-dir-1', '', 'my-dir-2', '']); - } - } - }); - } + const fixture = new ComponentFixture(StructuredComp); + fixture.update(); - const fixture = new ComponentFixture(ParentComp); - fixture.update(); + const section = fixture.hostElement.querySelector('section') !as any; + const manuallyCreatedElement = document.createElement('div'); + section.appendChild(manuallyCreatedElement); - const childCompHostElm = fixture.hostElement.querySelector('child-comp') !as any; + const context = getLContext(manuallyCreatedElement); + expect(context).toBeFalsy(); + }); - const lView = childCompHostElm[MONKEY_PATCH_KEY_NAME]; - expect(Array.isArray(lView)).toBeTruthy(); - expect((myDir1Instance as any)[MONKEY_PATCH_KEY_NAME]).toBe(lView); - expect((myDir2Instance as any)[MONKEY_PATCH_KEY_NAME]).toBe(lView); - expect((childComponentInstance as any)[MONKEY_PATCH_KEY_NAME]).toBe(lView); + it('should by default monkey-patch the bootstrap component with context details', () => { + class StructuredComp { + static ngComponentDef = defineComponent({ + type: StructuredComp, + selectors: [['structured-comp']], + factory: () => new StructuredComp(), + consts: 0, + vars: 0, + template: (rf: RenderFlags, ctx: StructuredComp) => {} + }); + } - const childNodeContext = getLContext(childCompHostElm) !; - expect(childNodeContext.component).toBeFalsy(); - expect(childNodeContext.directives).toBeFalsy(); - assertMonkeyPatchValueIsLView(myDir1Instance); - assertMonkeyPatchValueIsLView(myDir2Instance); - assertMonkeyPatchValueIsLView(childComponentInstance); + const fixture = new ComponentFixture(StructuredComp); + fixture.update(); - expect(getLContext(myDir1Instance)).toBe(childNodeContext); - expect(childNodeContext.component).toBeFalsy(); - expect(childNodeContext.directives !.length).toEqual(2); - assertMonkeyPatchValueIsLView(myDir1Instance, false); - assertMonkeyPatchValueIsLView(myDir2Instance, false); - assertMonkeyPatchValueIsLView(childComponentInstance); + const hostElm = fixture.hostElement; + const component = fixture.component; - expect(getLContext(myDir2Instance)).toBe(childNodeContext); - expect(childNodeContext.component).toBeFalsy(); - expect(childNodeContext.directives !.length).toEqual(2); - assertMonkeyPatchValueIsLView(myDir1Instance, false); - assertMonkeyPatchValueIsLView(myDir2Instance, false); - assertMonkeyPatchValueIsLView(childComponentInstance); + const componentLView = (component as any)[MONKEY_PATCH_KEY_NAME]; + expect(Array.isArray(componentLView)).toBeTruthy(); - expect(getLContext(childComponentInstance)).toBe(childNodeContext); - expect(childNodeContext.component).toBeTruthy(); - expect(childNodeContext.directives !.length).toEqual(2); - assertMonkeyPatchValueIsLView(myDir1Instance, false); - assertMonkeyPatchValueIsLView(myDir2Instance, false); - assertMonkeyPatchValueIsLView(childComponentInstance, false); + const hostLView = (hostElm as any)[MONKEY_PATCH_KEY_NAME]; + expect(hostLView).toBe(componentLView); - function assertMonkeyPatchValueIsLView(value: any, yesOrNo = true) { - expect(Array.isArray((value as any)[MONKEY_PATCH_KEY_NAME])).toBe(yesOrNo); - } - }); + const context1 = getLContext(hostElm) !; + expect(context1.lView).toBe(hostLView); + expect(context1.native).toEqual(hostElm); - it('should monkey-patch sub components with the view data and then replace them with the context result once a lookup occurs', - () => { - class ChildComp { - static ngComponentDef = defineComponent({ - type: ChildComp, - selectors: [['child-comp']], - factory: () => new ChildComp(), - consts: 3, - vars: 0, - template: (rf: RenderFlags, ctx: ChildComp) => { - if (rf & RenderFlags.Create) { - element(0, 'div'); - element(1, 'div'); - element(2, 'div'); - } - } - }); - } - - class ParentComp { - static ngComponentDef = defineComponent({ - type: ParentComp, - selectors: [['parent-comp']], - directives: [ChildComp], - factory: () => new ParentComp(), - consts: 2, - vars: 0, - template: (rf: RenderFlags, ctx: ParentComp) => { - if (rf & RenderFlags.Create) { - elementStart(0, 'section'); - elementStart(1, 'child-comp'); - elementEnd(); - elementEnd(); - } - } - }); - } - - const fixture = new ComponentFixture(ParentComp); - fixture.update(); - - const host = fixture.hostElement; - const child = host.querySelector('child-comp') as any; - expect(child[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); - - const context = getLContext(child) !; - expect(child[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); - - const componentData = context.lView[context.nodeIndex]; - const component = componentData[CONTEXT]; - expect(component instanceof ChildComp).toBeTruthy(); - expect(component[MONKEY_PATCH_KEY_NAME]).toBe(context.lView); - - const componentContext = getLContext(component) !; - expect(component[MONKEY_PATCH_KEY_NAME]).toBe(componentContext); - expect(componentContext.nodeIndex).toEqual(context.nodeIndex); - expect(componentContext.native).toEqual(context.native); - expect(componentContext.lView).toEqual(context.lView); - }); + const context2 = getLContext(component) !; + expect(context2).toBe(context1); + expect(context2.lView).toBe(hostLView); + expect(context2.native).toEqual(hostElm); }); - describe('sanitization', () => { - it('should sanitize data using the provided sanitization interface', () => { - class SanitizationComp { - static ngComponentDef = defineComponent({ - type: SanitizationComp, - selectors: [['sanitize-this']], - factory: () => new SanitizationComp(), - consts: 1, - vars: 1, - template: (rf: RenderFlags, ctx: SanitizationComp) => { - if (rf & RenderFlags.Create) { - element(0, 'a'); - } - if (rf & RenderFlags.Update) { - elementProperty(0, 'href', bind(ctx.href), sanitizeUrl); - } + it('should by default monkey-patch the directives with LView so that they can be examined', + () => { + let myDir1Instance: MyDir1|null = null; + let myDir2Instance: MyDir2|null = null; + let myDir3Instance: MyDir2|null = null; + + class MyDir1 { + static ngDirectiveDef = defineDirective({ + type: MyDir1, + selectors: [['', 'my-dir-1', '']], + factory: () => myDir1Instance = new MyDir1() + }); + } + + class MyDir2 { + static ngDirectiveDef = defineDirective({ + type: MyDir2, + selectors: [['', 'my-dir-2', '']], + factory: () => myDir2Instance = new MyDir2() + }); + } + + class MyDir3 { + static ngDirectiveDef = defineDirective({ + type: MyDir3, + selectors: [['', 'my-dir-3', '']], + factory: () => myDir3Instance = new MyDir2() + }); + } + + class StructuredComp { + static ngComponentDef = defineComponent({ + type: StructuredComp, + selectors: [['structured-comp']], + directives: [MyDir1, MyDir2, MyDir3], + factory: () => new StructuredComp(), + consts: 2, + vars: 0, + template: (rf: RenderFlags, ctx: StructuredComp) => { + if (rf & RenderFlags.Create) { + element(0, 'div', ['my-dir-1', '', 'my-dir-2', '']); + element(1, 'div', ['my-dir-3']); + } + } + }); + } + + const fixture = new ComponentFixture(StructuredComp); + fixture.update(); + + const hostElm = fixture.hostElement; + const div1 = hostElm.querySelector('div:first-child') !as any; + const div2 = hostElm.querySelector('div:last-child') !as any; + const context = getLContext(hostElm) !; + const componentView = context.lView[context.nodeIndex]; + + expect(componentView).toContain(myDir1Instance); + expect(componentView).toContain(myDir2Instance); + expect(componentView).toContain(myDir3Instance); + + expect(Array.isArray((myDir1Instance as any)[MONKEY_PATCH_KEY_NAME])).toBeTruthy(); + expect(Array.isArray((myDir2Instance as any)[MONKEY_PATCH_KEY_NAME])).toBeTruthy(); + expect(Array.isArray((myDir3Instance as any)[MONKEY_PATCH_KEY_NAME])).toBeTruthy(); + + const d1Context = getLContext(myDir1Instance) !; + const d2Context = getLContext(myDir2Instance) !; + const d3Context = getLContext(myDir3Instance) !; + + expect(d1Context.lView).toEqual(componentView); + expect(d2Context.lView).toEqual(componentView); + expect(d3Context.lView).toEqual(componentView); + + expect((myDir1Instance as any)[MONKEY_PATCH_KEY_NAME]).toBe(d1Context); + expect((myDir2Instance as any)[MONKEY_PATCH_KEY_NAME]).toBe(d2Context); + expect((myDir3Instance as any)[MONKEY_PATCH_KEY_NAME]).toBe(d3Context); + + expect(d1Context.nodeIndex).toEqual(HEADER_OFFSET); + expect(d1Context.native).toBe(div1); + expect(d1Context.directives as any[]).toEqual([myDir1Instance, myDir2Instance]); + + expect(d2Context.nodeIndex).toEqual(HEADER_OFFSET); + expect(d2Context.native).toBe(div1); + expect(d2Context.directives as any[]).toEqual([myDir1Instance, myDir2Instance]); + + expect(d3Context.nodeIndex).toEqual(HEADER_OFFSET + 1); + expect(d3Context.native).toBe(div2); + expect(d3Context.directives as any[]).toEqual([myDir3Instance]); + }); + + it('should monkey-patch the exact same context instance of the DOM node, component and any directives on the same element', + () => { + let myDir1Instance: MyDir1|null = null; + let myDir2Instance: MyDir2|null = null; + let childComponentInstance: ChildComp|null = null; + + class MyDir1 { + static ngDirectiveDef = defineDirective({ + type: MyDir1, + selectors: [['', 'my-dir-1', '']], + factory: () => myDir1Instance = new MyDir1() + }); + } + + class MyDir2 { + static ngDirectiveDef = defineDirective({ + type: MyDir2, + selectors: [['', 'my-dir-2', '']], + factory: () => myDir2Instance = new MyDir2() + }); + } + + class ChildComp { + static ngComponentDef = defineComponent({ + type: ChildComp, + selectors: [['child-comp']], + factory: () => childComponentInstance = new ChildComp(), + consts: 1, + vars: 0, + template: (rf: RenderFlags, ctx: ChildComp) => { + if (rf & RenderFlags.Create) { + element(0, 'div'); + } + } + }); + } + + class ParentComp { + static ngComponentDef = defineComponent({ + type: ParentComp, + selectors: [['parent-comp']], + directives: [ChildComp, MyDir1, MyDir2], + factory: () => new ParentComp(), + consts: 1, + vars: 0, + template: (rf: RenderFlags, ctx: ParentComp) => { + if (rf & RenderFlags.Create) { + element(0, 'child-comp', ['my-dir-1', '', 'my-dir-2', '']); + } + } + }); + } + + const fixture = new ComponentFixture(ParentComp); + fixture.update(); + + const childCompHostElm = fixture.hostElement.querySelector('child-comp') !as any; + + const lView = childCompHostElm[MONKEY_PATCH_KEY_NAME]; + expect(Array.isArray(lView)).toBeTruthy(); + expect((myDir1Instance as any)[MONKEY_PATCH_KEY_NAME]).toBe(lView); + expect((myDir2Instance as any)[MONKEY_PATCH_KEY_NAME]).toBe(lView); + expect((childComponentInstance as any)[MONKEY_PATCH_KEY_NAME]).toBe(lView); + + const childNodeContext = getLContext(childCompHostElm) !; + expect(childNodeContext.component).toBeFalsy(); + expect(childNodeContext.directives).toBeFalsy(); + assertMonkeyPatchValueIsLView(myDir1Instance); + assertMonkeyPatchValueIsLView(myDir2Instance); + assertMonkeyPatchValueIsLView(childComponentInstance); + + expect(getLContext(myDir1Instance)).toBe(childNodeContext); + expect(childNodeContext.component).toBeFalsy(); + expect(childNodeContext.directives !.length).toEqual(2); + assertMonkeyPatchValueIsLView(myDir1Instance, false); + assertMonkeyPatchValueIsLView(myDir2Instance, false); + assertMonkeyPatchValueIsLView(childComponentInstance); + + expect(getLContext(myDir2Instance)).toBe(childNodeContext); + expect(childNodeContext.component).toBeFalsy(); + expect(childNodeContext.directives !.length).toEqual(2); + assertMonkeyPatchValueIsLView(myDir1Instance, false); + assertMonkeyPatchValueIsLView(myDir2Instance, false); + assertMonkeyPatchValueIsLView(childComponentInstance); + + expect(getLContext(childComponentInstance)).toBe(childNodeContext); + expect(childNodeContext.component).toBeTruthy(); + expect(childNodeContext.directives !.length).toEqual(2); + assertMonkeyPatchValueIsLView(myDir1Instance, false); + assertMonkeyPatchValueIsLView(myDir2Instance, false); + assertMonkeyPatchValueIsLView(childComponentInstance, false); + + function assertMonkeyPatchValueIsLView(value: any, yesOrNo = true) { + expect(Array.isArray((value as any)[MONKEY_PATCH_KEY_NAME])).toBe(yesOrNo); + } + }); + + it('should monkey-patch sub components with the view data and then replace them with the context result once a lookup occurs', + () => { + class ChildComp { + static ngComponentDef = defineComponent({ + type: ChildComp, + selectors: [['child-comp']], + factory: () => new ChildComp(), + consts: 3, + vars: 0, + template: (rf: RenderFlags, ctx: ChildComp) => { + if (rf & RenderFlags.Create) { + element(0, 'div'); + element(1, 'div'); + element(2, 'div'); + } + } + }); + } + + class ParentComp { + static ngComponentDef = defineComponent({ + type: ParentComp, + selectors: [['parent-comp']], + directives: [ChildComp], + factory: () => new ParentComp(), + consts: 2, + vars: 0, + template: (rf: RenderFlags, ctx: ParentComp) => { + if (rf & RenderFlags.Create) { + elementStart(0, 'section'); + elementStart(1, 'child-comp'); + elementEnd(); + elementEnd(); + } + } + }); + } + + const fixture = new ComponentFixture(ParentComp); + fixture.update(); + + const host = fixture.hostElement; + const child = host.querySelector('child-comp') as any; + expect(child[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); + + const context = getLContext(child) !; + expect(child[MONKEY_PATCH_KEY_NAME]).toBeTruthy(); + + const componentData = context.lView[context.nodeIndex]; + const component = componentData[CONTEXT]; + expect(component instanceof ChildComp).toBeTruthy(); + expect(component[MONKEY_PATCH_KEY_NAME]).toBe(context.lView); + + const componentContext = getLContext(component) !; + expect(component[MONKEY_PATCH_KEY_NAME]).toBe(componentContext); + expect(componentContext.nodeIndex).toEqual(context.nodeIndex); + expect(componentContext.native).toEqual(context.native); + expect(componentContext.lView).toEqual(context.lView); + }); +}); + +describe('sanitization', () => { + it('should sanitize data using the provided sanitization interface', () => { + class SanitizationComp { + static ngComponentDef = defineComponent({ + type: SanitizationComp, + selectors: [['sanitize-this']], + factory: () => new SanitizationComp(), + consts: 1, + vars: 1, + template: (rf: RenderFlags, ctx: SanitizationComp) => { + if (rf & RenderFlags.Create) { + element(0, 'a'); } - }); - - private href = ''; - - updateLink(href: any) { this.href = href; } - } - - const sanitizer = new LocalSanitizer((value) => { return 'http://bar'; }); - - const fixture = new ComponentFixture(SanitizationComp, {sanitizer}); - fixture.component.updateLink('http://foo'); - fixture.update(); - - const anchor = fixture.hostElement.querySelector('a') !; - expect(anchor.getAttribute('href')).toEqual('http://bar'); - - fixture.component.updateLink(sanitizer.bypassSecurityTrustUrl('http://foo')); - fixture.update(); - - expect(anchor.getAttribute('href')).toEqual('http://foo'); - }); - - it('should sanitize HostBindings data using provided sanitization interface', () => { - let hostBindingDir: UnsafeUrlHostBindingDir; - class UnsafeUrlHostBindingDir { - // @HostBinding() - cite: any = 'http://cite-dir-value'; - - static ngDirectiveDef = defineDirective({ - type: UnsafeUrlHostBindingDir, - selectors: [['', 'unsafeUrlHostBindingDir', '']], - factory: () => hostBindingDir = new UnsafeUrlHostBindingDir(), - hostBindings: (rf: RenderFlags, ctx: any, elementIndex: number) => { - if (rf & RenderFlags.Create) { - allocHostVars(1); - } - if (rf & RenderFlags.Update) { - elementProperty(elementIndex, 'cite', bind(ctx.cite), sanitizeUrl, true); - } + if (rf & RenderFlags.Update) { + elementProperty(0, 'href', bind(ctx.href), sanitizeUrl); } - }); - } + } + }); - class SimpleComp { - static ngComponentDef = defineComponent({ - type: SimpleComp, - selectors: [['sanitize-this']], - factory: () => new SimpleComp(), - consts: 1, - vars: 0, - template: (rf: RenderFlags, ctx: SimpleComp) => { - if (rf & RenderFlags.Create) { - element(0, 'blockquote', ['unsafeUrlHostBindingDir', '']); - } - }, - directives: [UnsafeUrlHostBindingDir] - }); - } + private href = ''; - const sanitizer = new LocalSanitizer((value) => 'http://bar'); + updateLink(href: any) { this.href = href; } + } - const fixture = new ComponentFixture(SimpleComp, {sanitizer}); - hostBindingDir !.cite = 'http://foo'; - fixture.update(); + const sanitizer = new LocalSanitizer((value) => { return 'http://bar'; }); - const anchor = fixture.hostElement.querySelector('blockquote') !; - expect(anchor.getAttribute('cite')).toEqual('http://bar'); + const fixture = new ComponentFixture(SanitizationComp, {sanitizer}); + fixture.component.updateLink('http://foo'); + fixture.update(); - hostBindingDir !.cite = sanitizer.bypassSecurityTrustUrl('http://foo'); - fixture.update(); + const anchor = fixture.hostElement.querySelector('a') !; + expect(anchor.getAttribute('href')).toEqual('http://bar'); - expect(anchor.getAttribute('cite')).toEqual('http://foo'); - }); + fixture.component.updateLink(sanitizer.bypassSecurityTrustUrl('http://foo')); + fixture.update(); + + expect(anchor.getAttribute('href')).toEqual('http://foo'); + }); + + it('should sanitize HostBindings data using provided sanitization interface', () => { + let hostBindingDir: UnsafeUrlHostBindingDir; + class UnsafeUrlHostBindingDir { + // @HostBinding() + cite: any = 'http://cite-dir-value'; + + static ngDirectiveDef = defineDirective({ + type: UnsafeUrlHostBindingDir, + selectors: [['', 'unsafeUrlHostBindingDir', '']], + factory: () => hostBindingDir = new UnsafeUrlHostBindingDir(), + hostBindings: (rf: RenderFlags, ctx: any, elementIndex: number) => { + if (rf & RenderFlags.Create) { + allocHostVars(1); + } + if (rf & RenderFlags.Update) { + elementProperty(elementIndex, 'cite', bind(ctx.cite), sanitizeUrl, true); + } + } + }); + } + + class SimpleComp { + static ngComponentDef = defineComponent({ + type: SimpleComp, + selectors: [['sanitize-this']], + factory: () => new SimpleComp(), + consts: 1, + vars: 0, + template: (rf: RenderFlags, ctx: SimpleComp) => { + if (rf & RenderFlags.Create) { + element(0, 'blockquote', ['unsafeUrlHostBindingDir', '']); + } + }, + directives: [UnsafeUrlHostBindingDir] + }); + } + + const sanitizer = new LocalSanitizer((value) => 'http://bar'); + + const fixture = new ComponentFixture(SimpleComp, {sanitizer}); + hostBindingDir !.cite = 'http://foo'; + fixture.update(); + + const anchor = fixture.hostElement.querySelector('blockquote') !; + expect(anchor.getAttribute('cite')).toEqual('http://bar'); + + hostBindingDir !.cite = sanitizer.bypassSecurityTrustUrl('http://foo'); + fixture.update(); + + expect(anchor.getAttribute('cite')).toEqual('http://foo'); }); }); diff --git a/packages/core/test/render3/styling/class_and_style_bindings_spec.ts b/packages/core/test/render3/styling/class_and_style_bindings_spec.ts index 29d9a82642..f4c4a9e892 100644 --- a/packages/core/test/render3/styling/class_and_style_bindings_spec.ts +++ b/packages/core/test/render3/styling/class_and_style_bindings_spec.ts @@ -202,12 +202,12 @@ describe('style and class based bindings', () => { assertContext(template, [ masterConfig(9), [null, 2, false, null], - [null], - [null], + [null, null], + [null, null], [0, 0, 0, 0], element, - null, - null, + [0, 0, 9, null, 0], + [0, 0, 9, null, 0], null, ]); }); @@ -217,12 +217,12 @@ describe('style and class based bindings', () => { assertContext(template, [ masterConfig(9), [null, 2, false, null], - [null, 'color', 'red', 'width', '10px'], - [null, 'foo', true, 'bar', true], + [null, null, 'color', 'red', 'width', '10px'], + [null, null, 'foo', true, 'bar', true], [0, 0, 0, 0], element, - null, - null, + [0, 0, 9, null, 0], + [0, 0, 9, null, 0], null, ]); }); @@ -231,11 +231,13 @@ describe('style and class based bindings', () => { () => { const template = initContext(['color', 'red'], null, ['foo']); expect(template[StylingIndex.InitialStyleValuesPosition]).toEqual([ + null, null, 'color', 'red', ]); expect(template[StylingIndex.InitialClassValuesPosition]).toEqual([ + null, null, 'foo', true, @@ -243,21 +245,23 @@ describe('style and class based bindings', () => { patchContext(template, ['color', 'black', 'height', '200px'], ['bar', 'foo'], '1'); expect(template[StylingIndex.InitialStyleValuesPosition]).toEqual([ - null, 'color', 'red', 'height', '200px' + null, null, 'color', 'red', 'height', '200px' ]); expect(template[StylingIndex.InitialClassValuesPosition]).toEqual([ - null, 'foo', true, 'bar', true + null, null, 'foo', true, 'bar', true ]); }); it('should only populate static styles for a given directive once', () => { const template = initContext(['color', 'red'], null, ['foo']); expect(template[StylingIndex.InitialStyleValuesPosition]).toEqual([ + null, null, 'color', 'red', ]); expect(template[StylingIndex.InitialClassValuesPosition]).toEqual([ + null, null, 'foo', true, @@ -265,11 +269,13 @@ describe('style and class based bindings', () => { patchContext(template, ['color', 'black', 'height', '200px'], ['bar', 'foo']); expect(template[StylingIndex.InitialStyleValuesPosition]).toEqual([ + null, null, 'color', 'red', ]); expect(template[StylingIndex.InitialClassValuesPosition]).toEqual([ + null, null, 'foo', true, @@ -278,17 +284,6 @@ describe('style and class based bindings', () => { patchContext(template, ['color', 'black', 'height', '200px'], ['bar', 'foo'], '1'); expect(template[StylingIndex.InitialStyleValuesPosition]).toEqual([ null, - 'color', - 'red', - 'height', - '200px', - ]); - expect(template[StylingIndex.InitialClassValuesPosition]).toEqual([ - null, 'foo', true, 'bar', true - ]); - - patchContext(template, ['color', 'black', 'height', '200px'], ['bar', 'foo'], '1'); - expect(template[StylingIndex.InitialStyleValuesPosition]).toEqual([ null, 'color', 'red', @@ -296,6 +291,20 @@ describe('style and class based bindings', () => { '200px', ]); expect(template[StylingIndex.InitialClassValuesPosition]).toEqual([ + null, null, 'foo', true, 'bar', true + ]); + + patchContext(template, ['color', 'black', 'height', '200px'], ['bar', 'foo'], '1'); + expect(template[StylingIndex.InitialStyleValuesPosition]).toEqual([ + null, + null, + 'color', + 'red', + 'height', + '200px', + ]); + expect(template[StylingIndex.InitialClassValuesPosition]).toEqual([ + null, null, 'foo', true, @@ -381,10 +390,11 @@ describe('style and class based bindings', () => { ''); }); - it('should support binding to camelCased style properties', () => { - //
+ it('should support binding to camelCased and hyphenated style properties', () => { + //
class Comp { borderWidth: string = '3px'; + borderColor: string = 'red'; static ngComponentDef = defineComponent({ type: Comp, @@ -395,11 +405,12 @@ describe('style and class based bindings', () => { template: (rf: RenderFlags, ctx: Comp) => { if (rf & RenderFlags.Create) { elementStart(0, 'div'); - elementStyling(null, ['borderWidth']); + elementStyling(null, ['borderWidth', 'border-color']); elementEnd(); } if (rf & RenderFlags.Update) { elementStyleProp(0, 0, ctx.borderWidth); + elementStyleProp(0, 1, ctx.borderColor); elementStylingApply(0); } } @@ -412,7 +423,9 @@ describe('style and class based bindings', () => { const target = fixture.hostElement.querySelector('div') !as any; expect(target.style.borderWidth).toEqual('3px'); - expect(fixture.html).toEqual('
'); + expect(target.style.borderColor).toEqual('red'); + expect(fixture.html).toContain('border-width: 3px'); + expect(fixture.html).toContain('border-color: red'); }); }); @@ -426,34 +439,34 @@ describe('style and class based bindings', () => { assertContext(ctx, [ masterConfig(17, false, false), // [null, 2, false, null], - [null, 'width', null], - [null, 'foo', false], + [null, null, 'width', null], + [null, null, 'foo', false], [1, 1, 1, 1, 9, 13], null, - null, - null, + [1, 0, 21, null, 1], + [1, 0, 17, null, 1], null, // #9 - cleanStyle(2, 17), + cleanStyle(3, 17), 'width', null, 0, // #13 - cleanClass(2, 21), + cleanClass(3, 21), 'foo', null, 0, // #17 - cleanStyle(2, 9), + cleanStyle(3, 9), 'width', null, 0, // #21 - cleanClass(2, 13), + cleanClass(3, 13), 'foo', null, 0, @@ -464,58 +477,58 @@ describe('style and class based bindings', () => { assertContext(ctx, [ masterConfig(25, false, false), // [null, 2, false, null, 'SOME DIRECTIVE', 6, false, null], - [null, 'width', null, 'height', null], - [null, 'foo', false, 'bar', false], + [null, null, 'width', null, 'height', null], + [null, null, 'foo', false, 'bar', false], [2, 2, 1, 1, 9, 17, 2, 1, 9, 13, 21], null, - null, - null, + [2, 0, 33, null, 1, 0, 37, null, 1], + [2, 0, 25, null, 1, 0, 29, null, 1], null, // #9 - cleanStyle(2, 25), + cleanStyle(3, 25), 'width', null, 0, // #13 - cleanStyle(4, 29), + cleanStyle(5, 29), 'height', null, 1, // #17 - cleanClass(2, 33), + cleanClass(3, 33), 'foo', null, 0, // #21 - cleanClass(4, 37), + cleanClass(5, 37), 'bar', null, 1, // #25 - cleanStyle(2, 9), + cleanStyle(3, 9), 'width', null, 0, // #29 - cleanStyle(4, 13), + cleanStyle(5, 13), 'height', null, 1, // #33 - cleanClass(2, 17), + cleanClass(3, 17), 'foo', null, 0, // #37 - cleanClass(4, 21), + cleanClass(5, 21), 'bar', null, 1, @@ -530,82 +543,82 @@ describe('style and class based bindings', () => { null, 2, false, null, 'SOME DIRECTIVE', 6, false, null, 'SOME DIRECTIVE 2', 11, false, null ], - [null, 'width', null, 'height', null, 'opacity', null], - [null, 'foo', false, 'bar', false, 'baz', false], + [null, null, 'width', null, 'height', null, 'opacity', null], + [null, null, 'foo', false, 'bar', false, 'baz', false], [3, 3, 1, 1, 9, 21, 2, 1, 9, 13, 25, 3, 3, 17, 9, 13, 29, 25, 21], null, - null, - null, + [3, 0, 45, null, 1, 0, 49, null, 1, 0, 53, null, 1], + [3, 0, 33, null, 1, 0, 37, null, 1, 0, 41, null, 1], null, // #9 - cleanStyle(2, 33), + cleanStyle(3, 33), 'width', null, 0, // #13 - cleanStyle(4, 37), + cleanStyle(5, 37), 'height', null, 1, // #17 - cleanStyle(6, 41), + cleanStyle(7, 41), 'opacity', null, 2, // #21 - cleanClass(2, 45), + cleanClass(3, 45), 'foo', null, 0, // #25 - cleanClass(4, 49), + cleanClass(5, 49), 'bar', null, 1, // #29 - cleanClass(6, 53), + cleanClass(7, 53), 'baz', null, 2, // #33 - cleanStyle(2, 9), + cleanStyle(3, 9), 'width', null, 0, // #37 - cleanStyle(4, 13), + cleanStyle(5, 13), 'height', null, 1, // #41 - cleanStyle(6, 17), + cleanStyle(7, 17), 'opacity', null, 2, // #45 - cleanClass(2, 21), + cleanClass(3, 21), 'foo', null, 0, // #49 - cleanClass(4, 25), + cleanClass(5, 25), 'bar', null, 1, // #53 - cleanClass(6, 29), + cleanClass(7, 29), 'baz', null, 2, @@ -757,25 +770,25 @@ describe('style and class based bindings', () => { assertContextOnlyValues(stylingContext, [ // #9 - cleanStyle(2, 17), + cleanStyle(3, 17), 'width', null, 0, // #13 - cleanStyle(4, 21), + cleanStyle(5, 21), 'height', null, 0, // #17 - dirtyStyle(2, 9), + dirtyStyle(3, 9), 'width', '100px', 0, // #21 - dirtyStyle(4, 13), + dirtyStyle(5, 13), 'height', '100px', 0, @@ -786,19 +799,19 @@ describe('style and class based bindings', () => { assertContextOnlyValues(stylingContext, [ // #9 - cleanStyle(2, 17), + cleanStyle(3, 17), 'width', null, 0, // #13 - cleanStyle(4, 25), + cleanStyle(5, 25), 'height', null, 0, // #17 - dirtyStyle(2, 9), + dirtyStyle(3, 9), 'width', '200px', 0, @@ -810,7 +823,7 @@ describe('style and class based bindings', () => { 0, // #25 - dirtyStyle(4, 13), + dirtyStyle(5, 13), 'height', null, 0, @@ -819,19 +832,19 @@ describe('style and class based bindings', () => { getStyles(stylingContext); assertContextOnlyValues(stylingContext, [ // #9 - cleanStyle(2, 17), + cleanStyle(3, 17), 'width', null, 0, // #13 - cleanStyle(4, 25), + cleanStyle(5, 25), 'height', null, 0, // #17 - cleanStyle(2, 9), + cleanStyle(3, 9), 'width', '200px', 0, @@ -843,7 +856,7 @@ describe('style and class based bindings', () => { 0, // #23 - cleanStyle(4, 13), + cleanStyle(5, 13), 'height', null, 0, @@ -854,19 +867,19 @@ describe('style and class based bindings', () => { assertContextOnlyValues(stylingContext, [ // #9 - dirtyStyle(2, 17), + dirtyStyle(3, 17), 'width', '300px', 0, // #13 - cleanStyle(4, 25), + cleanStyle(5, 25), 'height', null, 0, // #17 - cleanStyle(2, 9), + cleanStyle(3, 9), 'width', null, 0, @@ -878,7 +891,7 @@ describe('style and class based bindings', () => { 0, // #23 - cleanStyle(4, 13), + cleanStyle(5, 13), 'height', null, 0, @@ -889,19 +902,19 @@ describe('style and class based bindings', () => { updateStyleProp(stylingContext, 0, null); assertContextOnlyValues(stylingContext, [ // #9 - dirtyStyle(2, 17), + dirtyStyle(3, 17), 'width', null, 0, // #13 - cleanStyle(4, 25), + cleanStyle(5, 25), 'height', null, 0, // #17 - cleanStyle(2, 9), + cleanStyle(3, 9), 'width', null, 0, @@ -913,7 +926,7 @@ describe('style and class based bindings', () => { 0, // #23 - cleanStyle(4, 13), + cleanStyle(5, 13), 'height', null, 0, @@ -922,15 +935,15 @@ describe('style and class based bindings', () => { it('should find the next available space in the context when data is added after being removed before', () => { - const stylingContext = initContext(null, ['lineHeight']); + const stylingContext = initContext(null, ['line-height']); const getStyles = trackStylesFactory(); updateStyles(stylingContext, {width: '100px', height: '100px', opacity: '0.5'}); assertContextOnlyValues(stylingContext, [ // #9 - cleanStyle(2, 25), - 'lineHeight', + cleanStyle(3, 25), + 'line-height', null, 0, @@ -953,8 +966,8 @@ describe('style and class based bindings', () => { 0, // #23 - cleanStyle(2, 9), - 'lineHeight', + cleanStyle(3, 9), + 'line-height', null, 0, ]); @@ -964,8 +977,8 @@ describe('style and class based bindings', () => { updateStyles(stylingContext, {}); assertContextOnlyValues(stylingContext, [ // #9 - cleanStyle(2, 25), - 'lineHeight', + cleanStyle(3, 25), + 'line-height', null, 0, @@ -988,27 +1001,25 @@ describe('style and class based bindings', () => { 0, // #23 - cleanStyle(2, 9), - 'lineHeight', + cleanStyle(3, 9), + 'line-height', null, 0, ]); getStyles(stylingContext); - updateStyles(stylingContext, { - borderWidth: '5px', - }); + updateStyles(stylingContext, {borderWidth: '5px'}); assertContextOnlyValues(stylingContext, [ // #9 - cleanStyle(2, 29), - 'lineHeight', + cleanStyle(3, 29), + 'line-height', null, 0, // #13 dirtyStyle(), - 'borderWidth', + 'border-width', '5px', 0, @@ -1031,8 +1042,8 @@ describe('style and class based bindings', () => { 0, // #29 - cleanStyle(2, 9), - 'lineHeight', + cleanStyle(3, 9), + 'line-height', null, 0, ]); @@ -1041,14 +1052,14 @@ describe('style and class based bindings', () => { assertContextOnlyValues(stylingContext, [ // #9 - dirtyStyle(2, 29), - 'lineHeight', + dirtyStyle(3, 29), + 'line-height', '200px', 0, // #13 dirtyStyle(), - 'borderWidth', + 'border-width', '5px', 0, @@ -1071,8 +1082,8 @@ describe('style and class based bindings', () => { 0, // #29 - cleanStyle(2, 9), - 'lineHeight', + cleanStyle(3, 9), + 'line-height', null, 0, ]); @@ -1081,20 +1092,20 @@ describe('style and class based bindings', () => { assertContextOnlyValues(stylingContext, [ // #9 - dirtyStyle(2, 33), - 'lineHeight', + dirtyStyle(3, 33), + 'line-height', '200px', 0, // #13 dirtyStyle(), - 'borderWidth', + 'border-width', '15px', 0, // #17 dirtyStyle(), - 'borderColor', + 'border-color', 'red', 0, @@ -1117,8 +1128,8 @@ describe('style and class based bindings', () => { 0, // #33 - cleanStyle(2, 9), - 'lineHeight', + cleanStyle(3, 9), + 'line-height', null, 0, ]); @@ -1128,22 +1139,24 @@ describe('style and class based bindings', () => { const getStyles = trackStylesFactory(); const stylingContext = initContext(null, ['height']); - updateStyles(stylingContext, {width: '100px'}); + const cachedStyleValue = {width: '100px'}; + + updateStyles(stylingContext, cachedStyleValue); updateStyleProp(stylingContext, 0, '200px'); assertContext(stylingContext, [ masterConfig(13, true), // [null, 2, true, null], - [null, 'height', null], - [null], + [null, null, 'height', null], + [null, null], [1, 0, 1, 0, 9], element, - null, - {width: '100px'}, + [0, 0, 21, null, 0], + [1, 0, 13, cachedStyleValue, 1], null, // #9 - dirtyStyle(2, 17), + dirtyStyle(3, 17), 'height', '200px', 0, @@ -1155,7 +1168,7 @@ describe('style and class based bindings', () => { 0, // #17 - cleanStyle(2, 9), + cleanStyle(3, 9), 'height', null, 0, @@ -1166,16 +1179,16 @@ describe('style and class based bindings', () => { assertContext(stylingContext, [ masterConfig(13, false), // [null, 2, false, null], - [null, 'height', null], - [null], + [null, null, 'height', null], + [null, null], [1, 0, 1, 0, 9], element, - null, - {width: '100px'}, + [0, 0, 21, null, 0], + [1, 0, 13, cachedStyleValue, 1], null, // #9 - cleanStyle(2, 17), + cleanStyle(3, 17), 'height', '200px', 0, @@ -1187,7 +1200,7 @@ describe('style and class based bindings', () => { 0, // #17 - cleanStyle(2, 9), + cleanStyle(3, 9), 'height', null, 0, @@ -1206,25 +1219,25 @@ describe('style and class based bindings', () => { assertContextOnlyValues(stylingContext, [ // #9 - dirtyStyleWithSanitization(2, 17), + dirtyStyleWithSanitization(3, 17), 'border-image', 'url(foo.jpg)', 0, // #13 - dirtyStyle(4, 21), + dirtyStyle(5, 21), 'border-width', '100px', 0, // #17 - cleanStyleWithSanitization(2, 9), + cleanStyleWithSanitization(3, 9), 'border-image', null, 0, // #21 - cleanStyle(4, 13), + cleanStyle(5, 13), 'border-width', null, 0, @@ -1234,13 +1247,13 @@ describe('style and class based bindings', () => { assertContextOnlyValues(stylingContext, [ // #9 - dirtyStyleWithSanitization(2, 21), + dirtyStyleWithSanitization(3, 21), 'border-image', 'url(foo.jpg)', 0, // #13 - dirtyStyle(4, 25), + dirtyStyle(5, 25), 'border-width', '100px', 0, @@ -1252,13 +1265,13 @@ describe('style and class based bindings', () => { 0, // #21 - cleanStyleWithSanitization(2, 9), + cleanStyleWithSanitization(3, 9), 'border-image', null, 0, // #23 - cleanStyle(4, 13), + cleanStyle(5, 13), 'border-width', null, 0, @@ -1268,13 +1281,13 @@ describe('style and class based bindings', () => { assertContextOnlyValues(stylingContext, [ // #9 - cleanStyleWithSanitization(2, 21), + cleanStyleWithSanitization(3, 21), 'border-image', 'url(foo.jpg)', 0, // #13 - cleanStyle(4, 25), + cleanStyle(5, 25), 'border-width', '100px', 0, @@ -1286,20 +1299,20 @@ describe('style and class based bindings', () => { 0, // #21 - cleanStyleWithSanitization(2, 9), + cleanStyleWithSanitization(3, 9), 'border-image', null, 0, // #23 - cleanStyle(4, 13), + cleanStyle(5, 13), 'border-width', null, 0, ]); }); - it('should only update styling values for successive directives if null in a former directive', + it('should only update single styling values for successive directives if null in a former directive', () => { const template = createEmptyStylingContext(); @@ -1339,6 +1352,347 @@ describe('style and class based bindings', () => { expect(getDirectiveIndexFromEntry(ctx, colorIndex)).toEqual(3); }); + it('should allow single style values to override a previous entry if a flag is passed in', + () => { + const template = createEmptyStylingContext(); + + const dir1 = {}; + const dir2 = {}; + const dir3 = {}; + + updateContextWithBindings(template, dir1, null, ['width', 'height']); + updateContextWithBindings(template, dir2, null, ['width', 'color']); + updateContextWithBindings(template, dir3, null, ['height', 'opacity']); + + const ctx = allocStylingContext(element, template); + + // styles 0 = width, 1 = height, 2 = color within the context + const widthIndex = StylingIndex.SingleStylesStartPosition + StylingIndex.Size * 0; + const heightIndex = StylingIndex.SingleStylesStartPosition + StylingIndex.Size * 1; + + updateStyleProp(ctx, 0, '100px', dir1); + updateStyleProp(ctx, 1, '100px', dir1); + expect(ctx[widthIndex + StylingIndex.ValueOffset]).toEqual('100px'); + expect(ctx[heightIndex + StylingIndex.ValueOffset]).toEqual('100px'); + expect(getDirectiveIndexFromEntry(ctx, widthIndex)).toEqual(1); + expect(getDirectiveIndexFromEntry(ctx, heightIndex)).toEqual(1); + + updateStyleProp(ctx, 0, '300px', dir1); + updateStyleProp(ctx, 1, '300px', dir1); + + updateStyleProp(ctx, 0, '900px', dir2); + updateStyleProp(ctx, 0, '900px', dir3, true); + + expect(ctx[widthIndex + StylingIndex.ValueOffset]).toEqual('300px'); + expect(ctx[heightIndex + StylingIndex.ValueOffset]).toEqual('900px'); + expect(getDirectiveIndexFromEntry(ctx, widthIndex)).toEqual(1); + expect(getDirectiveIndexFromEntry(ctx, heightIndex)).toEqual(3); + + updateStyleProp(ctx, 0, '400px', dir1); + updateStyleProp(ctx, 1, '400px', dir1); + + expect(ctx[widthIndex + StylingIndex.ValueOffset]).toEqual('400px'); + expect(ctx[heightIndex + StylingIndex.ValueOffset]).toEqual('400px'); + expect(getDirectiveIndexFromEntry(ctx, widthIndex)).toEqual(1); + expect(getDirectiveIndexFromEntry(ctx, heightIndex)).toEqual(1); + }); + + it('should only update missing multi styling values for successive directives if null in a former directive', + () => { + const template = createEmptyStylingContext(); + updateContextWithBindings(template, null); + + const dir1 = {}; + const dir2 = {}; + const dir3 = {}; + updateContextWithBindings(template, dir1, null, ['width', 'height']); + updateContextWithBindings(template, dir2); + updateContextWithBindings(template, dir3); + + const ctx = allocStylingContext(element, template); + let s1, s2, s3; + updateStylingMap(ctx, null, s1 = {width: '100px', height: '99px'}, dir1); + updateStylingMap(ctx, null, s2 = {width: '200px', opacity: '0.5'}, dir2); + updateStylingMap(ctx, null, s3 = {width: '300px', height: '999px'}, dir3); + + expect(ctx[StylingIndex.CachedMultiStyles]).toEqual([ + 3, 0, 17, null, 0, 0, 17, s1, 2, 0, 25, s2, 1, 0, 29, s3, 0 + ]); + + assertContextOnlyValues(ctx, [ + // #9 + cleanStyle(3, 17), + 'width', + null, + 1, + + // #13 + cleanStyle(5, 21), + 'height', + null, + 1, + + // #17 + dirtyStyle(3, 9), + 'width', + '100px', + 1, + + // #21 + dirtyStyle(5, 13), + 'height', + '99px', + 1, + + // #25 + dirtyStyle(0, 0), + 'opacity', + '0.5', + 2, + ]); + + updateStylingMap(ctx, null, {opacity: '0', width: null}, dir1); + updateStylingMap(ctx, null, {width: '200px', opacity: '0.5'}, dir2); + updateStylingMap(ctx, null, {width: '300px', height: '999px'}, dir3); + + assertContextOnlyValues(ctx, [ + // #9 + cleanStyle(3, 21), + 'width', + null, + 1, + + // #13 + cleanStyle(5, 25), + 'height', + null, + 1, + + // #17 + dirtyStyle(0, 0), + 'opacity', + '0', + 1, + + // #21 + dirtyStyle(3, 9), + 'width', + '200px', + 2, + + // #25 + dirtyStyle(5, 13), + 'height', + '999px', + 3, + ]); + + updateStylingMap(ctx, null, null, dir1); + updateStylingMap(ctx, null, {width: '500px', opacity: '0.2'}, dir2); + updateStylingMap(ctx, null, {width: '300px', height: '999px', color: 'red'}, dir3); + + assertContextOnlyValues(ctx, [ + // #9 + cleanStyle(3, 17), + 'width', + null, + 1, + + // #13 + cleanStyle(5, 25), + 'height', + null, + 1, + + // #17 + dirtyStyle(3, 9), + 'width', + '500px', + 2, + + // #21 + dirtyStyle(0, 0), + 'opacity', + '0.2', + 2, + + // #25 + dirtyStyle(5, 13), + 'height', + '999px', + 3, + + // #29 + dirtyStyle(0, 0), + 'color', + 'red', + 3, + ]); + }); + + it('should only update missing multi class values for successive directives if null in a former directive', + () => { + const template = createEmptyStylingContext(); + updateContextWithBindings(template, null); + + const dir1 = {}; + const dir2 = {}; + const dir3 = {}; + updateContextWithBindings(template, dir1, ['red', 'green']); + updateContextWithBindings(template, dir2); + updateContextWithBindings(template, dir3); + + const ctx = allocStylingContext(element, template); + let c1, c2, c3; + updateStylingMap(ctx, c1 = {red: true, orange: true}, null, dir1); + updateStylingMap(ctx, c2 = 'black red', null, dir2); + updateStylingMap(ctx, c3 = 'silver green', null, dir3); + + expect(ctx[StylingIndex.CachedMultiClasses]).toEqual([ + 5, 0, 17, null, 0, 0, 17, c1, 2, 0, 25, c2, 1, 0, 29, c3, 2 + ]); + + assertContextOnlyValues(ctx, [ + // #9 + cleanClass(3, 17), + 'red', + null, + 1, + + // #13 + cleanClass(5, 33), + 'green', + null, + 1, + + // #17 + dirtyClass(3, 9), + 'red', + true, + 1, + + // #21 + dirtyClass(0, 0), + 'orange', + true, + 1, + + // #25 + dirtyClass(0, 0), + 'black', + true, + 2, + + // #29 + dirtyClass(0, 0), + 'silver', + true, + 3, + + // #33 + dirtyClass(5, 13), + 'green', + true, + 3, + ]); + + updateStylingMap(ctx, c1 = {orange: true}, null, dir1); + updateStylingMap(ctx, c2 = 'black red', null, dir2); + updateStylingMap(ctx, c3 = 'green', null, dir3); + + assertContextOnlyValues(ctx, [ + // #9 + cleanClass(3, 25), + 'red', + null, + 1, + + // #13 + cleanClass(5, 29), + 'green', + null, + 1, + + // #17 + dirtyClass(0, 0), + 'orange', + true, + 1, + + // #21 + dirtyClass(0, 0), + 'black', + true, + 2, + + // #25 + dirtyClass(3, 9), + 'red', + true, + 2, + + // #29 + dirtyClass(5, 13), + 'green', + true, + 3, + + // #33 + dirtyClass(0, 0), + 'silver', + null, + 1, + ]); + + updateStylingMap(ctx, c1 = 'green', null, dir1); + updateStylingMap(ctx, c2 = null, null, dir2); + updateStylingMap(ctx, c3 = 'red', null, dir3); + + assertContextOnlyValues(ctx, [ + // #9 + cleanClass(3, 21), + 'red', + null, + 1, + + // #13 + cleanClass(5, 17), + 'green', + null, + 1, + + // #17 + dirtyClass(5, 13), + 'green', + true, + 1, + + // #21 + dirtyClass(3, 9), + 'red', + true, + 3, + + // #25 + dirtyClass(0, 0), + 'black', + null, + 1, + + // #29 + dirtyClass(0, 0), + 'orange', + null, + 1, + + // #33 + dirtyClass(0, 0), + 'silver', + null, + 1, + ]); + }); + it('should throw an error if a directive is provided that isn\'t registered', () => { const template = createEmptyStylingContext(); const knownDir = {}; @@ -1420,7 +1774,7 @@ describe('style and class based bindings', () => { const getStyles = trackStylesFactory(store); const otherDirective = {}; - let styles: any = {fontSize: ''}; + let styles: any = {'font-size': ''}; updateStyleProp(stylingContext, 0, ''); updateStylingMap(stylingContext, null, styles); patchContextWithStaticAttrs(stylingContext, [], 0, otherDirective); @@ -1428,19 +1782,19 @@ describe('style and class based bindings', () => { getStyles(stylingContext, otherDirective); expect(store.getValues()).toEqual({}); - styles = {fontSize: '20px'}; + styles = {'font-size': '20px'}; updateStyleProp(stylingContext, 0, 'red'); updateStylingMap(stylingContext, null, styles); getStyles(stylingContext); - expect(store.getValues()).toEqual({fontSize: '20px', color: 'red'}); + expect(store.getValues()).toEqual({'font-size': '20px', color: 'red'}); styles = {}; updateStyleProp(stylingContext, 0, ''); updateStylingMap(stylingContext, null, styles); getStyles(stylingContext); - expect(store.getValues()).toEqual({fontSize: null, color: ''}); + expect(store.getValues()).toEqual({'font-size': null, color: ''}); }); }); @@ -1450,34 +1804,34 @@ describe('style and class based bindings', () => { assertContext(template, [ masterConfig(17, false), // [null, 2, false, null], - [null], - [null, 'one', false, 'two', false], + [null, null], + [null, null, 'one', false, 'two', false], [0, 2, 0, 2, 9, 13], element, - null, - null, + [2, 0, 17, null, 2], + [0, 0, 17, null, 0], null, // #9 - cleanClass(2, 17), + cleanClass(3, 17), 'one', null, 0, // #13 - cleanClass(4, 21), + cleanClass(5, 21), 'two', null, 0, // #17 - cleanClass(2, 9), + cleanClass(3, 9), 'one', null, 0, // #21 - cleanClass(4, 13), + cleanClass(5, 13), 'two', null, 0, @@ -1531,58 +1885,58 @@ describe('style and class based bindings', () => { assertContext(stylingContext, [ masterConfig(25, false), // [null, 2, false, null], - [null, 'width', '100px', 'height', null], - [null, 'wide', true, 'tall', false], + [null, null, 'width', '100px', 'height', null], + [null, null, 'wide', true, 'tall', false], [2, 2, 2, 2, 9, 13, 17, 21], element, - null, - null, + [2, 0, 33, null, 2], + [2, 0, 25, null, 2], null, // #9 - cleanStyle(2, 25), + cleanStyle(3, 25), 'width', null, 0, // #13 - cleanStyle(4, 29), + cleanStyle(5, 29), 'height', null, 0, // #17 - cleanClass(2, 33), + cleanClass(3, 33), 'wide', null, 0, // #21 - cleanClass(4, 37), + cleanClass(5, 37), 'tall', null, 0, // #25 - cleanStyle(2, 9), + cleanStyle(3, 9), 'width', null, 0, // #29 - cleanStyle(4, 13), + cleanStyle(5, 13), 'height', null, 0, // #33 - cleanClass(2, 17), + cleanClass(3, 17), 'wide', null, 0, // #37 - cleanClass(4, 21), + cleanClass(5, 21), 'tall', null, 0, @@ -1590,44 +1944,45 @@ describe('style and class based bindings', () => { expect(getStylesAndClasses(stylingContext)).toEqual([{}, {}]); - updateStylingMap(stylingContext, 'tall round', {width: '200px', opacity: '0.5'}); + let cachedStyleMap: any = {width: '200px', opacity: '0.5'}; + updateStylingMap(stylingContext, 'tall round', cachedStyleMap); assertContext(stylingContext, [ masterConfig(25, true), // [null, 2, true, null], - [null, 'width', '100px', 'height', null], - [null, 'wide', true, 'tall', false], + [null, null, 'width', '100px', 'height', null], + [null, null, 'wide', true, 'tall', false], [2, 2, 2, 2, 9, 13, 17, 21], element, - 'tall round', - {width: '200px', opacity: '0.5'}, + [2, 0, 37, 'tall round', 2], + [2, 0, 25, cachedStyleMap, 2], null, // #9 - cleanStyle(2, 25), + cleanStyle(3, 25), 'width', null, 0, // #13 - cleanStyle(4, 45), + cleanStyle(5, 33), 'height', null, 0, // #17 - cleanClass(2, 41), + cleanClass(3, 45), 'wide', null, 0, // #21 - cleanClass(4, 33), + cleanClass(5, 37), 'tall', null, 0, - // #23 - dirtyStyle(2, 9), + // #25 + dirtyStyle(3, 9), 'width', '200px', 0, @@ -1639,26 +1994,26 @@ describe('style and class based bindings', () => { 0, // #33 - dirtyClass(4, 21), + cleanStyle(5, 13), + 'height', + null, + 0, + + // #37 + dirtyClass(5, 21), 'tall', true, 0, - // #37 + // #41 dirtyClass(0, 0), 'round', true, 0, - // #41 - cleanClass(2, 17), - 'wide', - null, - 0, - // #45 - cleanStyle(4, 13), - 'height', + cleanClass(3, 17), + 'wide', null, 0, ]); @@ -1668,79 +2023,81 @@ describe('style and class based bindings', () => { {width: '200px', opacity: '0.5'}, ]); - updateStylingMap(stylingContext, {tall: true, wide: true}, {width: '500px'}); + let cachedClassMap = {tall: true, wide: true}; + cachedStyleMap = {width: '500px'}; + updateStylingMap(stylingContext, cachedClassMap, cachedStyleMap); updateStyleProp(stylingContext, 0, '300px'); assertContext(stylingContext, [ masterConfig(25, true), // [null, 2, true, null], - [null, 'width', '100px', 'height', null], - [null, 'wide', true, 'tall', false], + [null, null, 'width', '100px', 'height', null], + [null, null, 'wide', true, 'tall', false], [2, 2, 2, 2, 9, 13, 17, 21], element, - {tall: true, wide: true}, - {width: '500px'}, + [2, 0, 37, cachedClassMap, 2], + [1, 0, 25, cachedStyleMap, 1], null, // #9 - dirtyStyle(2, 25), + dirtyStyle(3, 25), 'width', '300px', 0, // #13 - cleanStyle(4, 45), + cleanStyle(5, 33), 'height', null, 0, // #17 - cleanClass(2, 33), + cleanClass(3, 41), 'wide', null, 0, // #21 - cleanClass(4, 29), + cleanClass(5, 37), 'tall', null, 0, // #25 - cleanStyle(2, 9), + cleanStyle(3, 9), 'width', '500px', 0, // #29 - cleanClass(4, 21), - 'tall', - true, - 0, - - // #33 - cleanClass(2, 17), - 'wide', - true, - 0, - - // #37 - dirtyClass(0, 0), - 'round', - null, - 0, - - // #41 dirtyStyle(0, 0), 'opacity', null, 0, - // #45 - cleanStyle(4, 13), + // #33 + cleanStyle(5, 13), 'height', null, 0, + + // #37 + cleanClass(5, 21), + 'tall', + true, + 0, + + // #41 + cleanClass(3, 17), + 'wide', + true, + 0, + + // #45 + dirtyClass(0, 0), + 'round', + null, + 0, ]); expect(getStylesAndClasses(stylingContext)).toEqual([ @@ -1768,15 +2125,15 @@ describe('style and class based bindings', () => { getStylesAndClasses(stylingContext); assertContext(stylingContext, [ - masterConfig(9, false), // - [null, 2, false, null], // - [null], // - [null], // - [0, 0, 0, 0], // - element, // - {foo: true}, // - {width: '200px'}, // - null, // + masterConfig(9, false), // + [null, 2, false, null], // + [null, null], // + [null, null], // + [0, 0, 0, 0], // + element, // + [1, 0, 13, classesMap, 1], // + [1, 0, 9, stylesMap, 1], // + null, // // #9 cleanStyle(0, 0), 'width', '200px', 0, @@ -1794,15 +2151,15 @@ describe('style and class based bindings', () => { getStylesAndClasses(stylingContext); assertContext(stylingContext, [ - masterConfig(9, false), // - [null, 2, false, null], // - [null], // - [null], // - [0, 0, 0, 0], // - element, // - {foo: false}, // - {width: '300px'}, // - null, // + masterConfig(9, false), // + [null, 2, false, null], // + [null, null], // + [null, null], // + [0, 0, 0, 0], // + element, // + [1, 0, 13, classesMap, 1], // + [1, 0, 9, stylesMap, 1], // + null, // // #9 cleanStyle(0, 0), 'width', '200px', 0, @@ -1825,12 +2182,12 @@ describe('style and class based bindings', () => { assertContext(stylingContext, [ masterConfig(9, false), // [null, 2, false, null], - [null], - [null], + [null, null], + [null, null], [0, 0, 0, 0], element, - 'apple orange banana', - null, + [3, 0, 9, 'apple orange banana', 3], + [0, 0, 9, null, 0], null, // #9 @@ -1852,12 +2209,8 @@ describe('style and class based bindings', () => { 0, ]); - stylingContext - [StylingIndex.SingleStylesStartPosition + 1 * StylingIndex.Size + - StylingIndex.ValueOffset] = false; // no orange - stylingContext - [StylingIndex.SingleStylesStartPosition + 2 * StylingIndex.Size + - StylingIndex.ValueOffset] = false; // no banana + stylingContext[13 + StylingIndex.ValueOffset] = false; + stylingContext[17 + StylingIndex.ValueOffset] = false; updateStylingMap(stylingContext, classes); // apply the styles @@ -2193,43 +2546,45 @@ describe('style and class based bindings', () => { }; assertContext(context, [ - masterConfig(17, false), // - [null, 2, false, null], // - [null, 'color', null], // - [null, 'foo', false], // - [1, 1, 1, 1, 9, 13], // - element, // - null, // - null, // - null, // + masterConfig(17, false), // + [null, 2, false, null], // + [null, null, 'color', null], // + [null, null, 'foo', false], // + [1, 1, 1, 1, 9, 13], // + element, // + [1, 0, 21, null, 1], // + [1, 0, 17, null, 1], // + null, // // #9 - cleanStyle(2, 17), + cleanStyle(3, 17), 'color', null, 0, // #13 - cleanClass(2, 21), + cleanClass(3, 21), 'foo', null, 0, // #17 - cleanStyle(2, 9), + cleanStyle(3, 9), 'color', null, 0, // #21 - cleanClass(2, 13), + cleanClass(3, 13), 'foo', null, 0, ]); - const styleMapWithPlayerFactory = bindPlayerFactory(buildStyleFn, {opacity: '1'}); - const classMapWithPlayerFactory = bindPlayerFactory(buildClassFn, {map: true}); + const cachedClassMap = {map: true}; + const cachedStyleMap = {opacity: '1'}; + const styleMapWithPlayerFactory = bindPlayerFactory(buildStyleFn, cachedStyleMap); + const classMapWithPlayerFactory = bindPlayerFactory(buildClassFn, cachedClassMap); const styleMapPlayerBuilder = makePlayerBuilder(styleMapWithPlayerFactory, false); const classMapPlayerBuilder = makePlayerBuilder(classMapWithPlayerFactory, true); updateStylingMap(context, classMapWithPlayerFactory, styleMapWithPlayerFactory); @@ -2254,24 +2609,24 @@ describe('style and class based bindings', () => { ] as PlayerContext); assertContext(context, [ - masterConfig(17, false), // - [null, 2, false, null], // - [null, 'color', null], // - [null, 'foo', false], // - [1, 1, 1, 1, 9, 13], // - element, // - {map: true}, // - {opacity: '1'}, // + masterConfig(17, false), // + [null, 2, false, null], // + [null, null, 'color', null], // + [null, null, 'foo', false], // + [1, 1, 1, 1, 9, 13], // + element, // + [1, 0, 25, classMapWithPlayerFactory, 1], // + [1, 0, 17, styleMapWithPlayerFactory, 1], // playerContext, // #9 - cleanStyle(2, 25), + cleanStyle(3, 21), 'color', 'red', directiveOwnerPointers(0, 5), // #13 - cleanClass(2, 29), + cleanClass(3, 29), 'foo', true, directiveOwnerPointers(0, 7), @@ -2283,27 +2638,25 @@ describe('style and class based bindings', () => { directiveOwnerPointers(0, 3), // #21 + cleanStyle(3, 9), + 'color', + null, + 0, + + // #25 cleanClass(0, 0), 'map', true, directiveOwnerPointers(0, 1), - // #23 - cleanStyle(2, 9), - 'color', - null, - 0, - // #29 - cleanClass(2, 13), + cleanClass(3, 13), 'foo', null, 0, ]); - const styleMapWithoutPlayerFactory = {opacity: '1'}; - const classMapWithoutPlayerFactory = {map: true}; - updateStylingMap(context, classMapWithoutPlayerFactory, styleMapWithoutPlayerFactory); + updateStylingMap(context, cachedClassMap, cachedStyleMap); const colorWithoutPlayerFactory = 'blue'; const fooWithoutPlayerFactory = false; @@ -2317,24 +2670,24 @@ describe('style and class based bindings', () => { ] as PlayerContext); assertContext(context, [ - masterConfig(17, false), // - [null, 2, false, null], // - [null, 'color', null], // - [null, 'foo', false], // - [1, 1, 1, 1, 9, 13], // - element, // - {map: true}, // - {opacity: '1'}, // + masterConfig(17, false), // + [null, 2, false, null], // + [null, null, 'color', null], // + [null, null, 'foo', false], // + [1, 1, 1, 1, 9, 13], // + element, // + [1, 0, 25, cachedClassMap, 1], // + [1, 0, 17, cachedStyleMap, 1], // playerContext, // #9 - cleanStyle(2, 25), + cleanStyle(3, 21), 'color', 'blue', 0, // #13 - cleanClass(2, 29), + cleanClass(3, 29), 'foo', false, 0, @@ -2346,19 +2699,19 @@ describe('style and class based bindings', () => { 0, // #21 + cleanStyle(3, 9), + 'color', + null, + 0, + + // #25 cleanClass(0, 0), 'map', true, 0, - // #23 - cleanStyle(2, 9), - 'color', - null, - 0, - // #29 - cleanClass(2, 13), + cleanClass(3, 13), 'foo', null, 0, @@ -2382,9 +2735,8 @@ describe('style and class based bindings', () => { return new MockPlayer(); }; - const styleFactory = - bindPlayerFactory(buildStyleFn, {opacity: '1'}) as BoundPlayerFactory; - const classFactory = bindPlayerFactory(buildClassFn, 'bar') as BoundPlayerFactory; + let styleFactory = bindPlayerFactory(buildStyleFn, {opacity: '1'}) as BoundPlayerFactory; + let classFactory = bindPlayerFactory(buildClassFn, 'bar') as BoundPlayerFactory; updateStylingMap(context, classFactory, styleFactory); expect(styleCalls).toEqual(0); expect(classCalls).toEqual(0); @@ -2397,13 +2749,13 @@ describe('style and class based bindings', () => { expect(styleCalls).toEqual(1); expect(classCalls).toEqual(1); - styleFactory.value = {opacity: '0.5'}; + styleFactory = bindPlayerFactory(buildStyleFn, {opacity: '0.5'}) as BoundPlayerFactory; updateStylingMap(context, classFactory, styleFactory); renderStyles(context, false, undefined, lView); expect(styleCalls).toEqual(2); expect(classCalls).toEqual(1); - classFactory.value = 'foo'; + classFactory = bindPlayerFactory(buildClassFn, 'foo') as BoundPlayerFactory; updateStylingMap(context, classFactory, styleFactory); renderStyles(context, false, undefined, lView); expect(styleCalls).toEqual(2); @@ -2701,11 +3053,11 @@ describe('style and class based bindings', () => { return new MockPlayer(); }; - const styleMapFactory = + let styleMapFactory = bindPlayerFactory(buildFn, {height: '200px'}) as BoundPlayerFactory; - const classMapFactory = bindPlayerFactory(buildFn, {bar: true}) as BoundPlayerFactory; - const widthFactory = bindPlayerFactory(buildFn, '100px') as BoundPlayerFactory; - const fooFactory = bindPlayerFactory(buildFn, true) as BoundPlayerFactory; + let classMapFactory = bindPlayerFactory(buildFn, {bar: true}) as BoundPlayerFactory; + let widthFactory = bindPlayerFactory(buildFn, '100px') as BoundPlayerFactory; + let fooFactory = bindPlayerFactory(buildFn, true) as BoundPlayerFactory; class Comp { static ngComponentDef = defineComponent({ @@ -2746,6 +3098,11 @@ describe('style and class based bindings', () => { widthFactory.value = '50px'; fooFactory.value = false; + styleMapFactory = bindPlayerFactory(buildFn, {height: '100px'}) as BoundPlayerFactory; + classMapFactory = bindPlayerFactory(buildFn, {bar: false}) as BoundPlayerFactory; + widthFactory = bindPlayerFactory(buildFn, '50px') as BoundPlayerFactory; + fooFactory = bindPlayerFactory(buildFn, false) as BoundPlayerFactory; + fixture.update(); expect(firstRenderCaptures.length).toEqual(0); @@ -2903,11 +3260,19 @@ function assertContext(actual: StylingContext, target: StylingContext, startInde null; fieldName = 'Element Position'; break; - case StylingIndex.CachedClassValueOrInitialClassString: - case StylingIndex.CachedStyleValue: - valueIsTheSame = stringMapEqualsStringMap(actualValue, targetValue); - stringError = - !valueIsTheSame ? generateValueCompareError(actualValue, targetValue) : null; + case StylingIndex.CachedMultiClasses: + case StylingIndex.CachedMultiStyles: + valueIsTheSame = Array.isArray(actualValue) ? + valueEqualsValue(actualValue, targetValue) : + stringMapEqualsStringMap(actualValue, targetValue); + if (!valueIsTheSame) { + stringError = '\n\n ' + generateValueCompareError(actualValue, targetValue); + if (Array.isArray(actualValue)) { + stringError += '\n ....'; + stringError += + generateArrayCompareError(actualValue as any[], targetValue as any[], ' '); + } + } fieldName = 'Cached Style/Class Value'; break; default: @@ -2932,11 +3297,12 @@ function assertContext(actual: StylingContext, target: StylingContext, startInde function generateArrayCompareError(a: any[], b: any[], tab: string) { const values: string[] = []; - for (let i = 0; i < a.length; i++) { - if (a[i] !== b[i]) { - values.push(`${tab}a[${i}] !== b[${i}]`); + const length = Math.max(a.length, b.length); + for (let i = 0; i < length; i++) { + if (a[i] === b[i]) { + values.push(`${tab}a[${i}] === b[${i}] (${a[i]} === ${b[i]})`); } else { - values.push(`${tab}a[${i}] === b[${i}]`); + values.push(`${tab}a[${i}] !== b[${i}] (${a[i]} !== ${b[i]})`); } } return values.length ? '\n' + values.join('\n') : null;