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;