From ec5635430630fb895ddb43017f9939564824e8f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Tue, 2 Apr 2019 16:16:00 -0700 Subject: [PATCH] fix(ivy): ensure parent/sub-class components evaluate styling correctly (#29602) The new styling algorithm in angular is designed to evaluate host bindings stylinh priority in order of directive evaluation order. This, however, does not work with respect to parent/sub-class directives because sub-class host bindings are run after the parent host bindings but still have priority. This patch ensures that the host styling bindings for parent and sub-class components/directives are executed with respect to the styling algorithm prioritization. Jira Issue: FW-1132 PR Close #29602 --- integration/_payload-limits.json | 2 +- packages/core/src/render3/component.ts | 7 +- .../features/inherit_definition_feature.ts | 19 +- .../core/src/render3/instructions/element.ts | 36 +- .../render3/instructions/element_container.ts | 5 + .../core/src/render3/instructions/shared.ts | 29 +- .../instructions/styling_instructions.ts | 175 +-- .../core/src/render3/interfaces/styling.ts | 100 +- packages/core/src/render3/state.ts | 140 ++- .../styling/class_and_style_bindings.ts | 377 +++--- .../styling/host_instructions_queue.ts | 95 ++ packages/core/src/render3/styling/shared.ts | 17 + packages/core/src/render3/styling/util.ts | 35 +- packages/core/test/acceptance/styling_spec.ts | 168 ++- .../cyclic_import/bundle.golden_symbols.json | 19 +- .../hello_world/bundle.golden_symbols.json | 11 +- .../bundling/todo/bundle.golden_symbols.json | 40 +- .../styling/class_and_style_bindings_spec.ts | 1029 +++++++++-------- .../angular_material_test_blocklist.js | 13 +- 19 files changed, 1404 insertions(+), 913 deletions(-) create mode 100644 packages/core/src/render3/styling/host_instructions_queue.ts create mode 100644 packages/core/src/render3/styling/shared.ts diff --git a/integration/_payload-limits.json b/integration/_payload-limits.json index b27d86daaa..2d9ba4c9b0 100644 --- a/integration/_payload-limits.json +++ b/integration/_payload-limits.json @@ -12,7 +12,7 @@ "master": { "uncompressed": { "runtime": 1440, - "main": 14106, + "main": 14287, "polyfills": 43567 } } diff --git a/packages/core/src/render3/component.ts b/packages/core/src/render3/component.ts index b4a1b6b4e7..c93d8a10b0 100644 --- a/packages/core/src/render3/component.ts +++ b/packages/core/src/render3/component.ts @@ -23,7 +23,7 @@ import {PlayerHandler} from './interfaces/player'; import {RElement, Renderer3, RendererFactory3, domRendererFactory3} from './interfaces/renderer'; import {CONTEXT, FLAGS, HEADER_OFFSET, HOST, LView, LViewFlags, RENDERER, RootContext, RootContextFlags, TVIEW} from './interfaces/view'; import {applyOnCreateInstructions} from './node_util'; -import {enterView, getPreviousOrParentTNode, leaveView, resetComponentState} from './state'; +import {enterView, getPreviousOrParentTNode, leaveView, resetComponentState, setActiveHostElement} from './state'; import {renderInitialClasses, renderInitialStyles} from './styling/class_and_style_bindings'; import {publishDefaultGlobalUtils} from './util/global_utils'; import {defaultScheduler, renderStringify} from './util/misc_utils'; @@ -210,10 +210,15 @@ export function createRootComponent( const rootTNode = getPreviousOrParentTNode(); if (tView.firstTemplatePass && componentDef.hostBindings) { + const elementIndex = rootTNode.index - HEADER_OFFSET; + setActiveHostElement(elementIndex); + const expando = tView.expandoInstructions !; invokeHostBindingsInCreationMode( componentDef, expando, component, rootTNode, tView.firstTemplatePass); rootTNode.onElementCreationFns && applyOnCreateInstructions(rootTNode); + + setActiveHostElement(null); } if (rootTNode.stylingTemplate) { diff --git a/packages/core/src/render3/features/inherit_definition_feature.ts b/packages/core/src/render3/features/inherit_definition_feature.ts index 4e6f3e6152..dae44c86c3 100644 --- a/packages/core/src/render3/features/inherit_definition_feature.ts +++ b/packages/core/src/render3/features/inherit_definition_feature.ts @@ -10,6 +10,7 @@ import {Type} from '../../interface/type'; import {fillProperties} from '../../util/property'; import {EMPTY_ARRAY, EMPTY_OBJ} from '../empty'; import {ComponentDef, DirectiveDef, DirectiveDefFeature, RenderFlags} from '../interfaces/definition'; +import {adjustActiveDirectiveSuperClassDepthPosition} from '../state'; import {isComponentDef} from '../util/view_utils'; import {NgOnChangesFeature} from './ng_onchanges_feature'; @@ -63,8 +64,24 @@ export function InheritDefinitionFeature(definition: DirectiveDef| Componen const superHostBindings = superDef.hostBindings; if (superHostBindings) { if (prevHostBindings) { + // because inheritance is unknown during compile time, the runtime code + // needs to be informed of the super-class depth so that instruction code + // can distinguish one host bindings function from another. The reason why + // relying on the directive uniqueId exclusively is not enough is because the + // uniqueId value and the directive instance stay the same between hostBindings + // calls throughout the directive inheritance chain. This means that without + // a super-class depth value, there is no way to know whether a parent or + // sub-class host bindings function is currently being executed. definition.hostBindings = (rf: RenderFlags, ctx: any, elementIndex: number) => { - superHostBindings(rf, ctx, elementIndex); + // The reason why we increment first and then decrement is so that parent + // hostBindings calls have a higher id value compared to sub-class hostBindings + // calls (this way the leaf directive is always at a super-class depth of 0). + adjustActiveDirectiveSuperClassDepthPosition(1); + try { + superHostBindings(rf, ctx, elementIndex); + } finally { + adjustActiveDirectiveSuperClassDepthPosition(-1); + } prevHostBindings(rf, ctx, elementIndex); }; } else { diff --git a/packages/core/src/render3/instructions/element.ts b/packages/core/src/render3/instructions/element.ts index 12a33fa951..ab061d3720 100644 --- a/packages/core/src/render3/instructions/element.ts +++ b/packages/core/src/render3/instructions/element.ts @@ -11,20 +11,23 @@ import {assertHasParent} from '../assert'; import {attachPatchData} from '../context_discovery'; import {registerPostOrderHooks} from '../hooks'; import {TAttributes, TNodeFlags, TNodeType} from '../interfaces/node'; -import {RElement, Renderer3, isProceduralRenderer} from '../interfaces/renderer'; +import {RElement, isProceduralRenderer} from '../interfaces/renderer'; import {SanitizerFn} from '../interfaces/sanitization'; import {BINDING_INDEX, QUERIES, RENDERER, TVIEW} from '../interfaces/view'; import {assertNodeType} from '../node_assert'; import {appendChild} from '../node_manipulation'; import {applyOnCreateInstructions} from '../node_util'; -import {decreaseElementDepthCount, getActiveHostContext, getElementDepthCount, getIsParent, getLView, getNamespace, getPreviousOrParentTNode, increaseElementDepthCount, setIsParent, setPreviousOrParentTNode} from '../state'; +import {decreaseElementDepthCount, getActiveDirectiveId, getElementDepthCount, getIsParent, getLView, getNamespace, getPreviousOrParentTNode, getSelectedIndex, increaseElementDepthCount, setIsParent, setPreviousOrParentTNode} from '../state'; import {getInitialClassNameValue, getInitialStyleStringValue, initializeStaticContext, patchContextWithStaticAttrs, renderInitialClasses, renderInitialStyles} from '../styling/class_and_style_bindings'; import {getStylingContext, hasClassInput, hasStyleInput} from '../styling/util'; import {NO_CHANGE} from '../tokens'; import {attrsStylingIndexOf, setUpAttributes} from '../util/attrs_utils'; import {renderStringify} from '../util/misc_utils'; import {getNativeByIndex, getNativeByTNode, getTNode} from '../util/view_utils'; + import {createDirectivesAndLocals, createNodeAtIndex, elementCreate, executeContentQueries, initializeTNodeInputs, setInputsForProperty, setNodeStylingTemplate} from './shared'; +import {getActiveDirectiveStylingIndex} from './styling_instructions'; + /** * Create DOM element. The instruction must later be followed by `elementEnd()` call. @@ -256,17 +259,26 @@ export function elementAttribute( * @publicApi */ export function elementHostAttrs(attrs: TAttributes) { - const tNode = getPreviousOrParentTNode(); + const hostElementIndex = getSelectedIndex(); const lView = getLView(); - const native = getNativeByTNode(tNode, lView) as RElement; - const lastAttrIndex = setUpAttributes(native, attrs); - const stylingAttrsStartIndex = attrsStylingIndexOf(attrs, lastAttrIndex); - if (stylingAttrsStartIndex >= 0) { - const directive = getActiveHostContext(); - if (tNode.stylingTemplate) { - patchContextWithStaticAttrs(tNode.stylingTemplate, attrs, stylingAttrsStartIndex, directive); - } else { - tNode.stylingTemplate = initializeStaticContext(attrs, stylingAttrsStartIndex, directive); + const tNode = getTNode(hostElementIndex, lView); + + // non-element nodes (e.g. ``) are not rendered as actual + // element nodes and adding styles/classes on to them will cause runtime + // errors... + if (tNode.type === TNodeType.Element) { + const native = getNativeByTNode(tNode, lView) as RElement; + const lastAttrIndex = setUpAttributes(native, attrs); + const stylingAttrsStartIndex = attrsStylingIndexOf(attrs, lastAttrIndex); + if (stylingAttrsStartIndex >= 0) { + const directiveStylingIndex = getActiveDirectiveStylingIndex(); + if (tNode.stylingTemplate) { + patchContextWithStaticAttrs( + tNode.stylingTemplate, attrs, stylingAttrsStartIndex, directiveStylingIndex); + } else { + tNode.stylingTemplate = + initializeStaticContext(attrs, stylingAttrsStartIndex, directiveStylingIndex); + } } } } diff --git a/packages/core/src/render3/instructions/element_container.ts b/packages/core/src/render3/instructions/element_container.ts index 89a5d33639..29ac9d2598 100644 --- a/packages/core/src/render3/instructions/element_container.ts +++ b/packages/core/src/render3/instructions/element_container.ts @@ -13,6 +13,7 @@ import {TAttributes, TNodeType} from '../interfaces/node'; import {BINDING_INDEX, QUERIES, RENDERER, TVIEW} from '../interfaces/view'; import {assertNodeType} from '../node_assert'; import {appendChild} from '../node_manipulation'; +import {applyOnCreateInstructions} from '../node_util'; import {getIsParent, getLView, getPreviousOrParentTNode, setIsParent, setPreviousOrParentTNode} from '../state'; import {createDirectivesAndLocals, createNodeAtIndex, executeContentQueries, setNodeStylingTemplate} from './shared'; @@ -83,5 +84,9 @@ export function elementContainerEnd(): void { lView[QUERIES] = currentQueries.parent; } + // this is required for all host-level styling-related instructions to run + // in the correct order + previousOrParentTNode.onElementCreationFns && applyOnCreateInstructions(previousOrParentTNode); + registerPostOrderHooks(tView, previousOrParentTNode); } diff --git a/packages/core/src/render3/instructions/shared.ts b/packages/core/src/render3/instructions/shared.ts index 319756005b..8b0e0dd834 100644 --- a/packages/core/src/render3/instructions/shared.ts +++ b/packages/core/src/render3/instructions/shared.ts @@ -27,7 +27,7 @@ import {StylingContext} from '../interfaces/styling'; import {BINDING_INDEX, CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DECLARATION_VIEW, ExpandoInstructions, FLAGS, HEADER_OFFSET, HOST, INJECTOR, InitPhaseState, LView, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, RootContext, RootContextFlags, SANITIZER, TVIEW, TView, T_HOST} from '../interfaces/view'; import {assertNodeOfPossibleTypes, assertNodeType} from '../node_assert'; import {isNodeMatchingSelectorList} from '../node_selector_matcher'; -import {enterView, getBindingsEnabled, getCheckNoChangesMode, getIsParent, getLView, getNamespace, getPreviousOrParentTNode, isCreationMode, leaveView, namespaceHTML, resetComponentState, setActiveHost, setBindingRoot, setCheckNoChangesMode, setCurrentDirectiveDef, setCurrentQueryIndex, setIsParent, setPreviousOrParentTNode, setSelectedIndex} from '../state'; +import {enterView, getBindingsEnabled, getCheckNoChangesMode, getIsParent, getLView, getNamespace, getPreviousOrParentTNode, incrementActiveDirectiveId, isCreationMode, leaveView, namespaceHTML, resetComponentState, setActiveHostElement, setBindingRoot, setCheckNoChangesMode, setCurrentDirectiveDef, setCurrentQueryIndex, setIsParent, setPreviousOrParentTNode, setSelectedIndex} from '../state'; import {initializeStaticContext as initializeStaticStylingContext} from '../styling/class_and_style_bindings'; import {NO_CHANGE} from '../tokens'; import {attrsStylingIndexOf} from '../util/attrs_utils'; @@ -35,6 +35,7 @@ import {INTERPOLATION_DELIMITER, renderStringify} from '../util/misc_utils'; import {getLViewParent, getRootContext} from '../util/view_traversal_utils'; import {getComponentViewByIndex, getNativeByTNode, isComponentDef, isContentQueryHost, isRootView, readPatchedLView, resetPreOrderHookFlags, unwrapRNode, viewAttachedToChangeDetector} from '../util/view_utils'; + /** * A permanent marker promise which signifies that the current CD tree is * clean. @@ -107,6 +108,8 @@ export function setHostBindings(tView: TView, viewData: LView): void { // Negative numbers mean that we are starting new EXPANDO block and need to update // the current element and directive index. currentElementIndex = -instruction; + setActiveHostElement(currentElementIndex); + // Injector block and providers are taken into account. const providerCount = (tView.expandoInstructions[++i] as number); bindingRootIndex += INJECTOR_BLOOM_PARENT_SIZE + providerCount; @@ -124,14 +127,20 @@ export function setHostBindings(tView: TView, viewData: LView): void { if (instruction !== null) { viewData[BINDING_INDEX] = bindingRootIndex; const hostCtx = unwrapRNode(viewData[currentDirectiveIndex]); - setActiveHost(hostCtx, currentElementIndex); instruction(RenderFlags.Update, hostCtx, currentElementIndex); - setActiveHost(null); + + // Each directive gets a uniqueId value that is the same for both + // create and update calls when the hostBindings function is called. The + // directive uniqueId is not set anywhere--it is just incremented between + // each hostBindings call and is useful for helping instruction code + // uniquely determine which directive is currently active when executed. + incrementActiveDirectiveId(); } currentDirectiveIndex++; } } } + setActiveHostElement(null); } /** Refreshes content queries for all directives in the given view. */ @@ -897,15 +906,27 @@ function invokeDirectivesHostBindings(tView: TView, viewData: LView, tNode: TNod const end = tNode.directiveEnd; const expando = tView.expandoInstructions !; const firstTemplatePass = tView.firstTemplatePass; + const elementIndex = tNode.index - HEADER_OFFSET; + setActiveHostElement(elementIndex); + for (let i = start; i < end; i++) { const def = tView.data[i] as DirectiveDef; const directive = viewData[i]; if (def.hostBindings) { invokeHostBindingsInCreationMode(def, expando, directive, tNode, firstTemplatePass); + + // Each directive gets a uniqueId value that is the same for both + // create and update calls when the hostBindings function is called. The + // directive uniqueId is not set anywhere--it is just incremented between + // each hostBindings call and is useful for helping instruction code + // uniquely determine which directive is currently active when executed. + incrementActiveDirectiveId(); } else if (firstTemplatePass) { expando.push(null); } } + + setActiveHostElement(null); } export function invokeHostBindingsInCreationMode( @@ -914,9 +935,7 @@ export function invokeHostBindingsInCreationMode( const previousExpandoLength = expando.length; setCurrentDirectiveDef(def); const elementIndex = tNode.index - HEADER_OFFSET; - setActiveHost(directive, elementIndex); def.hostBindings !(RenderFlags.Create, directive, elementIndex); - setActiveHost(null); setCurrentDirectiveDef(null); // `hostBindings` function may or may not contain `allocHostVars` call // (e.g. it may not if it only contains host listeners), so we need to check whether diff --git a/packages/core/src/render3/instructions/styling_instructions.ts b/packages/core/src/render3/instructions/styling_instructions.ts index 07eb42cf69..c86162f81f 100644 --- a/packages/core/src/render3/instructions/styling_instructions.ts +++ b/packages/core/src/render3/instructions/styling_instructions.ts @@ -6,19 +6,24 @@ * found in the LICENSE file at https://angular.io/license */ import {StyleSanitizeFn} from '../../sanitization/style_sanitizer'; -import {TNode} from '../interfaces/node'; +import {TNode, TNodeType} from '../interfaces/node'; import {PlayerFactory} from '../interfaces/player'; import {FLAGS, HEADER_OFFSET, LViewFlags, RENDERER, RootContextFlags} from '../interfaces/view'; -import {getActiveHostContext, getActiveHostElementIndex, getLView, getPreviousOrParentTNode} from '../state'; +import {getActiveDirectiveId, getActiveDirectiveSuperClassDepth, getLView, getPreviousOrParentTNode, getSelectedIndex} from '../state'; import {getInitialClassNameValue, renderStyling, updateClassProp as updateElementClassProp, updateContextWithBindings, updateStyleProp as updateElementStyleProp, updateStylingMap} from '../styling/class_and_style_bindings'; +import {ParamsOf, enqueueHostInstruction, registerHostDirective} from '../styling/host_instructions_queue'; import {BoundPlayerFactory} from '../styling/player_factory'; -import {allocateDirectiveIntoContext, createEmptyStylingContext, forceClassesAsString, forceStylesAsString, getStylingContext, hasClassInput, hasStyleInput} from '../styling/util'; +import {DEFAULT_TEMPLATE_DIRECTIVE_INDEX} from '../styling/shared'; +import {allocateOrUpdateDirectiveIntoContext, createEmptyStylingContext, forceClassesAsString, forceStylesAsString, getStylingContext, hasClassInput, hasStyleInput} from '../styling/util'; import {NO_CHANGE} from '../tokens'; import {renderStringify} from '../util/misc_utils'; import {getRootContext} from '../util/view_traversal_utils'; import {getTNode} from '../util/view_utils'; + import {scheduleTick, setInputsForProperty} from './shared'; + + /* * The contents of this file include the instructions for all styling-related * operations in Angular. @@ -40,7 +45,6 @@ import {scheduleTick, setInputsForProperty} from './shared'; * - elementHostStylingApply */ - /** * Allocates style and class binding properties on the element during creation mode. * @@ -74,7 +78,9 @@ export function elementStyling( // components) then they will be applied at the end of the `elementEnd` // instruction (because directives are created first before styling is // executed for a new element). - initElementStyling(tNode, classBindingNames, styleBindingNames, styleSanitizer, null); + initElementStyling( + tNode, classBindingNames, styleBindingNames, styleSanitizer, + DEFAULT_TEMPLATE_DIRECTIVE_INDEX); } /** @@ -108,25 +114,28 @@ export function elementHostStyling( tNode.stylingTemplate = createEmptyStylingContext(); } - const directive = getActiveHostContext(); + const directiveStylingIndex = getActiveDirectiveStylingIndex(); // despite the binding being applied in a queue (below), the allocation // of the directive into the context happens right away. The reason for // this is to retain the ordering of the directives (which is important // for the prioritization of bindings). - allocateDirectiveIntoContext(tNode.stylingTemplate, directive); + allocateOrUpdateDirectiveIntoContext(tNode.stylingTemplate, directiveStylingIndex); const fns = tNode.onElementCreationFns = tNode.onElementCreationFns || []; - fns.push( - () => initElementStyling( - tNode, classBindingNames, styleBindingNames, styleSanitizer, directive)); + fns.push(() => { + initElementStyling( + tNode, classBindingNames, styleBindingNames, styleSanitizer, directiveStylingIndex); + registerHostDirective(tNode.stylingTemplate !, directiveStylingIndex); + }); } function initElementStyling( - tNode: TNode, classBindingNames?: string[] | null, styleBindingNames?: string[] | null, - styleSanitizer?: StyleSanitizeFn | null, directive?: {} | null): void { + tNode: TNode, classBindingNames: string[] | null | undefined, + styleBindingNames: string[] | null | undefined, + styleSanitizer: StyleSanitizeFn | null | undefined, directiveStylingIndex: number): void { updateContextWithBindings( - tNode.stylingTemplate !, directive || null, classBindingNames, styleBindingNames, + tNode.stylingTemplate !, directiveStylingIndex, classBindingNames, styleBindingNames, styleSanitizer); } @@ -160,7 +169,10 @@ function initElementStyling( export function elementStyleProp( index: number, styleIndex: number, value: string | number | String | PlayerFactory | null, suffix?: string | null, forceOverride?: boolean): void { - elementStylePropInternal(null, index, styleIndex, value, suffix, forceOverride); + const valueToAdd = resolveStylePropValue(value, suffix); + updateElementStyleProp( + getStylingContext(index + HEADER_OFFSET, getLView()), styleIndex, valueToAdd, + DEFAULT_TEMPLATE_DIRECTIVE_INDEX, forceOverride); } /** @@ -191,15 +203,20 @@ export function elementStyleProp( export function elementHostStyleProp( styleIndex: number, value: string | number | String | PlayerFactory | null, suffix?: string | null, forceOverride?: boolean): void { - elementStylePropInternal( - getActiveHostContext() !, getActiveHostElementIndex() !, styleIndex, value, suffix, - forceOverride); + const directiveStylingIndex = getActiveDirectiveStylingIndex(); + const hostElementIndex = getSelectedIndex(); + + const lView = getLView(); + const stylingContext = getStylingContext(hostElementIndex + HEADER_OFFSET, lView); + + const valueToAdd = resolveStylePropValue(value, suffix); + const args: ParamsOf = + [stylingContext, styleIndex, valueToAdd, directiveStylingIndex, forceOverride]; + enqueueHostInstruction(stylingContext, directiveStylingIndex, updateElementStyleProp, args); } -function elementStylePropInternal( - directive: {} | null, index: number, styleIndex: number, - value: string | number | String | PlayerFactory | null, suffix?: string | null, - forceOverride?: boolean): void { +function resolveStylePropValue( + value: string | number | String | PlayerFactory | null, suffix: string | null | undefined) { let valueToAdd: string|null = null; if (value !== null) { if (suffix) { @@ -214,9 +231,7 @@ function elementStylePropInternal( valueToAdd = value as any as string; } } - updateElementStyleProp( - getStylingContext(index + HEADER_OFFSET, getLView()), styleIndex, valueToAdd, directive, - forceOverride); + return valueToAdd; } @@ -241,7 +256,12 @@ function elementStylePropInternal( export function elementClassProp( index: number, classIndex: number, value: boolean | PlayerFactory, forceOverride?: boolean): void { - elementClassPropInternal(null, index, classIndex, value, forceOverride); + const input = (value instanceof BoundPlayerFactory) ? + (value as BoundPlayerFactory) : + booleanOrNull(value); + updateElementClassProp( + getStylingContext(index + HEADER_OFFSET, getLView()), classIndex, input, + DEFAULT_TEMPLATE_DIRECTIVE_INDEX, forceOverride); } @@ -265,19 +285,19 @@ export function elementClassProp( */ export function elementHostClassProp( classIndex: number, value: boolean | PlayerFactory, forceOverride?: boolean): void { - elementClassPropInternal( - getActiveHostContext() !, getActiveHostElementIndex() !, classIndex, value, forceOverride); -} + const directiveStylingIndex = getActiveDirectiveStylingIndex(); + const hostElementIndex = getSelectedIndex(); + + const lView = getLView(); + const stylingContext = getStylingContext(hostElementIndex + HEADER_OFFSET, lView); -function elementClassPropInternal( - directive: {} | null, index: number, classIndex: number, value: boolean | PlayerFactory, - forceOverride?: boolean): void { const input = (value instanceof BoundPlayerFactory) ? (value as BoundPlayerFactory) : booleanOrNull(value); - updateElementClassProp( - getStylingContext(index + HEADER_OFFSET, getLView()), classIndex, input, directive, - forceOverride); + + const args: ParamsOf = + [stylingContext, classIndex, input, directiveStylingIndex, forceOverride]; + enqueueHostInstruction(stylingContext, directiveStylingIndex, updateElementClassProp, args); } function booleanOrNull(value: any): boolean|null { @@ -309,7 +329,30 @@ function booleanOrNull(value: any): boolean|null { export function elementStylingMap( index: number, classes: {[key: string]: any} | string | NO_CHANGE | null, styles?: {[styleName: string]: any} | NO_CHANGE | null): void { - elementStylingMapInternal(null, index, classes, styles); + const lView = getLView(); + const tNode = getTNode(index, lView); + const stylingContext = getStylingContext(index + HEADER_OFFSET, lView); + + // 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 (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); } @@ -339,39 +382,15 @@ export function elementStylingMap( export function elementHostStylingMap( classes: {[key: string]: any} | string | NO_CHANGE | null, styles?: {[styleName: string]: any} | NO_CHANGE | null): void { - elementStylingMapInternal( - getActiveHostContext() !, getActiveHostElementIndex() !, classes, styles); -} + const directiveStylingIndex = getActiveDirectiveStylingIndex(); + const hostElementIndex = getSelectedIndex(); -function elementStylingMapInternal( - directive: {} | null, index: number, classes: {[key: string]: any} | string | NO_CHANGE | null, - styles?: {[styleName: string]: any} | NO_CHANGE | null): void { const lView = getLView(); - const tNode = getTNode(index, lView); - const stylingContext = getStylingContext(index + HEADER_OFFSET, lView); + const stylingContext = getStylingContext(hostElementIndex + HEADER_OFFSET, lView); - // 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); + const args: ParamsOf = + [stylingContext, classes, styles, directiveStylingIndex]; + enqueueHostInstruction(stylingContext, directiveStylingIndex, updateStylingMap, args); } @@ -387,7 +406,7 @@ function elementStylingMapInternal( * @publicApi */ export function elementStylingApply(index: number): void { - elementStylingApplyInternal(null, index); + elementStylingApplyInternal(DEFAULT_TEMPLATE_DIRECTIVE_INDEX, index); } /** @@ -401,17 +420,33 @@ export function elementStylingApply(index: number): void { * @publicApi */ export function elementHostStylingApply(): void { - elementStylingApplyInternal(getActiveHostContext() !, getActiveHostElementIndex() !); + elementStylingApplyInternal(getActiveDirectiveStylingIndex(), getSelectedIndex()); } -export function elementStylingApplyInternal(directive: {} | null, index: number): void { +export function elementStylingApplyInternal(directiveStylingIndex: number, index: number): void { const lView = getLView(); + const tNode = getTNode(index, lView); + + // if a non-element value is being processed then we can't render values + // on the element at all therefore by setting the renderer to null then + // the styling apply code knows not to actually apply the values... + const renderer = tNode.type === TNodeType.Element ? lView[RENDERER] : null; const isFirstRender = (lView[FLAGS] & LViewFlags.FirstLViewPass) !== 0; + const stylingContext = getStylingContext(index + HEADER_OFFSET, lView); const totalPlayersQueued = renderStyling( - getStylingContext(index + HEADER_OFFSET, lView), lView[RENDERER], lView, isFirstRender, null, - null, directive); + stylingContext, renderer, lView, isFirstRender, null, null, directiveStylingIndex); if (totalPlayersQueued > 0) { const rootContext = getRootContext(lView); scheduleTick(rootContext, RootContextFlags.FlushPlayers); } } + +export function getActiveDirectiveStylingIndex() { + // whenever a directive's hostBindings function is called a uniqueId value + // is assigned. Normally this is enough to help distinguish one directive + // from another for the styling context, but there are situations where a + // sub-class directive could inherit and assign styling in concert with a + // parent directive. To help the styling code distinguish between a parent + // sub-classed directive the inheritance depth is taken into account as well. + return getActiveDirectiveId() + getActiveDirectiveSuperClassDepth(); +} \ No newline at end of file diff --git a/packages/core/src/render3/interfaces/styling.ts b/packages/core/src/render3/interfaces/styling.ts index 93589c23da..0272270442 100644 --- a/packages/core/src/render3/interfaces/styling.ts +++ b/packages/core/src/render3/interfaces/styling.ts @@ -309,6 +309,21 @@ export interface StylingContext extends */ [StylingIndex.CachedMultiStyles]: any|MapBasedOffsetValues; + /** + * A queue of all hostStyling instructions. + * + * This array (queue) is populated only when host-level styling instructions + * (e.g. `hostStylingMap` and `hostClassProp`) are used to apply style and + * class values via host bindings to the host element. Despite these being + * standard angular instructions, they are not designed to immediately apply + * their values to the styling context when executed. What happens instead is + * a queue is constructed and each instruction is populated into the queue. + * Then, once the style/class values are set to flush (via `elementStylingApply` or + * `hostStylingApply`), the queue is flushed and the values are rendered onto + * the host element. + */ + [StylingIndex.HostInstructionsQueue]: HostInstructionsQueue|null; + /** * Location of animation context (which contains the active players) for this element styling * context. @@ -316,6 +331,66 @@ export interface StylingContext extends [StylingIndex.PlayerContext]: PlayerContext|null; } +/** + * A queue of all host-related styling instructions (these are buffered and evaluated just before + * the styling is applied). + * + * This queue is used when any `hostStyling` instructions are executed from the `hostBindings` + * function. Template-level styling functions (e.g. `elementStylingMap` and `elementClassProp`) + * do not make use of this queue (they are applied to the styling context immediately). + * + * Due to the nature of how components/directives are evaluated, directives (both parent and + * subclass directives) may not apply their styling at the right time for the styling + * algorithm code to prioritize them. Therefore, all host-styling instructions are queued up + * (buffered) into the array below and are automatically sorted in terms of priority. The + * priority for host-styling is as follows: + * + * 1. The template (this doesn't get queued, but gets evaluated immediately) + * 2. Any directives present on the host + * 2a) first child directive styling bindings are updated + * 2b) then any parent directives + * 3. Component host bindings + * + * Angular runs change detection for each of these cases in a different order. Because of this + * the array below is populated with each of the host styling functions + their arguments. + * + * context[HostInstructionsQueue] = [ + * directiveIndex, + * hostStylingFn, + * [argumentsForFn], + * ... + * anotherDirectiveIndex, <-- this has a lower priority (a higher directive index) + * anotherHostStylingFn, + * [argumentsForFn], + * ] + * + * When `renderStyling` is called (within `class_and_host_bindings.ts`) then the queue is + * drained and each of the instructions are executed. Once complete the queue is empty then + * the style/class binding code is rendered on the element (which is what happens normally + * inside of `renderStyling`). + * + * Right now each directive's hostBindings function, as well the template function, both + * call `elementStylingApply()` and `hostStylingApply()`. The fact that this is called + * multiple times for the same element (b/c of change detection) causes some issues. To avoid + * having styling code be rendered on an element multiple times, the `HostInstructionsQueue` + * reserves a slot for a reference pointing to the very last directive that was registered and + * only allows for styling to be applied once that directive is encountered (which will happen + * as the last update for that element). + */ +export interface HostInstructionsQueue extends Array { [0]: number; } + +/** + * Used as a reference for any values contained within `HostInstructionsQueue`. + */ +export const enum HostInstructionsQueueIndex { + LastRegisteredDirectiveIndexPosition = 0, + ValuesStartPosition = 1, + DirectiveIndexOffset = 0, + InstructionFnOffset = 1, + ParamsOffset = 2, + Size = 3, +} + /** * Used as a styling array to house static class and style values that were extracted * by the compiler and placed in the animation context via `elementStart` and @@ -511,9 +586,7 @@ export const enum InitialStylingValuesIndex { * 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; [DirectiveRegistryValuesIndex.SinglePropValuesIndexOffset]: number; - [DirectiveRegistryValuesIndex.DirtyFlagOffset]: boolean; [DirectiveRegistryValuesIndex.StyleSanitizerOffset]: StyleSanitizeFn|null; } @@ -522,11 +595,9 @@ export interface DirectiveRegistryValues extends Array + * + * dirOne->hostBindings() (id == 1) + * dirTwo->hostBindings() (id == 2) + * + * Note that this is only active when `hostBinding` functions are being processed. + * + * Note that directive id values are specific to an element (this means that + * the same id value could be present on another element with a completely + * different set of directives). + */ +export function getActiveDirectiveId() { + return activeDirectiveId; } -export function getActiveHostElementIndex() { - return activeHostElementIndex; +/** + * Increments the current directive id value. + * + * For example we have an element that has two directives on it: + *
+ * + * dirOne->hostBindings() (index = 1) + * // increment + * dirTwo->hostBindings() (index = 2) + * + * Depending on whether or not a previous directive had any inherited + * directives present, that value will be incremented in addition + * to the id jumping up by one. + * + * Note that this is only active when `hostBinding` functions are being processed. + * + * Note that directive id values are specific to an element (this means that + * the same id value could be present on another element with a completely + * different set of directives). + */ +export function incrementActiveDirectiveId() { + activeDirectiveId += 1 + activeDirectiveSuperClassHeight; + + // because we are dealing with a new directive this + // means we have exited out of the inheritance chain + activeDirectiveSuperClassDepthPosition = 0; + activeDirectiveSuperClassHeight = 0; +} + +/** + * Set the current super class (reverse inheritance) position depth for a directive. + * + * For example we have two directives: Child and Other (but Child is a sub-class of Parent) + *
+ * + * // increment + * parentInstance->hostBindings() (depth = 1) + * // decrement + * childInstance->hostBindings() (depth = 0) + * otherInstance->hostBindings() (depth = 0 b/c it's a different directive) + * + * Note that this is only active when `hostBinding` functions are being processed. + */ +export function adjustActiveDirectiveSuperClassDepthPosition(delta: number) { + activeDirectiveSuperClassDepthPosition += delta; + + // we keep track of the height value so that when the next directive is visited + // then Angular knows to generate a new directive id value which has taken into + // account how many sub-class directives were a part of the previous directive. + activeDirectiveSuperClassHeight = + Math.max(activeDirectiveSuperClassHeight, activeDirectiveSuperClassDepthPosition); +} + +/** + * Returns the current super class (reverse inheritance) depth for a directive. + * + * This is designed to help instruction code distinguish different hostBindings + * calls from each other when a directive has extended from another directive. + * Normally using the directive id value is enough, but with the case + * of parent/sub-class directive inheritance more information is required. + * + * Note that this is only active when `hostBinding` functions are being processed. + */ +export function getActiveDirectiveSuperClassDepth() { + return activeDirectiveSuperClassDepthPosition; } /** 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 f4b933f583..59d13727c3 100644 --- a/packages/core/src/render3/styling/class_and_style_bindings.ts +++ b/packages/core/src/render3/styling/class_and_style_bindings.ts @@ -10,14 +10,14 @@ 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, MapBasedOffsetValues, MapBasedOffsetValuesIndex, SinglePropOffsetValues, SinglePropOffsetValuesIndex, StylingContext, StylingFlags, StylingIndex} from '../interfaces/styling'; +import {DirectiveOwnerAndPlayerBuilderIndex, 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/view_traversal_utils'; +import {allowFlush as allowHostInstructionsQueueFlush, flushQueue as flushHostInstructionsQueue} from './host_instructions_queue'; import {BoundPlayerFactory} from './player_factory'; -import {addPlayerInternal, allocPlayerContext, allocateDirectiveIntoContext, createEmptyStylingContext, getPlayerContext} from './util'; - +import {addPlayerInternal, allocPlayerContext, allocateOrUpdateDirectiveIntoContext, createEmptyStylingContext, getPlayerContext} from './util'; /** @@ -42,9 +42,9 @@ import {addPlayerInternal, allocPlayerContext, allocateDirectiveIntoContext, cre * Creates a new StylingContext an fills it with the provided static styling attribute values. */ export function initializeStaticContext( - attrs: TAttributes, stylingStartIndex: number, directiveRef?: any | null): StylingContext { + attrs: TAttributes, stylingStartIndex: number, directiveIndex: number = 0): StylingContext { const context = createEmptyStylingContext(); - patchContextWithStaticAttrs(context, attrs, stylingStartIndex, directiveRef); + patchContextWithStaticAttrs(context, attrs, stylingStartIndex, directiveIndex); return context; } @@ -57,25 +57,14 @@ export function initializeStaticContext( * assigned to the context * @param attrsStylingStartIndex what index to start iterating within the * provided `attrs` array to start reading style and class values - * @param directiveRef the directive instance with which static data is associated with. */ export function patchContextWithStaticAttrs( context: StylingContext, attrs: TAttributes, attrsStylingStartIndex: number, - directiveRef?: any | null): void { + directiveIndex: number): void { // this means the context has already been set and instantiated if (context[StylingIndex.MasterFlagPosition] & StylingFlags.BindingAllocationLocked) return; - // 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]; - let detectedIndex = getDirectiveRegistryValuesIndexOf(directives, directiveRef || null); - if (detectedIndex === -1) { - // this is a new directive which we have not seen yet. - detectedIndex = allocateDirectiveIntoContext(context, directiveRef); - } - const directiveIndex = detectedIndex / DirectiveRegistryValuesIndex.Size; + allocateOrUpdateDirectiveIntoContext(context, directiveIndex); let initialClasses: InitialStylingValues|null = null; let initialStyles: InitialStylingValues|null = null; @@ -199,7 +188,6 @@ export function allowNewBindingsForStylingContext(context: StylingContext): bool * reference the provided directive. * * @param context the existing styling context - * @param directiveRef the directive that the new bindings will reference * @param classBindingNames an array of class binding names that will be added to the context * @param styleBindingNames an array of style binding names that will be added to the context * @param styleSanitizer an optional sanitizer that handle all sanitization on for each of @@ -207,13 +195,14 @@ export function allowNewBindingsForStylingContext(context: StylingContext): bool * instance will only be active if and when the directive updates the bindings that it owns. */ export function updateContextWithBindings( - context: StylingContext, directiveRef: any | null, classBindingNames?: string[] | null, + context: StylingContext, directiveIndex: number, classBindingNames?: string[] | null, 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 - const directiveIndex = findOrPatchDirectiveIntoRegistry(context, directiveRef, styleSanitizer); - if (directiveIndex === -1) { + const isNewDirective = + findOrPatchDirectiveIntoRegistry(context, directiveIndex, false, styleSanitizer); + if (!isNewDirective) { // this means the directive has already been patched in ... No point in doing anything return; } @@ -470,46 +459,22 @@ export function updateContextWithBindings( * Searches through the existing registry of directives */ export function findOrPatchDirectiveIntoRegistry( - context: StylingContext, directiveRef: any, styleSanitizer?: StyleSanitizeFn | null) { - const directiveRefs = context[StylingIndex.DirectiveRegistryPosition]; - const nextOffsetInsertionIndex = context[StylingIndex.SinglePropOffsetPositions].length; + context: StylingContext, directiveIndex: number, staticModeOnly: boolean, + styleSanitizer?: StyleSanitizeFn | null): boolean { + const directiveRegistry = context[StylingIndex.DirectiveRegistryPosition]; + const index = directiveIndex * DirectiveRegistryValuesIndex.Size; + const singlePropStartPosition = index + DirectiveRegistryValuesIndex.SinglePropValuesIndexOffset; - let directiveIndex: number; - let detectedIndex = getDirectiveRegistryValuesIndexOf(directiveRefs, directiveRef); + // this means that the directive has already been registered into the registry + if (index < directiveRegistry.length && + (directiveRegistry[singlePropStartPosition] as number) >= 0) + return false; - if (detectedIndex === -1) { - detectedIndex = directiveRefs.length; - directiveIndex = directiveRefs.length / DirectiveRegistryValuesIndex.Size; - - allocateDirectiveIntoContext(context, directiveRef); - directiveRefs[detectedIndex + DirectiveRegistryValuesIndex.SinglePropValuesIndexOffset] = - nextOffsetInsertionIndex; - directiveRefs[detectedIndex + DirectiveRegistryValuesIndex.StyleSanitizerOffset] = - styleSanitizer || null; - } else { - const singlePropStartPosition = - detectedIndex + DirectiveRegistryValuesIndex.SinglePropValuesIndexOffset; - if (directiveRefs[singlePropStartPosition] ! >= 0) { - // the directive has already been patched into the context - return -1; - } - - directiveIndex = detectedIndex / DirectiveRegistryValuesIndex.Size; - - // because the directive already existed this means that it was set during elementHostAttrs or - // elementStart which means that the binding values were not here. Therefore, the values below - // need to be applied so that single class and style properties can be assigned later. - const singlePropPositionIndex = - detectedIndex + DirectiveRegistryValuesIndex.SinglePropValuesIndexOffset; - directiveRefs[singlePropPositionIndex] = nextOffsetInsertionIndex; - - // the sanitizer is also apart of the binding process and will be used when bindings are - // applied. - const styleSanitizerIndex = detectedIndex + DirectiveRegistryValuesIndex.StyleSanitizerOffset; - directiveRefs[styleSanitizerIndex] = styleSanitizer || null; - } - - return directiveIndex; + const singlePropsStartIndex = + staticModeOnly ? -1 : context[StylingIndex.SinglePropOffsetPositions].length; + allocateOrUpdateDirectiveIntoContext( + context, directiveIndex, singlePropsStartIndex, styleSanitizer); + return true; } function getMatchingBindingIndex( @@ -543,18 +508,13 @@ function getMatchingBindingIndex( * 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| null, stylesInput?: {[key: string]: any} | BoundPlayerFactory| null, - directiveRef?: any): void { - const directiveIndex = getDirectiveIndexFromRegistry(context, directiveRef || null); - + directiveIndex: number = 0): void { + ngDevMode && assertValidDirectiveIndex(context, directiveIndex); classesInput = classesInput || null; stylesInput = stylesInput || null; const ignoreAllClassUpdates = isMultiValueCacheHit(context, true, directiveIndex, classesInput); @@ -887,7 +847,6 @@ function patchStylingMapIntoContext( if (dirty) { setContextDirty(context, true); - setDirectiveDirty(context, directiveIndex, true); } return totalNewAllocatedSlots; @@ -901,18 +860,14 @@ function patchStylingMapIntoContext( * 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, - input: boolean | BoundPlayerFactory| null, directiveRef?: any, + input: boolean | BoundPlayerFactory| null, directiveIndex: number = 0, forceOverride?: boolean): void { - updateSingleStylingValue(context, offset, input, true, directiveRef, forceOverride); + updateSingleStylingValue(context, offset, input, true, directiveIndex, forceOverride); } /** @@ -928,25 +883,21 @@ export function updateClassProp( * newly provided style value. * @param offset The index of the property which is being updated. * @param value The CSS style value that will be assigned - * @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 updateStyleProp( context: StylingContext, offset: number, - input: string | boolean | null | BoundPlayerFactory, directiveRef?: any, - forceOverride?: boolean): void { - updateSingleStylingValue(context, offset, input, false, directiveRef, forceOverride); + input: string | boolean | null | BoundPlayerFactory, + directiveIndex: number = 0, forceOverride?: boolean): void { + updateSingleStylingValue(context, offset, input, false, directiveIndex, forceOverride); } function updateSingleStylingValue( context: StylingContext, offset: number, input: string | boolean | null | BoundPlayerFactory, isClassBased: boolean, - directiveRef: any, forceOverride?: boolean): void { - const directiveIndex = getDirectiveIndexFromRegistry(context, directiveRef || null); + directiveIndex: number, forceOverride?: boolean): void { + ngDevMode && assertValidDirectiveIndex(context, directiveIndex); const singleIndex = getSinglePropIndexValue(context, directiveIndex, offset, isClassBased); const currValue = getValue(context, singleIndex); const currFlag = getPointers(context, singleIndex); @@ -1001,7 +952,6 @@ function updateSingleStylingValue( setDirty(context, indexForMulti, multiDirty); setDirty(context, singleIndex, singleDirty); - setDirectiveDirty(context, directiveIndex, true); setContextDirty(context, true); } @@ -1029,120 +979,128 @@ function updateSingleStylingValue( * to this key/value map instead of being renderered via the renderer. * @param stylesStore if provided, the updated style values will be applied * to this key/value map instead of being renderered via the renderer. - * @param directiveRef an optional directive that will be used to target which - * styling values are rendered. If left empty, only the bindings that are - * registered on the template will be rendered. * @returns number the total amount of players that got queued for animation (if any) */ export function renderStyling( - context: StylingContext, renderer: Renderer3, rootOrView: RootContext | LView, + context: StylingContext, renderer: Renderer3 | null, rootOrView: RootContext | LView, isFirstRender: boolean, classesStore?: BindingStore | null, stylesStore?: BindingStore | null, - directiveRef?: any): number { + directiveIndex: number = 0): number { let totalPlayersQueued = 0; - const targetDirectiveIndex = getDirectiveIndexFromRegistry(context, directiveRef || null); - if (isContextDirty(context) && isDirectiveDirty(context, targetDirectiveIndex)) { - const flushPlayerBuilders: any = - context[StylingIndex.MasterFlagPosition] & StylingFlags.PlayerBuildersDirty; - const native = context[StylingIndex.ElementPosition] !; - const multiStartIndex = getMultiStylesStartIndex(context); + // this prevents multiple attempts to render style/class values on + // the same element... + if (allowHostInstructionsQueueFlush(context, directiveIndex)) { + // all styling instructions present within any hostBindings functions + // do not update the context immediately when called. They are instead + // queued up and applied to the context right at this point. Why? This + // is because Angular evaluates component/directive and directive + // sub-class code at different points and it's important that the + // styling values are applied to the context in the right order + // (see `interfaces/styling.ts` for more information). + flushHostInstructionsQueue(context); - let stillDirty = false; - for (let i = StylingIndex.SingleStylesStartPosition; i < context.length; - i += StylingIndex.Size) { - // there is no point in rendering styles that have not changed on screen - if (isDirty(context, i)) { - const flag = getPointers(context, i); - const directiveIndex = getDirectiveIndexFromEntry(context, i); - if (targetDirectiveIndex !== directiveIndex) { - stillDirty = true; - continue; - } + if (isContextDirty(context)) { + // this is here to prevent things like ... + // or if there are any host style or class bindings present in a directive set on + // a container node + const native = context[StylingIndex.ElementPosition] !as HTMLElement; - const prop = getProp(context, i); - const value = getValue(context, i); - const styleSanitizer = - (flag & StylingFlags.Sanitize) ? getStyleSanitizer(context, directiveIndex) : null; - const playerBuilder = getPlayerBuilder(context, i); - const isClassBased = flag & StylingFlags.Class ? true : false; - const isInSingleRegion = i < multiStartIndex; + const flushPlayerBuilders: any = + context[StylingIndex.MasterFlagPosition] & StylingFlags.PlayerBuildersDirty; + const multiStartIndex = getMultiStylesStartIndex(context); - let valueToApply: string|boolean|null = value; + for (let i = StylingIndex.SingleStylesStartPosition; i < context.length; + i += StylingIndex.Size) { + // there is no point in rendering styles that have not changed on screen + if (isDirty(context, i)) { + const flag = getPointers(context, i); + const directiveIndex = getDirectiveIndexFromEntry(context, i); + const prop = getProp(context, i); + const value = getValue(context, i); + const styleSanitizer = + (flag & StylingFlags.Sanitize) ? getStyleSanitizer(context, directiveIndex) : null; + const playerBuilder = getPlayerBuilder(context, i); + const isClassBased = flag & StylingFlags.Class ? true : false; + const isInSingleRegion = i < multiStartIndex; - // VALUE DEFER CASE 1: Use a multi value instead of a null single value - // this check implies that a single value was removed and we - // should now defer to a multi value and use that (if set). - if (isInSingleRegion && !valueExists(valueToApply, isClassBased)) { - // single values ALWAYS have a reference to a multi index - const multiIndex = getMultiOrSingleIndex(flag); - valueToApply = getValue(context, multiIndex); - } + let valueToApply: string|boolean|null = value; - // VALUE DEFER CASE 2: Use the initial value if all else fails (is falsy) - // the initial value will always be a string or null, - // therefore we can safely adopt it in case there's nothing else - // note that this should always be a falsy check since `false` is used - // for both class and style comparisons (styles can't be false and false - // 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 (!valueExists(valueToApply, isClassBased)) { - valueToApply = getInitialValue(context, flag); - } - - // if the first render is true then we do not want to start applying falsy - // values to the DOM element's styling. Otherwise then we know there has - // been a change and even if it's falsy then it's removing something that - // was truthy before. - const doApplyValue = isFirstRender ? valueToApply : true; - if (doApplyValue) { - if (isClassBased) { - setClass( - native, prop, valueToApply ? true : false, renderer, classesStore, playerBuilder); - } else { - setStyle( - native, prop, valueToApply as string | null, renderer, styleSanitizer, stylesStore, - playerBuilder); + // VALUE DEFER CASE 1: Use a multi value instead of a null single value + // this check implies that a single value was removed and we + // should now defer to a multi value and use that (if set). + if (isInSingleRegion && !valueExists(valueToApply, isClassBased)) { + // single values ALWAYS have a reference to a multi index + const multiIndex = getMultiOrSingleIndex(flag); + valueToApply = getValue(context, multiIndex); } - } - setDirty(context, i, false); - } - } + // VALUE DEFER CASE 2: Use the initial value if all else fails (is falsy) + // the initial value will always be a string or null, + // therefore we can safely adopt it in case there's nothing else + // note that this should always be a falsy check since `false` is used + // for both class and style comparisons (styles can't be false and false + // 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 (!valueExists(valueToApply, isClassBased)) { + valueToApply = getInitialValue(context, flag); + } - if (flushPlayerBuilders) { - const rootContext = - Array.isArray(rootOrView) ? getRootContext(rootOrView) : rootOrView as RootContext; - const playerContext = getPlayerContext(context) !; - const playersStartIndex = playerContext[PlayerIndex.NonBuilderPlayersStart]; - for (let i = PlayerIndex.PlayerBuildersStartPosition; i < playersStartIndex; - i += PlayerIndex.PlayerAndPlayerBuildersTupleSize) { - const builder = playerContext[i] as ClassAndStylePlayerBuilder| null; - const playerInsertionIndex = i + PlayerIndex.PlayerOffsetPosition; - const oldPlayer = playerContext[playerInsertionIndex] as Player | null; - if (builder) { - const player = builder.buildPlayer(oldPlayer, isFirstRender); - if (player !== undefined) { - if (player != null) { - const wasQueued = addPlayerInternal( - playerContext, rootContext, native as HTMLElement, player, playerInsertionIndex); - wasQueued && totalPlayersQueued++; - } - if (oldPlayer) { - oldPlayer.destroy(); + // if the first render is true then we do not want to start applying falsy + // values to the DOM element's styling. Otherwise then we know there has + // been a change and even if it's falsy then it's removing something that + // was truthy before. + const doApplyValue = renderer && (isFirstRender ? valueToApply : true); + if (doApplyValue) { + if (isClassBased) { + setClass( + native, prop, valueToApply ? true : false, renderer !, classesStore, + playerBuilder); + } else { + setStyle( + native, prop, valueToApply as string | null, renderer !, styleSanitizer, + stylesStore, playerBuilder); } } - } else if (oldPlayer) { - // the player builder has been removed ... therefore we should delete the associated - // player - oldPlayer.destroy(); + + setDirty(context, i, false); } } - setContextPlayersDirty(context, false); - } - setDirectiveDirty(context, targetDirectiveIndex, false); - setContextDirty(context, stillDirty); + if (flushPlayerBuilders) { + const rootContext = + Array.isArray(rootOrView) ? getRootContext(rootOrView) : rootOrView as RootContext; + const playerContext = getPlayerContext(context) !; + const playersStartIndex = playerContext[PlayerIndex.NonBuilderPlayersStart]; + for (let i = PlayerIndex.PlayerBuildersStartPosition; i < playersStartIndex; + i += PlayerIndex.PlayerAndPlayerBuildersTupleSize) { + const builder = playerContext[i] as ClassAndStylePlayerBuilder| null; + const playerInsertionIndex = i + PlayerIndex.PlayerOffsetPosition; + const oldPlayer = playerContext[playerInsertionIndex] as Player | null; + if (builder) { + const player = builder.buildPlayer(oldPlayer, isFirstRender); + if (player !== undefined) { + if (player != null) { + const wasQueued = addPlayerInternal( + playerContext, rootContext, native as HTMLElement, player, + playerInsertionIndex); + wasQueued && totalPlayersQueued++; + } + if (oldPlayer) { + oldPlayer.destroy(); + } + } + } else if (oldPlayer) { + // the player builder has been removed ... therefore we should delete the associated + // player + oldPlayer.destroy(); + } + } + setContextPlayersDirty(context, false); + } + + setContextDirty(context, false); + } } return totalPlayersQueued; @@ -1622,43 +1580,6 @@ export function getDirectiveIndexFromEntry(context: StylingContext, index: numbe return value & DirectiveOwnerAndPlayerBuilderIndex.BitMask; } -function getDirectiveIndexFromRegistry(context: StylingContext, directiveRef: any) { - let directiveIndex: number; - - const dirs = context[StylingIndex.DirectiveRegistryPosition]; - let index = getDirectiveRegistryValuesIndexOf(dirs, directiveRef); - if (index === -1) { - // if the directive was not allocated then this means that styling is - // being applied in a dynamic way AFTER the element was already instantiated - index = dirs.length; - directiveIndex = index > 0 ? index / DirectiveRegistryValuesIndex.Size : 0; - - dirs.push(null, null, null, null); - dirs[index + DirectiveRegistryValuesIndex.DirectiveValueOffset] = directiveRef; - dirs[index + DirectiveRegistryValuesIndex.DirtyFlagOffset] = false; - dirs[index + DirectiveRegistryValuesIndex.SinglePropValuesIndexOffset] = -1; - - const classesStartIndex = - getMultiClassesStartIndex(context) || StylingIndex.SingleStylesStartPosition; - registerMultiMapEntry(context, directiveIndex, true, context.length); - registerMultiMapEntry(context, directiveIndex, false, classesStartIndex); - } else { - directiveIndex = index > 0 ? index / DirectiveRegistryValuesIndex.Size : 0; - } - - return directiveIndex; -} - -function getDirectiveRegistryValuesIndexOf( - directives: DirectiveRegistryValues, directive: {}): number { - for (let i = 0; i < directives.length; i += DirectiveRegistryValuesIndex.Size) { - if (directives[i] === directive) { - return i; - } - } - return -1; -} - function getInitialStylingValuesIndexOf(keyValues: InitialStylingValues, key: string): number { for (let i = InitialStylingValuesIndex.KeyValueStartPosition; i < keyValues.length; i += InitialStylingValuesIndex.Size) { @@ -1724,21 +1645,6 @@ function getStyleSanitizer(context: StylingContext, directiveIndex: number): Sty return value as StyleSanitizeFn | null; } -function isDirectiveDirty(context: StylingContext, directiveIndex: number): boolean { - const dirs = context[StylingIndex.DirectiveRegistryPosition]; - return dirs - [directiveIndex * DirectiveRegistryValuesIndex.Size + - DirectiveRegistryValuesIndex.DirtyFlagOffset] as boolean; -} - -function setDirectiveDirty( - context: StylingContext, directiveIndex: number, dirtyYes: boolean): void { - const dirs = context[StylingIndex.DirectiveRegistryPosition]; - dirs - [directiveIndex * DirectiveRegistryValuesIndex.Size + - DirectiveRegistryValuesIndex.DirtyFlagOffset] = dirtyYes; -} - function allowValueChange( currentValue: string | boolean | null, newValue: string | boolean | null, currentDirectiveOwner: number, newDirectiveOwner: number) { @@ -1994,3 +1900,12 @@ function addOrUpdateStaticStyle( staticStyles[index + InitialStylingValuesIndex.DirectiveOwnerOffset] = directiveOwnerIndex; return index; } + +function assertValidDirectiveIndex(context: StylingContext, directiveIndex: number) { + const dirs = context[StylingIndex.DirectiveRegistryPosition]; + const index = directiveIndex * DirectiveRegistryValuesIndex.Size; + if (index >= dirs.length || + dirs[index + DirectiveRegistryValuesIndex.SinglePropValuesIndexOffset] === -1) { + throw new Error('The provided directive is not registered with the styling context'); + } +} \ No newline at end of file diff --git a/packages/core/src/render3/styling/host_instructions_queue.ts b/packages/core/src/render3/styling/host_instructions_queue.ts new file mode 100644 index 0000000000..07d01cd94a --- /dev/null +++ b/packages/core/src/render3/styling/host_instructions_queue.ts @@ -0,0 +1,95 @@ +/** +* @license +* Copyright Google Inc. All Rights Reserved. +* +* Use of this source code is governed by an MIT-style license that can be +* found in the LICENSE file at https://angular.io/license +*/ +import {HostInstructionsQueue, HostInstructionsQueueIndex, StylingContext, StylingIndex} from '../interfaces/styling'; +import {DEFAULT_TEMPLATE_DIRECTIVE_INDEX} from '../styling/shared'; + +/* + * This file contains the logic to defer all hostBindings-related styling code to run + * at a later point, instead of immediately (as is the case with how template-level + * styling instructions are run). + * + * Certain styling instructions, present within directives, components and sub-classed + * directives, are evaluated at different points (depending on priority) and will therefore + * not be applied to the styling context of an element immediately. They are instead + * designed to be applied just before styling is applied to an element. + * + * (The priority for when certain host-related styling operations are executed is discussed + * more within `interfaces/styling.ts`.) + */ + +export function registerHostDirective(context: StylingContext, directiveIndex: number) { + let buffer = context[StylingIndex.HostInstructionsQueue]; + if (!buffer) { + buffer = context[StylingIndex.HostInstructionsQueue] = [DEFAULT_TEMPLATE_DIRECTIVE_INDEX]; + } + buffer[HostInstructionsQueueIndex.LastRegisteredDirectiveIndexPosition] = directiveIndex; +} + +/** + * Queues a styling instruction to be run just before `renderStyling()` is executed. + */ +export function enqueueHostInstruction( + context: StylingContext, priority: number, instructionFn: T, instructionFnArgs: ParamsOf) { + const buffer: HostInstructionsQueue = context[StylingIndex.HostInstructionsQueue] !; + const index = findNextInsertionIndex(buffer, priority); + buffer.splice(index, 0, priority, instructionFn, instructionFnArgs); +} + +/** + * Figures out where exactly to to insert the next host instruction queue entry. + */ +function findNextInsertionIndex(buffer: HostInstructionsQueue, priority: number): number { + for (let i = HostInstructionsQueueIndex.ValuesStartPosition; i < buffer.length; + i += HostInstructionsQueueIndex.Size) { + const p = buffer[i + HostInstructionsQueueIndex.DirectiveIndexOffset] as number; + if (p > priority) { + return i; + } + } + return buffer.length; +} + +/** + * Iterates through the host instructions queue (if present within the provided + * context) and executes each queued instruction entry. + */ +export function flushQueue(context: StylingContext): void { + const buffer = context[StylingIndex.HostInstructionsQueue]; + if (buffer) { + for (let i = HostInstructionsQueueIndex.ValuesStartPosition; i < buffer.length; + i += HostInstructionsQueueIndex.Size) { + const fn = buffer[i + HostInstructionsQueueIndex.InstructionFnOffset] as Function; + const args = buffer[i + HostInstructionsQueueIndex.ParamsOffset] as any[]; + fn(...args); + } + buffer.length = HostInstructionsQueueIndex.ValuesStartPosition; + } +} + +/** + * Determines whether or not to allow the host instructions queue to be flushed or not. + * + * Because the hostBindings function code is unaware of the presence of other host bindings + * (as well as the template function) then styling is evaluated multiple times per element. + * To prevent style and class values from being applied to the element multiple times, a + * flush is only allowed when the last directive (the directive that was registered into + * the styling context) attempts to render its styling. + */ +export function allowFlush(context: StylingContext, directiveIndex: number): boolean { + const buffer = context[StylingIndex.HostInstructionsQueue]; + if (buffer) { + return buffer[HostInstructionsQueueIndex.LastRegisteredDirectiveIndexPosition] === + directiveIndex; + } + return true; +} + +/** + * Infers the parameters of a given function into a typed array. + */ +export type ParamsOf = T extends(...args: infer T) => any ? T : never; diff --git a/packages/core/src/render3/styling/shared.ts b/packages/core/src/render3/styling/shared.ts new file mode 100644 index 0000000000..1e57632841 --- /dev/null +++ b/packages/core/src/render3/styling/shared.ts @@ -0,0 +1,17 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +/** + * The default directive styling index value for template-based bindings. + * + * All host-level bindings (e.g. `hostStyleProp` and `hostStylingMap`) are + * assigned a directive styling index value based on the current directive + * uniqueId and the directive super-class inheritance depth. But for template + * bindings they always have the same directive styling index value. + */ +export const DEFAULT_TEMPLATE_DIRECTIVE_INDEX = 0; diff --git a/packages/core/src/render3/styling/util.ts b/packages/core/src/render3/styling/util.ts index 0331d39397..50c52987eb 100644 --- a/packages/core/src/render3/styling/util.ts +++ b/packages/core/src/render3/styling/util.ts @@ -19,6 +19,7 @@ import {HEADER_OFFSET, HOST, LView, RootContext} from '../interfaces/view'; import {getTNode, isStylingContext} from '../util/view_utils'; import {CorePlayerHandler} from './core_player_handler'; +import {DEFAULT_TEMPLATE_DIRECTIVE_INDEX} from './shared'; export const ANIMATION_PROP_PREFIX = '@'; @@ -35,12 +36,13 @@ export function createEmptyStylingContext( [0, 0], // SinglePropOffsets [0], // CachedMultiClassValue [0], // CachedMultiStyleValue + null, // HostBuffer null, // PlayerContext ]; // whenever a context is created there is always a `null` directive // that is registered (which is a placeholder for the "template"). - allocateDirectiveIntoContext(context, null); + allocateOrUpdateDirectiveIntoContext(context, DEFAULT_TEMPLATE_DIRECTIVE_INDEX); return context; } @@ -60,25 +62,28 @@ export function createEmptyStylingContext( * @param directiveRef the directive that will be allocated into the context * @returns the index where the directive was inserted into */ -export function allocateDirectiveIntoContext( - context: StylingContext, directiveRef: any | null): number { - // this is a new directive which we have not seen yet. - const dirs = context[StylingIndex.DirectiveRegistryPosition]; - const i = dirs.length; +export function allocateOrUpdateDirectiveIntoContext( + context: StylingContext, directiveIndex: number, singlePropValuesIndex: number = -1, + styleSanitizer?: StyleSanitizeFn | null | undefined): void { + const directiveRegistry = context[StylingIndex.DirectiveRegistryPosition]; + const index = directiveIndex * DirectiveRegistryValuesIndex.Size; // we preemptively make space into the directives array and then // assign values slot-by-slot to ensure that if the directive ordering // changes then it will still function - dirs.push(null, null, null, null); - dirs[i + DirectiveRegistryValuesIndex.DirectiveValueOffset] = directiveRef; - dirs[i + DirectiveRegistryValuesIndex.DirtyFlagOffset] = false; - dirs[i + DirectiveRegistryValuesIndex.StyleSanitizerOffset] = null; + const limit = index + DirectiveRegistryValuesIndex.Size; + for (let i = directiveRegistry.length; i < limit; i += DirectiveRegistryValuesIndex.Size) { + // -1 is used to signal that the directive has been allocated, but + // no actual style or class bindings have been registered yet... + directiveRegistry.push(-1, null); + } - // -1 is used to signal that the directive has been allocated, but - // no actual style or class bindings have been registered yet... - dirs[i + DirectiveRegistryValuesIndex.SinglePropValuesIndexOffset] = -1; - - return i; + const propValuesStartPosition = index + DirectiveRegistryValuesIndex.SinglePropValuesIndexOffset; + if (singlePropValuesIndex >= 0 && directiveRegistry[propValuesStartPosition] === -1) { + directiveRegistry[propValuesStartPosition] = singlePropValuesIndex; + directiveRegistry[index + DirectiveRegistryValuesIndex.StyleSanitizerOffset] = + styleSanitizer || null; + } } /** diff --git a/packages/core/test/acceptance/styling_spec.ts b/packages/core/test/acceptance/styling_spec.ts index 692164dc5b..1644933aea 100644 --- a/packages/core/test/acceptance/styling_spec.ts +++ b/packages/core/test/acceptance/styling_spec.ts @@ -11,12 +11,12 @@ import {expect} from '@angular/platform-browser/testing/src/matchers'; import {ivyEnabled, onlyInIvy} from '@angular/private/testing'; describe('acceptance integration tests', () => { - onlyInIvy('[style] and [class] bindings are a new feature') + onlyInIvy('map-based [style] and [class] bindings are not supported in VE') .it('should render host bindings on the root component', () => { @Component({template: '...'}) class MyApp { - @HostBinding('style') public myStylesExp = {}; - @HostBinding('class') public myClassesExp = {}; + @HostBinding('style') myStylesExp = {}; + @HostBinding('class') myClassesExp = {}; } TestBed.configureTestingModule({declarations: [MyApp]}); @@ -153,4 +153,166 @@ describe('acceptance integration tests', () => { expect(element.style.width).toEqual('300px'); expect(element.classList.contains('abc')).toBeFalsy(); }); + + it('should render styling for parent and sub-classed components in order', () => { + @Component({ + template: ` + + ` + }) + class MyApp { + } + + @Component({template: '...'}) + class ParentCmp { + @HostBinding('style.width') width1 = '100px'; + @HostBinding('style.height') height1 = '100px'; + @HostBinding('style.opacity') opacity1 = '0.5'; + } + + @Component({selector: 'child-and-parent-cmp', template: '...'}) + class ChildCmp extends ParentCmp { + @HostBinding('style.width') width2 = '200px'; + @HostBinding('style.height') height2 = '200px'; + } + + TestBed.configureTestingModule({declarations: [MyApp, ParentCmp, ChildCmp]}); + const fixture = TestBed.createComponent(MyApp); + const element = fixture.nativeElement; + fixture.detectChanges(); + + const childElement = element.querySelector('child-and-parent-cmp'); + expect(childElement.style.width).toEqual('200px'); + expect(childElement.style.height).toEqual('200px'); + expect(childElement.style.opacity).toEqual('0.5'); + }); + + onlyInIvy('[style.prop] and [class.name] prioritization is a new feature') + .it('should prioritize styling present in the order of directive hostBinding evaluation, but consider sub-classed directive styling to be the most important', + () => { + const log: string[] = []; + + @Component({template: '
'}) + class MyApp { + } + + @Directive({selector: '[parent-dir]'}) + class ParentDir { + @HostBinding('style.width') + get width1() { return '100px'; } + + @HostBinding('style.height') + get height1() { return '100px'; } + + @HostBinding('style.color') + get color1() { return 'red'; } + } + + @Directive({selector: '[child-dir]'}) + class ChildDir extends ParentDir { + @HostBinding('style.width') + get width2() { return '200px'; } + + @HostBinding('style.height') + get height2() { return '200px'; } + } + + @Directive({selector: '[sibling-dir]'}) + class SiblingDir { + @HostBinding('style.width') + get width3() { return '300px'; } + + @HostBinding('style.height') + get height3() { return '300px'; } + + @HostBinding('style.opacity') + get opacity3() { return '0.5'; } + + @HostBinding('style.color') + get color1() { return 'blue'; } + } + + TestBed.configureTestingModule( + {declarations: [MyApp, ParentDir, ChildDir, SiblingDir]}); + const fixture = TestBed.createComponent(MyApp); + const element = fixture.nativeElement; + fixture.detectChanges(); + + const childElement = element.querySelector('div'); + + // width/height values were set in all directives, but the sub-class directive + // (ChildDir) + // had priority over the parent directive (ParentDir) which is why its value won. It + // also + // won over Dir because the SiblingDir directive was evaluated later on. + expect(childElement.style.width).toEqual('200px'); + expect(childElement.style.height).toEqual('200px'); + + // ParentDir styled the color first before Dir + expect(childElement.style.color).toEqual('red'); + + // Dir was the only directive to style opacity + expect(childElement.style.opacity).toEqual('0.5'); + }); + + it('should ensure that static classes are assigned to ng-container elements and picked up for content projection', + () => { + @Component({ + template: ` + + outer + + inner + + + ` + }) + class MyApp { + } + + @Component({ + selector: 'project', + template: ` +
+ +
+
+ +
+ ` + }) + class ProjectCmp { + } + + TestBed.configureTestingModule({declarations: [MyApp, ProjectCmp]}); + const fixture = TestBed.createComponent(MyApp); + const element = fixture.nativeElement; + fixture.detectChanges(); + + const inner = element.querySelector('.inner-area'); + expect(inner.textContent.trim()).toEqual('inner'); + const outer = element.querySelector('.outer-area'); + expect(outer.textContent.trim()).toEqual('outer'); + }); + + it('should allow class-bindings to be placed on ng-container elements', () => { + @Component({ + template: ` + ... + ` + }) + class MyApp { + } + + @Directive({selector: '[dir-that-adds-other-classes]'}) + class DirThatAddsOtherClasses { + @HostBinding('class.other-class') bool = true; + } + + TestBed.configureTestingModule({declarations: [MyApp, DirThatAddsOtherClasses]}); + expect(() => { + const fixture = TestBed.createComponent(MyApp); + fixture.detectChanges(); + }).not.toThrow(); + }); }); 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 6806041525..54e8e2c836 100644 --- a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json +++ b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json @@ -29,6 +29,9 @@ { "name": "DECLARATION_VIEW" }, + { + "name": "DEFAULT_TEMPLATE_DIRECTIVE_INDEX" + }, { "name": "DepComponent" }, @@ -158,6 +161,9 @@ { "name": "_renderCompCount" }, + { + "name": "_selectedIndex" + }, { "name": "addComponentLogic" }, @@ -171,7 +177,7 @@ "name": "allocStylingContext" }, { - "name": "allocateDirectiveIntoContext" + "name": "allocateOrUpdateDirectiveIntoContext" }, { "name": "allowValueChange" @@ -338,9 +344,6 @@ { "name": "getDirectiveDef" }, - { - "name": "getDirectiveRegistryValuesIndexOf" - }, { "name": "getElementDepthCount" }, @@ -449,6 +452,9 @@ { "name": "increaseElementDepthCount" }, + { + "name": "incrementActiveDirectiveId" + }, { "name": "initNodeFlags" }, @@ -627,7 +633,7 @@ "name": "saveResolvedLocalsInData" }, { - "name": "setActiveHost" + "name": "setActiveHostElement" }, { "name": "setBindingRoot" @@ -668,6 +674,9 @@ { "name": "setPreviousOrParentTNode" }, + { + "name": "setSelectedIndex" + }, { "name": "setStyle" }, 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 12b3e484af..35e5ec607a 100644 --- a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json @@ -134,6 +134,9 @@ { "name": "_renderCompCount" }, + { + "name": "_selectedIndex" + }, { "name": "addToViewTree" }, @@ -323,6 +326,9 @@ { "name": "includeViewProviders" }, + { + "name": "incrementActiveDirectiveId" + }, { "name": "initNodeFlags" }, @@ -435,7 +441,7 @@ "name": "resetPreOrderHookFlags" }, { - "name": "setActiveHost" + "name": "setActiveHostElement" }, { "name": "setBindingRoot" @@ -464,6 +470,9 @@ { "name": "setPreviousOrParentTNode" }, + { + "name": "setSelectedIndex" + }, { "name": "setStyle" }, diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 839509d7b0..aada45b7fc 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -44,6 +44,9 @@ { "name": "DECLARATION_VIEW" }, + { + "name": "DEFAULT_TEMPLATE_DIRECTIVE_INDEX" + }, { "name": "DefaultIterableDiffer" }, @@ -371,6 +374,9 @@ { "name": "_renderCompCount" }, + { + "name": "_selectedIndex" + }, { "name": "_symbolIterator" }, @@ -399,7 +405,10 @@ "name": "allocStylingContext" }, { - "name": "allocateDirectiveIntoContext" + "name": "allocateOrUpdateDirectiveIntoContext" + }, + { + "name": "allowFlush" }, { "name": "allowValueChange" @@ -572,9 +581,6 @@ { "name": "elementClassProp" }, - { - "name": "elementClassPropInternal" - }, { "name": "elementCreate" }, @@ -644,6 +650,9 @@ { "name": "findViaComponent" }, + { + "name": "flushQueue" + }, { "name": "forwardRef" }, @@ -698,12 +707,6 @@ { "name": "getDirectiveIndexFromEntry" }, - { - "name": "getDirectiveIndexFromRegistry" - }, - { - "name": "getDirectiveRegistryValuesIndexOf" - }, { "name": "getElementDepthCount" }, @@ -755,9 +758,6 @@ { "name": "getMatchingBindingIndex" }, - { - "name": "getMultiClassesStartIndex" - }, { "name": "getMultiOrSingleIndex" }, @@ -908,6 +908,9 @@ { "name": "increaseElementDepthCount" }, + { + "name": "incrementActiveDirectiveId" + }, { "name": "initElementStyling" }, @@ -983,9 +986,6 @@ { "name": "isDifferent" }, - { - "name": "isDirectiveDirty" - }, { "name": "isDirty" }, @@ -1224,7 +1224,7 @@ "name": "select" }, { - "name": "setActiveHost" + "name": "setActiveHostElement" }, { "name": "setBindingRoot" @@ -1247,9 +1247,6 @@ { "name": "setCurrentQueryIndex" }, - { - "name": "setDirectiveDirty" - }, { "name": "setDirty" }, @@ -1292,6 +1289,9 @@ { "name": "setSanitizeFlag" }, + { + "name": "setSelectedIndex" + }, { "name": "setStyle" }, 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 045cdb3071..68e8688656 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 @@ -19,6 +19,7 @@ import {CONTEXT, LView, LViewFlags, RootContext} from '../../../src/render3/inte import {addPlayer, getPlayers} from '../../../src/render3/players'; import {ClassAndStylePlayerBuilder, compareLogSummaries, directiveOwnerPointers, generateConfigSummary, getDirectiveIndexFromEntry, initializeStaticContext, isContextDirty, patchContextWithStaticAttrs, renderStyling as _renderStyling, setContextDirty, updateClassProp, updateContextWithBindings, updateStyleProp, updateStylingMap} from '../../../src/render3/styling/class_and_style_bindings'; import {CorePlayerHandler} from '../../../src/render3/styling/core_player_handler'; +import {registerHostDirective} from '../../../src/render3/styling/host_instructions_queue'; import {BoundPlayerFactory, bindPlayerFactory} from '../../../src/render3/styling/player_factory'; import {allocStylingContext, createEmptyStylingContext} from '../../../src/render3/styling/util'; import {defaultStyleSanitizer} from '../../../src/sanitization/sanitization'; @@ -55,7 +56,7 @@ describe('style and class based bindings', () => { } const tpl = initializeStaticContext(attrsWithStyling, 0) !; - updateContextWithBindings(tpl, null, classBindings || null, styleBindings || null, sanitizer); + updateContextWithBindings(tpl, 0, classBindings || null, styleBindings || null, sanitizer); return tpl; } @@ -70,7 +71,7 @@ describe('style and class based bindings', () => { function patchContext( context: StylingContext, styles?: string[] | null, classes?: string[] | null, - directiveRef?: any) { + directiveIndex: number = 0) { const attrs: (string | AttributeMarker)[] = []; if (classes && classes.length) { attrs.push(AttributeMarker.Classes); @@ -80,7 +81,7 @@ describe('style and class based bindings', () => { attrs.push(AttributeMarker.Styles); attrs.push(...styles); } - patchContextWithStaticAttrs(context, attrs, 0, directiveRef || null); + patchContextWithStaticAttrs(context, attrs, 0, directiveIndex); } function getRootContextInternal(lView: LView) { return lView[CONTEXT] as RootContext; } @@ -211,13 +212,14 @@ describe('style and class based bindings', () => { const template = createStylingContext(); assertContext(template, [ element, - masterConfig(9), - [null, 2, false, null], + masterConfig(10), + [2, null], [null, null], [null, null], [0, 0, 0, 0], - [0, 0, 9, null, 0], - [0, 0, 9, null, 0], + [0, 0, 10, null, 0], + [0, 0, 10, null, 0], + null, null, ]); }); @@ -227,13 +229,14 @@ describe('style and class based bindings', () => { createStylingContext(['color', 'red', 'width', '10px'], null, ['foo', 'bar']); assertContext(template, [ element, - masterConfig(9), - [null, 2, false, null], + masterConfig(10), + [2, null], [null, null, 'color', 'red', 0, 'width', '10px', 0], [null, null, 'foo', true, 0, 'bar', true, 0], [0, 0, 0, 0], - [0, 0, 9, null, 0], - [0, 0, 9, null, 0], + [0, 0, 10, null, 0], + [0, 0, 10, null, 0], + null, null, ]); }); @@ -256,7 +259,7 @@ describe('style and class based bindings', () => { 0, ]); - patchContext(template, ['color', 'black', 'height', '200px'], ['bar', 'foo'], '1'); + patchContext(template, ['color', 'black', 'height', '200px'], ['bar', 'foo'], 1); expect(template[StylingIndex.InitialStyleValuesPosition]).toEqual([ null, null, @@ -330,6 +333,7 @@ describe('style and class based bindings', () => { '200px', 0, ]); + expect(template[StylingIndex.InitialClassValuesPosition]).toEqual([ null, null, @@ -341,10 +345,10 @@ describe('style and class based bindings', () => { 0, ]); - allocStylingContext(element, template); + const context = allocStylingContext(element, template); - patchContext(template, ['color', 'black', 'height', '200px'], ['bar', 'foo'], '1'); - expect(template[StylingIndex.InitialStyleValuesPosition]).toEqual([ + patchContext(context, ['color', 'orange', 'height', '300px'], ['bar', 'foo', 'car']); + expect(context[StylingIndex.InitialStyleValuesPosition]).toEqual([ null, null, 'color', @@ -354,7 +358,7 @@ describe('style and class based bindings', () => { '200px', 0, ]); - expect(template[StylingIndex.InitialClassValuesPosition]).toEqual([ + expect(context[StylingIndex.InitialClassValuesPosition]).toEqual([ null, null, 'foo', @@ -487,191 +491,190 @@ describe('style and class based bindings', () => { it('should initialize a context with a series of styling bindings as well as single property offsets', () => { const ctx = createEmptyStylingContext(); - updateContextWithBindings(ctx, null, ['foo'], ['width']); + updateContextWithBindings(ctx, 0, ['foo'], ['width']); assertContext(ctx, [ null, - masterConfig(17, false, false), // - [null, 2, false, null], + masterConfig(18, false, false), // + [2, null], [null, null, 'width', null, 0], [null, null, 'foo', false, 0], - [1, 1, 1, 1, 9, 13], - [1, 0, 21, null, 1], - [1, 0, 17, null, 1], + [1, 1, 1, 1, 10, 14], + [1, 0, 22, null, 1], + [1, 0, 18, null, 1], + null, null, - // #9 - cleanStyle(3, 17), + // #10 + cleanStyle(3, 18), 'width', null, 0, - // #13 - cleanClass(3, 21), + // #14 + cleanClass(3, 22), 'foo', null, 0, - // #17 - cleanStyle(3, 9), + // #18 + cleanStyle(3, 10), 'width', null, 0, - // #21 - cleanClass(3, 13), + // #22 + cleanClass(3, 14), 'foo', null, 0, ]); - updateContextWithBindings(ctx, 'SOME DIRECTIVE', ['bar'], ['width', 'height']); + updateContextWithBindings(ctx, 1, ['bar'], ['width', 'height']); assertContext(ctx, [ null, - masterConfig(25, false, false), // - [null, 2, false, null, 'SOME DIRECTIVE', 6, false, null], + masterConfig(26, false, false), // + [2, null, 6, null], [null, null, 'width', null, 0, 'height', null, 1], [null, null, 'foo', false, 0, 'bar', false, 1], - [2, 2, 1, 1, 9, 17, 2, 1, 9, 13, 21], - [2, 0, 33, null, 1, 0, 37, null, 1], - [2, 0, 25, null, 1, 0, 29, null, 1], + [2, 2, 1, 1, 10, 18, 2, 1, 10, 14, 22], + [2, 0, 34, null, 1, 0, 38, null, 1], + [2, 0, 26, null, 1, 0, 30, null, 1], + null, null, - // #9 - cleanStyle(3, 25), + // #10 + cleanStyle(3, 26), 'width', null, 0, - // #13 - cleanStyle(6, 29), + // #14 + cleanStyle(6, 30), 'height', null, 1, - // #17 - cleanClass(3, 33), + // #18 + cleanClass(3, 34), 'foo', null, 0, - // #21 - cleanClass(6, 37), + // #22 + cleanClass(6, 38), 'bar', null, 1, - // #25 - cleanStyle(3, 9), + // #26 + cleanStyle(3, 10), 'width', null, 0, - // #29 - cleanStyle(6, 13), + // #30 + cleanStyle(6, 14), 'height', null, 1, - // #33 - cleanClass(3, 17), + // #34 + cleanClass(3, 18), 'foo', null, 0, - // #37 - cleanClass(6, 21), + // #38 + cleanClass(6, 22), 'bar', null, 1, ]); - updateContextWithBindings( - ctx, 'SOME DIRECTIVE 2', ['baz', 'bar', 'foo'], ['opacity', 'width', 'height']); + updateContextWithBindings(ctx, 2, ['baz', 'bar', 'foo'], ['opacity', 'width', 'height']); assertContext(ctx, [ null, - masterConfig(33, false, false), // - [ - null, 2, false, null, 'SOME DIRECTIVE', 6, false, null, 'SOME DIRECTIVE 2', 11, - false, null - ], + masterConfig(34, false, false), // + [2, null, 6, null, 11, null], [null, null, 'width', null, 0, 'height', null, 1, 'opacity', null, 2], [null, null, 'foo', false, 0, 'bar', false, 1, 'baz', false, 2], - [3, 3, 1, 1, 9, 21, 2, 1, 9, 13, 25, 3, 3, 17, 9, 13, 29, 25, 21], - [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], + [3, 3, 1, 1, 10, 22, 2, 1, 10, 14, 26, 3, 3, 18, 10, 14, 30, 26, 22], + [3, 0, 46, null, 1, 0, 50, null, 1, 0, 54, null, 1], + [3, 0, 34, null, 1, 0, 38, null, 1, 0, 42, null, 1], + null, null, - // #9 - cleanStyle(3, 33), + // #10 + cleanStyle(3, 34), 'width', null, 0, - // #13 - cleanStyle(6, 37), + // #14 + cleanStyle(6, 38), 'height', null, 1, - // #17 - cleanStyle(9, 41), + // #18 + cleanStyle(9, 42), 'opacity', null, 2, - // #21 - cleanClass(3, 45), + // #22 + cleanClass(3, 46), 'foo', null, 0, - // #25 - cleanClass(6, 49), + // #26 + cleanClass(6, 50), 'bar', null, 1, - // #29 - cleanClass(9, 53), + // #30 + cleanClass(9, 54), 'baz', null, 2, - // #33 - cleanStyle(3, 9), + // #34 + cleanStyle(3, 10), 'width', null, 0, - // #37 - cleanStyle(6, 13), + // #38 + cleanStyle(6, 14), 'height', null, 1, - // #41 - cleanStyle(9, 17), + // #42 + cleanStyle(9, 18), 'opacity', null, 2, - // #45 - cleanClass(3, 21), + // #46 + cleanClass(3, 22), 'foo', null, 0, - // #49 - cleanClass(6, 25), + // #50 + cleanClass(6, 26), 'bar', null, 1, - // #53 - cleanClass(9, 29), + // #54 + cleanClass(9, 30), 'baz', null, 2, @@ -680,17 +683,17 @@ describe('style and class based bindings', () => { it('should only populate bindings for a given directive once', () => { const ctx = createEmptyStylingContext(); - updateContextWithBindings(ctx, null, ['foo'], ['width']); - expect(ctx.length).toEqual(25); + updateContextWithBindings(ctx, 0, ['foo'], ['width']); + expect(ctx.length).toEqual(26); - updateContextWithBindings(ctx, null, ['bar'], ['height']); - expect(ctx.length).toEqual(25); + updateContextWithBindings(ctx, 0, ['bar'], ['height']); + expect(ctx.length).toEqual(26); - updateContextWithBindings(ctx, '1', ['bar'], ['height']); - expect(ctx.length).toEqual(41); + updateContextWithBindings(ctx, 1, ['bar'], ['height']); + expect(ctx.length).toEqual(42); - updateContextWithBindings(ctx, '1', ['bar'], ['height']); - expect(ctx.length).toEqual(41); + updateContextWithBindings(ctx, 1, ['bar'], ['height']); + expect(ctx.length).toEqual(42); }); it('should build a list of multiple styling values', () => { @@ -822,26 +825,26 @@ describe('style and class based bindings', () => { updateStyles(stylingContext, {width: '100px', height: '100px'}); assertContextOnlyValues(stylingContext, [ - // #9 - cleanStyle(3, 17), + // #10 + cleanStyle(3, 18), 'width', null, 0, - // #13 - cleanStyle(6, 21), + // #14 + cleanStyle(6, 22), 'height', null, 0, - // #17 - dirtyStyle(3, 9), + // #18 + dirtyStyle(3, 10), 'width', '100px', 0, - // #21 - dirtyStyle(6, 13), + // #22 + dirtyStyle(6, 14), 'height', '100px', 0, @@ -851,32 +854,32 @@ describe('style and class based bindings', () => { updateStyles(stylingContext, {width: '200px', opacity: '0'}); assertContextOnlyValues(stylingContext, [ - // #9 - cleanStyle(3, 17), + // #10 + cleanStyle(3, 18), 'width', null, 0, - // #13 - cleanStyle(6, 25), + // #14 + cleanStyle(6, 26), 'height', null, 0, - // #17 - dirtyStyle(3, 9), + // #18 + dirtyStyle(3, 10), 'width', '200px', 0, - // #21 + // #22 dirtyStyle(), 'opacity', '0', 0, - // #25 - dirtyStyle(6, 13), + // #26 + dirtyStyle(6, 14), 'height', null, 0, @@ -884,32 +887,32 @@ describe('style and class based bindings', () => { getStyles(stylingContext); assertContextOnlyValues(stylingContext, [ - // #9 - cleanStyle(3, 17), + // #10 + cleanStyle(3, 18), 'width', null, 0, - // #13 - cleanStyle(6, 25), + // #14 + cleanStyle(6, 26), 'height', null, 0, - // #17 - cleanStyle(3, 9), + // #18 + cleanStyle(3, 10), 'width', '200px', 0, - // #21 + // #22 cleanStyle(), 'opacity', '0', 0, // #23 - cleanStyle(6, 13), + cleanStyle(6, 14), 'height', null, 0, @@ -919,32 +922,32 @@ describe('style and class based bindings', () => { updateStyleProp(stylingContext, 0, '300px'); assertContextOnlyValues(stylingContext, [ - // #9 - dirtyStyle(3, 17), + // #10 + dirtyStyle(3, 18), 'width', '300px', 0, - // #13 - cleanStyle(6, 25), + // #14 + cleanStyle(6, 26), 'height', null, 0, - // #17 - cleanStyle(3, 9), + // #18 + cleanStyle(3, 10), 'width', null, 0, - // #21 + // #22 dirtyStyle(), 'opacity', null, 0, // #23 - cleanStyle(6, 13), + cleanStyle(6, 14), 'height', null, 0, @@ -954,32 +957,32 @@ describe('style and class based bindings', () => { updateStyleProp(stylingContext, 0, null); assertContextOnlyValues(stylingContext, [ - // #9 - dirtyStyle(3, 17), + // #10 + dirtyStyle(3, 18), 'width', null, 0, - // #13 - cleanStyle(6, 25), + // #14 + cleanStyle(6, 26), 'height', null, 0, - // #17 - cleanStyle(3, 9), + // #18 + cleanStyle(3, 10), 'width', null, 0, - // #21 + // #22 cleanStyle(), 'opacity', null, 0, // #23 - cleanStyle(6, 13), + cleanStyle(6, 14), 'height', null, 0, @@ -994,32 +997,32 @@ describe('style and class based bindings', () => { updateStyles(stylingContext, {width: '100px', height: '100px', opacity: '0.5'}); assertContextOnlyValues(stylingContext, [ - // #9 - cleanStyle(3, 25), + // #10 + cleanStyle(3, 26), 'line-height', null, 0, - // #13 + // #14 dirtyStyle(), 'width', '100px', 0, - // #17 + // #18 dirtyStyle(), 'height', '100px', 0, - // #21 + // #22 dirtyStyle(), 'opacity', '0.5', 0, // #23 - cleanStyle(3, 9), + cleanStyle(3, 10), 'line-height', null, 0, @@ -1029,32 +1032,32 @@ describe('style and class based bindings', () => { updateStyles(stylingContext, {}); assertContextOnlyValues(stylingContext, [ - // #9 - cleanStyle(3, 25), + // #10 + cleanStyle(3, 26), 'line-height', null, 0, - // #13 + // #14 dirtyStyle(), 'width', null, 0, - // #17 + // #18 dirtyStyle(), 'height', null, 0, - // #21 + // #22 dirtyStyle(), 'opacity', null, 0, // #23 - cleanStyle(3, 9), + cleanStyle(3, 10), 'line-height', null, 0, @@ -1064,25 +1067,25 @@ describe('style and class based bindings', () => { updateStyles(stylingContext, {borderWidth: '5px'}); assertContextOnlyValues(stylingContext, [ - // #9 - cleanStyle(3, 29), + // #10 + cleanStyle(3, 30), 'line-height', null, 0, - // #13 + // #14 dirtyStyle(), 'border-width', '5px', 0, - // #17 + // #18 cleanStyle(), 'width', null, 0, - // #21 + // #22 cleanStyle(), 'height', null, @@ -1094,8 +1097,8 @@ describe('style and class based bindings', () => { null, 0, - // #29 - cleanStyle(3, 9), + // #30 + cleanStyle(3, 10), 'line-height', null, 0, @@ -1104,25 +1107,25 @@ describe('style and class based bindings', () => { updateStyleProp(stylingContext, 0, '200px'); assertContextOnlyValues(stylingContext, [ - // #9 - dirtyStyle(3, 29), + // #10 + dirtyStyle(3, 30), 'line-height', '200px', 0, - // #13 + // #14 dirtyStyle(), 'border-width', '5px', 0, - // #17 + // #18 cleanStyle(), 'width', null, 0, - // #21 + // #22 cleanStyle(), 'height', null, @@ -1134,8 +1137,8 @@ describe('style and class based bindings', () => { null, 0, - // #29 - cleanStyle(3, 9), + // #30 + cleanStyle(3, 10), 'line-height', null, 0, @@ -1144,25 +1147,25 @@ describe('style and class based bindings', () => { updateStyles(stylingContext, {borderWidth: '15px', borderColor: 'red'}); assertContextOnlyValues(stylingContext, [ - // #9 - dirtyStyle(3, 33), + // #10 + dirtyStyle(3, 34), 'line-height', '200px', 0, - // #13 + // #14 dirtyStyle(), 'border-width', '15px', 0, - // #17 + // #18 dirtyStyle(), 'border-color', 'red', 0, - // #21 + // #22 cleanStyle(), 'width', null, @@ -1174,14 +1177,14 @@ describe('style and class based bindings', () => { null, 0, - // #29 + // #30 cleanStyle(), 'opacity', null, 0, - // #33 - cleanStyle(3, 9), + // #34 + cleanStyle(3, 10), 'line-height', null, 0, @@ -1199,29 +1202,30 @@ describe('style and class based bindings', () => { assertContext(stylingContext, [ element, - masterConfig(13, true), // - [null, 2, true, null], + masterConfig(14, true), // + [2, null], [null, null, 'height', null, 0], [null, null], - [1, 0, 1, 0, 9], - [0, 0, 21, null, 0], - [1, 0, 13, cachedStyleValue, 1], + [1, 0, 1, 0, 10], + [0, 0, 22, null, 0], + [1, 0, 14, cachedStyleValue, 1], + null, null, - // #9 - dirtyStyle(3, 17), + // #10 + dirtyStyle(3, 18), 'height', '200px', 0, - // #13 + // #14 dirtyStyle(), 'width', '100px', 0, - // #17 - cleanStyle(3, 9), + // #18 + cleanStyle(3, 10), 'height', null, 0, @@ -1231,29 +1235,30 @@ describe('style and class based bindings', () => { assertContext(stylingContext, [ element, - masterConfig(13, false), // - [null, 2, false, null], + masterConfig(14, false), // + [2, null], [null, null, 'height', null, 0], [null, null], - [1, 0, 1, 0, 9], - [0, 0, 21, null, 0], - [1, 0, 13, cachedStyleValue, 1], + [1, 0, 1, 0, 10], + [0, 0, 22, null, 0], + [1, 0, 14, cachedStyleValue, 1], + null, null, - // #9 - cleanStyle(3, 17), + // #10 + cleanStyle(3, 18), 'height', '200px', 0, - // #13 + // #14 cleanStyle(), 'width', '100px', 0, - // #17 - cleanStyle(3, 9), + // #18 + cleanStyle(3, 10), 'height', null, 0, @@ -1272,26 +1277,26 @@ describe('style and class based bindings', () => { updateStyleProp(stylingContext, 1, '100px'); assertContextOnlyValues(stylingContext, [ - // #9 - dirtyStyleWithSanitization(3, 17), + // #10 + dirtyStyleWithSanitization(3, 18), 'border-image', 'url(foo.jpg)', 0, - // #13 - dirtyStyle(6, 21), + // #14 + dirtyStyle(6, 22), 'border-width', '100px', 0, - // #17 - cleanStyleWithSanitization(3, 9), + // #18 + cleanStyleWithSanitization(3, 10), 'border-image', null, 0, - // #21 - cleanStyle(6, 13), + // #22 + cleanStyle(6, 14), 'border-width', null, 0, @@ -1300,32 +1305,32 @@ describe('style and class based bindings', () => { updateStyles(stylingContext, {'background-image': 'unsafe'}); assertContextOnlyValues(stylingContext, [ - // #9 - dirtyStyleWithSanitization(3, 21), + // #10 + dirtyStyleWithSanitization(3, 22), 'border-image', 'url(foo.jpg)', 0, - // #13 - dirtyStyle(6, 25), + // #14 + dirtyStyle(6, 26), 'border-width', '100px', 0, - // #17 + // #18 dirtyStyleWithSanitization(0, 0), 'background-image', 'unsafe', 0, - // #21 - cleanStyleWithSanitization(3, 9), + // #22 + cleanStyleWithSanitization(3, 10), 'border-image', null, 0, // #23 - cleanStyle(6, 13), + cleanStyle(6, 14), 'border-width', null, 0, @@ -1334,32 +1339,32 @@ describe('style and class based bindings', () => { getStyles(stylingContext); assertContextOnlyValues(stylingContext, [ - // #9 - cleanStyleWithSanitization(3, 21), + // #10 + cleanStyleWithSanitization(3, 22), 'border-image', 'url(foo.jpg)', 0, - // #13 - cleanStyle(6, 25), + // #14 + cleanStyle(6, 26), 'border-width', '100px', 0, - // #17 + // #18 cleanStyleWithSanitization(0, 0), 'background-image', 'unsafe', 0, - // #21 - cleanStyleWithSanitization(3, 9), + // #22 + cleanStyleWithSanitization(3, 10), 'border-image', null, 0, // #23 - cleanStyle(6, 13), + cleanStyle(6, 14), 'border-width', null, 0, @@ -1370,9 +1375,9 @@ describe('style and class based bindings', () => { () => { const template = createEmptyStylingContext(); - const dir1 = {}; - const dir2 = {}; - const dir3 = {}; + const dir1 = 1; + const dir2 = 2; + const dir3 = 3; updateContextWithBindings(template, dir1, null, ['width', 'height']); updateContextWithBindings(template, dir2, null, ['width', 'color']); @@ -1410,9 +1415,9 @@ describe('style and class based bindings', () => { () => { const template = createEmptyStylingContext(); - const dir1 = {}; - const dir2 = {}; - const dir3 = {}; + const dir1 = 1; + const dir2 = 2; + const dir3 = 3; updateContextWithBindings(template, dir1, null, ['width', 'height']); updateContextWithBindings(template, dir2, null, ['width', 'color']); @@ -1454,11 +1459,11 @@ describe('style and class based bindings', () => { it('should only update missing multi styling values for successive directives if null in a former directive', () => { const template = createEmptyStylingContext(); - updateContextWithBindings(template, null); + updateContextWithBindings(template, 0); - const dir1 = {}; - const dir2 = {}; - const dir3 = {}; + const dir1 = 1; + const dir2 = 2; + const dir3 = 3; updateContextWithBindings(template, dir1, null, ['width', 'height']); updateContextWithBindings(template, dir2); updateContextWithBindings(template, dir3); @@ -1470,35 +1475,51 @@ describe('style and class based bindings', () => { 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 + 3, + 0, + 18, + null, + 0, + 0, + 18, + s1, + 2, + 0, + 26, + s2, + 1, + 0, + 30, + s3, + 0, ]); assertContextOnlyValues(ctx, [ - // #9 - cleanStyle(3, 17), + // #10 + cleanStyle(3, 18), 'width', null, 1, - // #13 - cleanStyle(6, 21), + // #14 + cleanStyle(6, 22), 'height', null, 1, - // #17 - dirtyStyle(3, 9), + // #18 + dirtyStyle(3, 10), 'width', '100px', 1, - // #21 - dirtyStyle(6, 13), + // #22 + dirtyStyle(6, 14), 'height', '99px', 1, - // #25 + // #26 dirtyStyle(0, 0), 'opacity', '0.5', @@ -1510,32 +1531,32 @@ describe('style and class based bindings', () => { updateStylingMap(ctx, null, {width: '300px', height: '999px'}, dir3); assertContextOnlyValues(ctx, [ - // #9 - cleanStyle(3, 21), + // #10 + cleanStyle(3, 22), 'width', null, 1, - // #13 - cleanStyle(6, 25), + // #14 + cleanStyle(6, 26), 'height', null, 1, - // #17 + // #18 dirtyStyle(0, 0), 'opacity', '0', 1, - // #21 - dirtyStyle(3, 9), + // #22 + dirtyStyle(3, 10), 'width', '200px', 2, - // #25 - dirtyStyle(6, 13), + // #26 + dirtyStyle(6, 14), 'height', '999px', 3, @@ -1546,37 +1567,37 @@ describe('style and class based bindings', () => { updateStylingMap(ctx, null, {width: '300px', height: '999px', color: 'red'}, dir3); assertContextOnlyValues(ctx, [ - // #9 - cleanStyle(3, 17), + // #10 + cleanStyle(3, 18), 'width', null, 1, - // #13 - cleanStyle(6, 25), + // #14 + cleanStyle(6, 26), 'height', null, 1, - // #17 - dirtyStyle(3, 9), + // #18 + dirtyStyle(3, 10), 'width', '500px', 2, - // #21 + // #22 dirtyStyle(0, 0), 'opacity', '0.2', 2, - // #25 - dirtyStyle(6, 13), + // #26 + dirtyStyle(6, 14), 'height', '999px', 3, - // #29 + // #30 dirtyStyle(0, 0), 'color', 'red', @@ -1587,11 +1608,11 @@ describe('style and class based bindings', () => { it('should only update missing multi class values for successive directives if null in a former directive', () => { const template = createEmptyStylingContext(); - updateContextWithBindings(template, null); + updateContextWithBindings(template, 0); - const dir1 = {}; - const dir2 = {}; - const dir3 = {}; + const dir1 = 1; + const dir2 = 2; + const dir3 = 3; updateContextWithBindings(template, dir1, ['red', 'green']); updateContextWithBindings(template, dir2); updateContextWithBindings(template, dir3); @@ -1603,48 +1624,48 @@ describe('style and class based bindings', () => { 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 + 5, 0, 18, null, 0, 0, 18, c1, 2, 0, 26, c2, 1, 0, 30, c3, 2 ]); assertContextOnlyValues(ctx, [ - // #9 - cleanClass(3, 17), + // #10 + cleanClass(3, 18), 'red', null, 1, - // #13 - cleanClass(6, 33), + // #14 + cleanClass(6, 34), 'green', null, 1, - // #17 - dirtyClass(3, 9), + // #18 + dirtyClass(3, 10), 'red', true, 1, - // #21 + // #22 dirtyClass(0, 0), 'orange', true, 1, - // #25 + // #26 dirtyClass(0, 0), 'black', true, 2, - // #29 + // #30 dirtyClass(0, 0), 'silver', true, 3, - // #33 - dirtyClass(6, 13), + // #34 + dirtyClass(6, 14), 'green', true, 3, @@ -1655,43 +1676,43 @@ describe('style and class based bindings', () => { updateStylingMap(ctx, c3 = 'green', null, dir3); assertContextOnlyValues(ctx, [ - // #9 - cleanClass(3, 25), + // #10 + cleanClass(3, 26), 'red', null, 1, - // #13 - cleanClass(6, 29), + // #14 + cleanClass(6, 30), 'green', null, 1, - // #17 + // #18 dirtyClass(0, 0), 'orange', true, 1, - // #21 + // #22 dirtyClass(0, 0), 'black', true, 2, - // #25 - dirtyClass(3, 9), + // #26 + dirtyClass(3, 10), 'red', true, 2, - // #29 - dirtyClass(6, 13), + // #30 + dirtyClass(6, 14), 'green', true, 3, - // #33 + // #34 dirtyClass(0, 0), 'silver', null, @@ -1703,43 +1724,43 @@ describe('style and class based bindings', () => { updateStylingMap(ctx, c3 = 'red', null, dir3); assertContextOnlyValues(ctx, [ - // #9 - cleanClass(3, 21), + // #10 + cleanClass(3, 22), 'red', null, 1, - // #13 - cleanClass(6, 17), + // #14 + cleanClass(6, 18), 'green', null, 1, - // #17 - dirtyClass(6, 13), + // #18 + dirtyClass(6, 14), 'green', true, 1, - // #21 - dirtyClass(3, 9), + // #22 + dirtyClass(3, 10), 'red', true, 3, - // #25 + // #26 dirtyClass(0, 0), 'black', null, 1, - // #29 + // #30 dirtyClass(0, 0), 'orange', null, 1, - // #33 + // #34 dirtyClass(0, 0), 'silver', null, @@ -1758,32 +1779,24 @@ describe('style and class based bindings', () => { }; const template = createEmptyStylingContext(); - const dirWithSanitizer1 = {}; + const dirWithSanitizer1 = 1; const sanitizer1 = makeSanitizer('1'); - const dirWithSanitizer2 = {}; + const dirWithSanitizer2 = 2; const sanitizer2 = makeSanitizer('2'); - const dirWithoutSanitizer = {}; + const dirWithoutSanitizer = 3; updateContextWithBindings(template, dirWithSanitizer1, null, ['color'], sanitizer1); updateContextWithBindings(template, dirWithSanitizer2, null, ['color'], sanitizer2); updateContextWithBindings(template, dirWithoutSanitizer, null, ['color']); const ctx = allocStylingContext(element, template); expect(ctx[StylingIndex.DirectiveRegistryPosition]).toEqual([ - null, // - -1, // - false, // - null, // - dirWithSanitizer1, // - 2, // - false, // - sanitizer1, // - dirWithSanitizer2, // - 5, // - false, // - sanitizer2, // - dirWithoutSanitizer, // - 8, // - false, // + -1, // + null, // + 2, // + sanitizer1, // + 5, // + sanitizer2, // + 8, // null ]); @@ -1809,91 +1822,69 @@ describe('style and class based bindings', () => { expect(getStyles(ctx, dirWithoutSanitizer)).toEqual({color: 'green'}); }); - it('should automatically register a styling context with a foreign directive if styling is applied with said directive', + it('should not allow a foreign directive index to update values in the styling context unless it has been registered', () => { const template = createEmptyStylingContext(); - const knownDir = {}; - const foreignDir = {}; + const knownDir = 1; updateContextWithBindings(template, knownDir); const ctx = allocStylingContext(element, template); expect(ctx[StylingIndex.DirectiveRegistryPosition]).toEqual([ - null, // - -1, // - false, // - null, // - knownDir, // - 2, // - false, // - null, // + -1, // + null, // + 2, // + null, // ]); expect(ctx[StylingIndex.CachedMultiClasses].length) .toEqual(template[StylingIndex.CachedMultiClasses].length); - expect(ctx[StylingIndex.CachedMultiClasses]).toEqual([0, 0, 9, null, 0, 0, 9, null, 0]); + expect(ctx[StylingIndex.CachedMultiClasses]).toEqual([ + 0, 0, 10, null, 0, 0, 10, null, 0 + ]); expect(ctx[StylingIndex.CachedMultiStyles].length) .toEqual(template[StylingIndex.CachedMultiStyles].length); - expect(ctx[StylingIndex.CachedMultiStyles]).toEqual([0, 0, 9, null, 0, 0, 9, null, 0]); + expect(ctx[StylingIndex.CachedMultiStyles]).toEqual([0, 0, 10, null, 0, 0, 10, null, 0]); - updateStylingMap(ctx, 'foo', null, foreignDir); - expect(ctx[StylingIndex.DirectiveRegistryPosition]).toEqual([ - null, // - -1, // - false, // - null, // - knownDir, // - 2, // - false, // - null, // - foreignDir, // - -1, // - true, // - null, // - ]); - - expect(ctx[StylingIndex.CachedMultiClasses].length) - .not.toEqual(template[StylingIndex.CachedMultiClasses].length); - expect(ctx[StylingIndex.CachedMultiClasses]).toEqual([ - 1, 0, 9, null, 0, 0, 9, null, 0, 0, 9, 'foo', 1 - ]); - - expect(ctx[StylingIndex.CachedMultiStyles].length) - .not.toEqual(template[StylingIndex.CachedMultiStyles].length); - expect(ctx[StylingIndex.CachedMultiStyles]).toEqual([ - 0, 0, 9, null, 0, 0, 9, null, 0, 0, 9, null, 0 - ]); + const foreignDir = 2; + expect(() => { + updateStylingMap(ctx, 'foo', null, foreignDir); + }).toThrowError('The provided directive is not registered with the styling context'); }); }); - it('should skip issuing style updates if there is nothing to update upon first render', () => { - const stylingContext = createStylingContext(null, ['color']); - const store = new MockStylingStore(element as HTMLElement, BindingType.Class); - const getStyles = trackStylesFactory(store); - const otherDirective = {}; + it('should always render styling when called regardless of the directive unless the host is set', + () => { + const stylingContext = createStylingContext(null, ['color']); + const store = new MockStylingStore(element as HTMLElement, BindingType.Class); + const getStyles = trackStylesFactory(store); + const otherDirective = 1; - let styles: any = {'font-size': ''}; - updateStyleProp(stylingContext, 0, ''); - updateStylingMap(stylingContext, null, styles); - patchContextWithStaticAttrs(stylingContext, [], 0, otherDirective); + let styles: any = {'font-size': ''}; + updateStyleProp(stylingContext, 0, ''); + updateStylingMap(stylingContext, null, styles); - getStyles(stylingContext, otherDirective); - expect(store.getValues()).toEqual({}); + getStyles(stylingContext); + expect(store.getValues()).toEqual({'font-size': '', 'color': ''}); - styles = {'font-size': '20px'}; - updateStyleProp(stylingContext, 0, 'red'); - updateStylingMap(stylingContext, null, styles); + patchContextWithStaticAttrs(stylingContext, [], 0, otherDirective); + registerHostDirective(stylingContext, otherDirective); - getStyles(stylingContext); - expect(store.getValues()).toEqual({'font-size': '20px', color: 'red'}); + updateStyleProp(stylingContext, 0, 'red'); + updateStylingMap(stylingContext, null, styles = {'font-size': '20px'}); - styles = {}; - updateStyleProp(stylingContext, 0, ''); - updateStylingMap(stylingContext, null, styles); + getStyles(stylingContext); + expect(store.getValues()).toEqual({'font-size': '', 'color': ''}); - getStyles(stylingContext); - expect(store.getValues()).toEqual({'font-size': null, color: ''}); - }); + getStyles(stylingContext, otherDirective); + expect(store.getValues()).toEqual({'font-size': '20px', color: 'red'}); + + updateStyleProp(stylingContext, 0, ''); + updateStylingMap(stylingContext, null, styles = {}); + + getStyles(stylingContext, otherDirective); + expect(store.getValues()).toEqual({'font-size': null, color: ''}); + }); }); describe('classes', () => { @@ -1901,35 +1892,36 @@ describe('style and class based bindings', () => { const template = createStylingContext(null, null, null, ['one', 'two']); assertContext(template, [ element, - masterConfig(17, false), // - [null, 2, false, null], + masterConfig(18, false), // + [2, null], [null, null], [null, null, 'one', false, 0, 'two', false, 0], - [0, 2, 0, 2, 9, 13], - [2, 0, 17, null, 2], - [0, 0, 17, null, 0], + [0, 2, 0, 2, 10, 14], + [2, 0, 18, null, 2], + [0, 0, 18, null, 0], + null, null, - // #9 - cleanClass(3, 17), + // #10 + cleanClass(3, 18), 'one', null, 0, - // #13 - cleanClass(6, 21), + // #14 + cleanClass(6, 22), 'two', null, 0, - // #17 - cleanClass(3, 9), + // #18 + cleanClass(3, 10), 'one', null, 0, - // #21 - cleanClass(6, 13), + // #22 + cleanClass(6, 14), 'two', null, 0, @@ -1982,59 +1974,60 @@ describe('style and class based bindings', () => { ['width', '100px'], ['width', 'height'], ['wide'], ['wide', 'tall']); assertContext(stylingContext, [ element, - masterConfig(25, false), // - [null, 2, false, null], + masterConfig(26, false), // + [2, null], [null, null, 'width', '100px', 0, 'height', null, 0], [null, null, 'wide', true, 0, 'tall', false, 0], - [2, 2, 2, 2, 9, 13, 17, 21], - [2, 0, 33, null, 2], - [2, 0, 25, null, 2], + [2, 2, 2, 2, 10, 14, 18, 22], + [2, 0, 34, null, 2], + [2, 0, 26, null, 2], + null, null, - // #9 - cleanStyle(3, 25), + // #10 + cleanStyle(3, 26), 'width', null, 0, - // #13 - cleanStyle(6, 29), + // #14 + cleanStyle(6, 30), 'height', null, 0, - // #17 - cleanClass(3, 33), + // #18 + cleanClass(3, 34), 'wide', null, 0, - // #21 - cleanClass(6, 37), + // #22 + cleanClass(6, 38), 'tall', null, 0, - // #25 - cleanStyle(3, 9), + // #26 + cleanStyle(3, 10), 'width', null, 0, - // #29 - cleanStyle(6, 13), + // #30 + cleanStyle(6, 14), 'height', null, 0, - // #33 - cleanClass(3, 17), + // #34 + cleanClass(3, 18), 'wide', null, 0, - // #37 - cleanClass(6, 21), + // #38 + cleanClass(6, 22), 'tall', null, 0, @@ -2046,71 +2039,72 @@ describe('style and class based bindings', () => { updateStylingMap(stylingContext, 'tall round', cachedStyleMap); assertContext(stylingContext, [ element, - masterConfig(25, true), // - [null, 2, true, null], + masterConfig(26, true), // + [2, null], [null, null, 'width', '100px', 0, 'height', null, 0], [null, null, 'wide', true, 0, 'tall', false, 0], - [2, 2, 2, 2, 9, 13, 17, 21], - [2, 0, 37, 'tall round', 2], - [2, 0, 25, cachedStyleMap, 2], + [2, 2, 2, 2, 10, 14, 18, 22], + [2, 0, 38, 'tall round', 2], + [2, 0, 26, cachedStyleMap, 2], + null, null, - // #9 - cleanStyle(3, 25), + // #10 + cleanStyle(3, 26), 'width', null, 0, - // #13 - cleanStyle(6, 33), + // #14 + cleanStyle(6, 34), 'height', null, 0, - // #17 - cleanClass(3, 45), + // #18 + cleanClass(3, 46), 'wide', null, 0, - // #21 - cleanClass(6, 37), + // #22 + cleanClass(6, 38), 'tall', null, 0, - // #25 - dirtyStyle(3, 9), + // #26 + dirtyStyle(3, 10), 'width', '200px', 0, - // #29 + // #30 dirtyStyle(0, 0), 'opacity', '0.5', 0, - // #33 - cleanStyle(6, 13), + // #34 + cleanStyle(6, 14), 'height', null, 0, - // #37 - dirtyClass(6, 21), + // #38 + dirtyClass(6, 22), 'tall', true, 0, - // #41 + // #42 dirtyClass(0, 0), 'round', true, 0, - // #45 - cleanClass(3, 17), + // #46 + cleanClass(3, 18), 'wide', null, 0, @@ -2128,70 +2122,71 @@ describe('style and class based bindings', () => { assertContext(stylingContext, [ element, - masterConfig(25, true), // - [null, 2, true, null], + masterConfig(26, true), // + [2, null], [null, null, 'width', '100px', 0, 'height', null, 0], [null, null, 'wide', true, 0, 'tall', false, 0], - [2, 2, 2, 2, 9, 13, 17, 21], - [2, 0, 37, cachedClassMap, 2], - [1, 0, 25, cachedStyleMap, 1], + [2, 2, 2, 2, 10, 14, 18, 22], + [2, 0, 38, cachedClassMap, 2], + [1, 0, 26, cachedStyleMap, 1], + null, null, - // #9 - dirtyStyle(3, 25), + // #10 + dirtyStyle(3, 26), 'width', '300px', 0, - // #13 - cleanStyle(6, 33), + // #14 + cleanStyle(6, 34), 'height', null, 0, - // #17 - cleanClass(3, 41), + // #18 + cleanClass(3, 42), 'wide', null, 0, - // #21 - cleanClass(6, 37), + // #22 + cleanClass(6, 38), 'tall', null, 0, - // #25 - cleanStyle(3, 9), + // #26 + cleanStyle(3, 10), 'width', '500px', 0, - // #29 + // #30 dirtyStyle(0, 0), 'opacity', null, 0, - // #33 - cleanStyle(6, 13), + // #34 + cleanStyle(6, 14), 'height', null, 0, - // #37 - cleanClass(6, 21), + // #38 + cleanClass(6, 22), 'tall', true, 0, - // #41 - cleanClass(3, 17), + // #42 + cleanClass(3, 18), 'wide', true, 0, - // #45 + // #46 dirtyClass(0, 0), 'round', null, @@ -2224,19 +2219,20 @@ describe('style and class based bindings', () => { assertContext(stylingContext, [ element, // - masterConfig(9, false), // - [null, 2, false, null], // + masterConfig(10, false), // + [2, null], // [null, null], // [null, null], // [0, 0, 0, 0], // - [1, 0, 13, classesMap, 1], // - [1, 0, 9, stylesMap, 1], // + [1, 0, 14, classesMap, 1], // + [1, 0, 10, stylesMap, 1], // + null, // null, // - // #9 + // #10 cleanStyle(0, 0), 'width', '200px', 0, - // #13 + // #14 cleanClass(0, 0), 'foo', true, 0 ]); @@ -2250,19 +2246,20 @@ describe('style and class based bindings', () => { assertContext(stylingContext, [ element, // - masterConfig(9, false), // - [null, 2, false, null], // + masterConfig(10, false), // + [2, null], // [null, null], // [null, null], // [0, 0, 0, 0], // - [1, 0, 13, classesMap, 1], // - [1, 0, 9, stylesMap, 1], // + [1, 0, 14, classesMap, 1], // + [1, 0, 10, stylesMap, 1], // + null, // null, // - // #9 + // #10 cleanStyle(0, 0), 'width', '200px', 0, - // #13 + // #14 cleanClass(0, 0), 'foo', true, 0 ]); }); @@ -2279,36 +2276,37 @@ describe('style and class based bindings', () => { assertContext(stylingContext, [ element, - masterConfig(9, false), // - [null, 2, false, null], + masterConfig(10, false), // + [2, null], [null, null], [null, null], [0, 0, 0, 0], - [3, 0, 9, 'apple orange banana', 3], - [0, 0, 9, null, 0], + [3, 0, 10, 'apple orange banana', 3], + [0, 0, 10, null, 0], + null, null, - // #9 + // #10 cleanClass(0, 0), 'apple', true, 0, - // #13 + // #14 cleanClass(0, 0), 'orange', true, 0, - // #17 + // #18 cleanClass(0, 0), 'banana', true, 0, ]); - stylingContext[13 + StylingIndex.ValueOffset] = false; - stylingContext[17 + StylingIndex.ValueOffset] = false; + stylingContext[14 + StylingIndex.ValueOffset] = false; + stylingContext[18 + StylingIndex.ValueOffset] = false; updateStylingMap(stylingContext, classes); // apply the styles @@ -2645,35 +2643,36 @@ describe('style and class based bindings', () => { assertContext(context, [ element, // - masterConfig(17, false), // - [null, 2, false, null], // + masterConfig(18, false), // + [2, null], // [null, null, 'color', null, 0], // [null, null, 'foo', false, 0], // - [1, 1, 1, 1, 9, 13], // - [1, 0, 21, null, 1], // - [1, 0, 17, null, 1], // + [1, 1, 1, 1, 10, 14], // + [1, 0, 22, null, 1], // + [1, 0, 18, null, 1], // + null, // null, // - // #9 - cleanStyle(3, 17), + // #10 + cleanStyle(3, 18), 'color', null, 0, - // #13 - cleanClass(3, 21), + // #14 + cleanClass(3, 22), 'foo', null, 0, - // #17 - cleanStyle(3, 9), + // #18 + cleanStyle(3, 10), 'color', null, 0, - // #21 - cleanClass(3, 13), + // #22 + cleanClass(3, 14), 'foo', null, 0, @@ -2708,47 +2707,48 @@ describe('style and class based bindings', () => { assertContext(context, [ element, // - masterConfig(17, false), // - [null, 2, false, null], // + masterConfig(18, false), // + [2, null], // [null, null, 'color', null, 0], // [null, null, 'foo', false, 0], // - [1, 1, 1, 1, 9, 13], // - [1, 0, 25, classMapWithPlayerFactory, 1], // - [1, 0, 17, styleMapWithPlayerFactory, 1], // + [1, 1, 1, 1, 10, 14], // + [1, 0, 26, classMapWithPlayerFactory, 1], // + [1, 0, 18, styleMapWithPlayerFactory, 1], // + null, // playerContext, - // #9 - cleanStyle(3, 21), + // #10 + cleanStyle(3, 22), 'color', 'red', directiveOwnerPointers(0, 5), - // #13 - cleanClass(3, 29), + // #14 + cleanClass(3, 30), 'foo', true, directiveOwnerPointers(0, 7), - // #17 + // #18 cleanStyle(0, 0), 'opacity', '1', directiveOwnerPointers(0, 3), - // #21 - cleanStyle(3, 9), + // #22 + cleanStyle(3, 10), 'color', null, 0, - // #25 + // #26 cleanClass(0, 0), 'map', true, directiveOwnerPointers(0, 1), - // #29 - cleanClass(3, 13), + // #30 + cleanClass(3, 14), 'foo', null, 0, @@ -2769,47 +2769,48 @@ describe('style and class based bindings', () => { assertContext(context, [ element, // - masterConfig(17, false), // - [null, 2, false, null], // + masterConfig(18, false), // + [2, null], // [null, null, 'color', null, 0], // [null, null, 'foo', false, 0], // - [1, 1, 1, 1, 9, 13], // - [1, 0, 25, cachedClassMap, 1], // - [1, 0, 17, cachedStyleMap, 1], // + [1, 1, 1, 1, 10, 14], // + [1, 0, 26, cachedClassMap, 1], // + [1, 0, 18, cachedStyleMap, 1], // + null, // playerContext, - // #9 - cleanStyle(3, 21), + // #10 + cleanStyle(3, 22), 'color', 'blue', 0, - // #13 - cleanClass(3, 29), + // #14 + cleanClass(3, 30), 'foo', false, 0, - // #17 + // #18 cleanStyle(0, 0), 'opacity', '1', 0, - // #21 - cleanStyle(3, 9), + // #22 + cleanStyle(3, 10), 'color', null, 0, - // #25 + // #26 cleanClass(0, 0), 'map', true, 0, - // #29 - cleanClass(3, 13), + // #30 + cleanClass(3, 14), 'foo', null, 0, diff --git a/tools/material-ci/angular_material_test_blocklist.js b/tools/material-ci/angular_material_test_blocklist.js index 17bf32c8b0..a66259a878 100644 --- a/tools/material-ci/angular_material_test_blocklist.js +++ b/tools/material-ci/angular_material_test_blocklist.js @@ -16,14 +16,5 @@ // clang-format off // tslint:disable -window.testBlocklist = { - "MatSidenav should be fixed position when in fixed mode": { - "error": "Error: Expected ng-tns-c380-0 ng-trigger ng-trigger-transform mat-drawer mat-sidenav mat-drawer-over ng-star-inserted to contain 'mat-sidenav-fixed'.", - "notes": "FW-1132: Host class bindings don't work if super class has host class bindings" - }, - "MatSidenav should set fixed bottom and top when in fixed mode": { - "error": "Error: Expected '' to be '20px'.", - "notes": "FW-1132: Host class bindings don't work if super class has host class bindings" - } -}; -// clang-format on +window.testBlocklist = {}; +// clang-format on \ No newline at end of file