diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts index f646309f0a..13aec1cc79 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -163,6 +163,7 @@ export { export { Player as ɵPlayer, + PlayerFactory as ɵPlayerFactory, PlayState as ɵPlayState, PlayerHandler as ɵPlayerHandler, } from './render3/interfaces/player'; @@ -171,6 +172,10 @@ export { LContext as ɵLContext, } from './render3/interfaces/context'; +export { + bindPlayerFactory as ɵbindPlayerFactory, +} from './render3/styling/player_factory'; + export { addPlayer as ɵaddPlayer, getPlayers as ɵgetPlayers, diff --git a/packages/core/src/render3/discovery_utils.ts b/packages/core/src/render3/discovery_utils.ts index bbba6fb7fd..1752d045d8 100644 --- a/packages/core/src/render3/discovery_utils.ts +++ b/packages/core/src/render3/discovery_utils.ts @@ -74,9 +74,9 @@ export function getHostComponent(target: {}): T|null { * Returns the `RootContext` instance that is associated with * the application where the target is situated. */ -export function getRootContext(target: {}): RootContext { - const context = loadContext(target) !; - const rootLViewData = getRootView(context.lViewData); +export function getRootContext(target: LViewData | {}): RootContext { + const lViewData = Array.isArray(target) ? target : loadContext(target) !.lViewData; + const rootLViewData = getRootView(lViewData); return rootLViewData[CONTEXT] as RootContext; } diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index 3f533573bf..19580eedb3 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -20,6 +20,7 @@ import {ACTIVE_INDEX, LContainer, VIEWS} from './interfaces/container'; import {ComponentDef, ComponentQuery, ComponentTemplate, DirectiveDef, DirectiveDefListOrFactory, InitialStylingFlags, PipeDefListOrFactory, RenderFlags} from './interfaces/definition'; import {INJECTOR_SIZE} from './interfaces/injector'; import {AttributeMarker, InitialInputData, InitialInputs, LocalRefExtractor, PropertyAliasValue, PropertyAliases, TAttributes, TContainerNode, TElementContainerNode, TElementNode, TNode, TNodeFlags, TNodeType, TProjectionNode, TViewNode} from './interfaces/node'; +import {PlayerFactory} from './interfaces/player'; import {CssSelectorList, NG_PROJECT_AS_ATTR_NAME} from './interfaces/projection'; import {LQueries} from './interfaces/query'; import {ProceduralRenderer3, RComment, RElement, RNode, RText, Renderer3, RendererFactory3, isProceduralRenderer} from './interfaces/renderer'; @@ -27,9 +28,10 @@ import {BINDING_INDEX, CLEANUP, CONTAINER_INDEX, CONTENT_QUERIES, CONTEXT, Curre import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert'; import {appendChild, appendProjectedNode, createTextNode, findComponentView, getLViewChild, getRenderParent, insertView, removeView} from './node_manipulation'; import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher'; -import {createStylingContextTemplate, renderStyling as renderElementStyles, updateClassProp as updateElementClassProp, updateStyleProp as updateElementStyleProp, updateStylingMap} from './styling/class_and_style_bindings'; +import {createStylingContextTemplate, renderStyleAndClassBindings, updateClassProp as updateElementClassProp, updateStyleProp as updateElementStyleProp, updateStylingMap} from './styling/class_and_style_bindings'; +import {BoundPlayerFactory} from './styling/player_factory'; import {getStylingContext} from './styling/util'; -import {assertDataInRangeInternal, getComponentViewByIndex, getNativeByIndex, getNativeByTNode, getRootView, getTNode, isComponent, isContentQueryHost, isDifferent, loadInternal, readPatchedLViewData, stringify} from './util'; +import {assertDataInRangeInternal, getComponentViewByIndex, getNativeByIndex, getNativeByTNode, getRootContext, getRootView, getTNode, isComponent, isContentQueryHost, isDifferent, loadInternal, readPatchedLViewData, stringify} from './util'; @@ -1501,9 +1503,11 @@ function generatePropertyAliases( * renaming as part of minification. * @param value A value indicating if a given class should be added or removed. */ -export function elementClassProp( - index: number, stylingIndex: number, value: T | NO_CHANGE): void { - updateElementClassProp(getStylingContext(index, viewData), stylingIndex, value ? true : false); +export function elementClassProp( + index: number, stylingIndex: number, value: boolean | PlayerFactory): void { + const val = + (value instanceof BoundPlayerFactory) ? (value as BoundPlayerFactory) : (!!value); + updateElementClassProp(getStylingContext(index, viewData), stylingIndex, val); } /** @@ -1534,7 +1538,7 @@ export function elementClassProp( * @param styleSanitizer An optional sanitizer function that will be used (if provided) * to sanitize the any CSS property values that are applied to the element (during rendering). */ -export function elementStyling( +export function elementStyling( classDeclarations?: (string | boolean | InitialStylingFlags)[] | null, styleDeclarations?: (string | boolean | InitialStylingFlags)[] | null, styleSanitizer?: StyleSanitizeFn | null): void { @@ -1565,8 +1569,13 @@ export function elementStyling( * specifically for element styling--the index must be the next index after the element * index.) */ -export function elementStylingApply(index: number): void { - renderElementStyles(getStylingContext(index, viewData), renderer); +export function elementStylingApply(index: number): void { + const totalPlayersQueued = + renderStyleAndClassBindings(getStylingContext(index, viewData), renderer, viewData); + if (totalPlayersQueued > 0) { + const rootContext = getRootContext(viewData); + scheduleTick(rootContext, RootContextFlags.FlushPlayers); + } } /** @@ -1589,8 +1598,9 @@ export function elementStylingApply(index: number): void { * Note that when a suffix is provided then the underlying sanitizer will * be ignored. */ -export function elementStyleProp( - index: number, styleIndex: number, value: T | null, suffix?: string): void { +export function elementStyleProp( + index: number, styleIndex: number, value: string | number | String | PlayerFactory | null, + suffix?: string): void { let valueToAdd: string|null = null; if (value) { if (suffix) { @@ -2386,11 +2396,7 @@ export function markViewDirty(view: LViewData): void { ngDevMode && assertDefined(currentView[CONTEXT], 'rootContext should be defined'); const rootContext = currentView[CONTEXT] as RootContext; - const nothingScheduled = rootContext.flags === RootContextFlags.Empty; - rootContext.flags |= RootContextFlags.DetectChanges; - if (nothingScheduled) { - scheduleTick(rootContext); - } + scheduleTick(rootContext, RootContextFlags.DetectChanges); } /** @@ -2404,8 +2410,11 @@ export function markViewDirty(view: LViewData): void { * `scheduleTick` requests. The scheduling function can be overridden in * `renderComponent`'s `scheduler` option. */ -export function scheduleTick(rootContext: RootContext) { - if (rootContext.clean == _CLEAN_PROMISE) { +export function scheduleTick(rootContext: RootContext, flags: RootContextFlags) { + const nothingScheduled = rootContext.flags === RootContextFlags.Empty; + rootContext.flags |= flags; + + if (nothingScheduled && rootContext.clean == _CLEAN_PROMISE) { let res: null|((val: null) => void); rootContext.clean = new Promise((r) => res = r); rootContext.scheduler(() => { diff --git a/packages/core/src/render3/interfaces/player.ts b/packages/core/src/render3/interfaces/player.ts index b14a44707e..465edf1387 100644 --- a/packages/core/src/render3/interfaces/player.ts +++ b/packages/core/src/render3/interfaces/player.ts @@ -19,6 +19,43 @@ export interface Player { addEventListener(state: PlayState|string, cb: (data?: any) => any): void; } +export const enum BindingType { + Unset = 0, + Class = 2, + Style = 3, +} + +export interface BindingStore { setValue(prop: string, value: any): void; } + +/** + * Defines the shape which produces the Player. + * + * Used to produce a player that will be placed on an element that contains + * styling bindings that make use of the player. This function is designed + * to be used with `PlayerFactory`. + */ +export interface PlayerFactoryBuildFn { + (element: HTMLElement, type: BindingType, values: {[key: string]: any}, + currentPlayer: Player|null): Player|null; +} + +/** + * Used as a reference to build a player from a styling template binding + * (`[style]` and `[class]`). + * + * The `fn` function will be called once any styling-related changes are + * evaluated on an element and is expected to return a player that will + * be then run on the element. + * + * `[style]`, `[style.prop]`, `[class]` and `[class.name]` template bindings + * all accept a `PlayerFactory` as input and this player factories. + */ +export interface PlayerFactory { '__brand__': 'Brand for PlayerFactory that nothing will match'; } + +export interface PlayerBuilder extends BindingStore { + buildPlayer(currentPlayer: Player|null): Player|undefined|null; +} + /** * The state of a given player * @@ -29,11 +66,15 @@ export interface Player { export const enum PlayState {Pending = 0, Running = 1, Paused = 2, Finished = 100, Destroyed = 200} /** - * The context that stores all active animation players present on an element. + * The context that stores all the active players and queued player factories present on an element. */ -export declare type PlayerContext = Player[]; -export declare type ComponentInstance = {}; -export declare type DirectiveInstance = {}; +export interface PlayerContext extends Array { + [PlayerIndex.NonBuilderPlayersStart]: number; + [PlayerIndex.ClassMapPlayerBuilderPosition]: PlayerBuilder|null; + [PlayerIndex.ClassMapPlayerPosition]: Player|null; + [PlayerIndex.StyleMapPlayerBuilderPosition]: PlayerBuilder|null; + [PlayerIndex.StyleMapPlayerPosition]: Player|null; +} /** * Designed to be used as an injection service to capture all animation players. @@ -54,3 +95,29 @@ export interface PlayerHandler { */ queuePlayer(player: Player, context: ComponentInstance|DirectiveInstance|HTMLElement): void; } + +export const enum PlayerIndex { + // The position where the index that reveals where players start in the PlayerContext + NonBuilderPlayersStart = 0, + // The position where the player builder lives (which handles {key:value} map expression) for + // classes + ClassMapPlayerBuilderPosition = 1, + // The position where the last player assigned to the class player builder is stored + ClassMapPlayerPosition = 2, + // The position where the player builder lives (which handles {key:value} map expression) for + // styles + StyleMapPlayerBuilderPosition = 3, + // The position where the last player assigned to the style player builder is stored + StyleMapPlayerPosition = 4, + // The position where any player builders start in the PlayerContext + PlayerBuildersStartPosition = 1, + // The position where non map-based player builders start in the PlayerContext + SinglePlayerBuildersStartPosition = 5, + // For each player builder there is a player in the player context (therefore size = 2) + PlayerAndPlayerBuildersTupleSize = 2, + // The player exists next to the player builder in the list + PlayerOffsetPosition = 1, +} + +export declare type ComponentInstance = {}; +export declare type DirectiveInstance = {}; diff --git a/packages/core/src/render3/interfaces/styling.ts b/packages/core/src/render3/interfaces/styling.ts index f6f6575b03..880bc59ad0 100644 --- a/packages/core/src/render3/interfaces/styling.ts +++ b/packages/core/src/render3/interfaces/styling.ts @@ -5,14 +5,11 @@ * 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 {StyleSanitizeFn} from '../../sanitization/style_sanitizer'; import {RElement} from '../interfaces/renderer'; - import {PlayerContext} from './player'; - /** * The styling context acts as a styling manifest (shaped as an array) for determining which * styling properties have been assigned via the provided `updateStylingMap`, `updateStyleProp` @@ -184,17 +181,19 @@ export interface InitialStyles extends Array { [0]: null; } */ export const enum StylingFlags { // Implies no configurations - None = 0b000, + None = 0b0000, // Whether or not the entry or context itself is dirty - Dirty = 0b001, + Dirty = 0b0001, // Whether or not this is a class-based assignment - Class = 0b010, + Class = 0b0010, // Whether or not a sanitizer was applied to this property - Sanitize = 0b100, + Sanitize = 0b0100, + // Whether or not any player builders within need to produce new players + PlayerBuildersDirty = 0b1000, // The max amount of bits used to represent these configuration values - BitCountSize = 3, + BitCountSize = 4, // There are only three bits here - BitMask = 0b111 + BitMask = 0b1111 } /** Used as numeric pointer values to determine what cells to update in the `StylingContext` */ @@ -222,10 +221,11 @@ export const enum StylingIndex { FlagsOffset = 0, PropertyOffset = 1, ValueOffset = 2, - // Size of each multi or single entry (flag + prop + value) - Size = 3, + PlayerBuilderIndexOffset = 3, + // Size of each multi or single entry (flag + prop + value + playerBuilderIndex) + Size = 4, // Each flag has a binary digit length of this value - BitCountSize = 14, // (32 - 3) / 2 = ~14 + BitCountSize = 14, // (32 - 4) / 2 = ~14 // The binary digit value as a mask - BitMask = 0b11111111111111 // 14 bits + BitMask = 0b11111111111111, // 14 bits } diff --git a/packages/core/src/render3/interfaces/view.ts b/packages/core/src/render3/interfaces/view.ts index 022d8b298d..13af334301 100644 --- a/packages/core/src/render3/interfaces/view.ts +++ b/packages/core/src/render3/interfaces/view.ts @@ -9,15 +9,16 @@ import {Injector} from '../../di/injector'; import {QueryList} from '../../linker'; import {Sanitizer} from '../../sanitization/security'; -import {PlayerHandler} from '../interfaces/player'; import {LContainer} from './container'; import {ComponentDef, ComponentQuery, ComponentTemplate, DirectiveDef, DirectiveDefList, HostBindingsFunction, PipeDef, PipeDefList} from './definition'; import {TElementNode, TNode, TViewNode} from './node'; +import {PlayerHandler} from './player'; import {LQueries} from './query'; import {RElement, Renderer3} from './renderer'; import {StylingContext} from './styling'; + /** Size of LViewData's header. Necessary to adjust for it when setting slots. */ export const HEADER_OFFSET = 17; diff --git a/packages/core/src/render3/players.ts b/packages/core/src/render3/players.ts index bd79d42ed9..5cf2762cae 100644 --- a/packages/core/src/render3/players.ts +++ b/packages/core/src/render3/players.ts @@ -5,43 +5,60 @@ * 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 {getContext} from './context_discovery'; -import {scheduleTick} from './instructions'; -import {ComponentInstance, DirectiveInstance, PlayState, Player} from './interfaces/player'; -import {RootContextFlags} from './interfaces/view'; -import {CorePlayerHandler} from './styling/core_player_handler'; -import {getOrCreatePlayerContext} from './styling/util'; -import {getRootContext} from './util'; +import './ng_dev_mode'; +import {getContext} from './context_discovery'; +import {getRootContext} from './discovery_utils'; +import {scheduleTick} from './instructions'; +import {ComponentInstance, DirectiveInstance, Player} from './interfaces/player'; +import {HEADER_OFFSET, RootContextFlags} from './interfaces/view'; +import {addPlayerInternal, getOrCreatePlayerContext, getPlayerContext, getPlayersInternal, getStylingContext, throwInvalidRefError} from './styling/util'; + +/** + * Adds a player to an element, directive or component instance that will later be + * animated once change detection has passed. + * + * When a player is added to a reference it will stay active until `player.destroy()` + * is called. Once called then the player will be removed from the active players + * present on the associated ref instance. + * + * To get a list of all the active players on an element see [getPlayers]. + * + * @param ref The element, directive or component that the player will be placed on. + * @param player The player that will be triggered to play once change detection has run. + */ export function addPlayer( ref: ComponentInstance | DirectiveInstance | HTMLElement, player: Player): void { - const elementContext = getContext(ref) !; - const animationContext = getOrCreatePlayerContext(elementContext.native, elementContext) !; - animationContext.push(player); - - player.addEventListener(PlayState.Destroyed, () => { - const index = animationContext.indexOf(player); - if (index >= 0) { - animationContext.splice(index, 1); - } - player.destroy(); - }); - - const rootContext = getRootContext(elementContext.lViewData); - const playerHandler = - rootContext.playerHandler || (rootContext.playerHandler = new CorePlayerHandler()); - playerHandler.queuePlayer(player, ref); - - const nothingScheduled = rootContext.flags === RootContextFlags.Empty; - - // change detection may or may not happen therefore - // the core code needs to be kicked off to flush the animations - rootContext.flags |= RootContextFlags.FlushPlayers; - if (nothingScheduled) { - scheduleTick(rootContext); + const context = getContext(ref); + if (!context) { + ngDevMode && throwInvalidRefError(); + return; } + + const element = context.native as HTMLElement; + const lViewData = context.lViewData; + const playerContext = getOrCreatePlayerContext(element, context) !; + const rootContext = getRootContext(lViewData); + addPlayerInternal(playerContext, rootContext, element, player, 0, ref); + scheduleTick(rootContext, RootContextFlags.FlushPlayers); } +/** + * Returns a list of all the active players present on the provided ref instance (which can + * be an instance of a directive, component or element). + * + * This function will only return players that have been added to the ref instance using + * `addPlayer` or any players that are active through any template styling bindings + * (`[style]`, `[style.prop]`, `[class]` and `[class.name]`). + */ export function getPlayers(ref: ComponentInstance | DirectiveInstance | HTMLElement): Player[] { - return getOrCreatePlayerContext(ref); + const context = getContext(ref); + if (!context) { + ngDevMode && throwInvalidRefError(); + return []; + } + + const stylingContext = getStylingContext(context.nodeIndex - HEADER_OFFSET, context.lViewData); + const playerContext = stylingContext ? getPlayerContext(stylingContext) : null; + return playerContext ? getPlayersInternal(playerContext) : []; } 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 529192f6e6..c59e0857cc 100644 --- a/packages/core/src/render3/styling/class_and_style_bindings.ts +++ b/packages/core/src/render3/styling/class_and_style_bindings.ts @@ -5,13 +5,20 @@ * 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 {StyleSanitizeFn} from '../../sanitization/style_sanitizer'; import {InitialStylingFlags} from '../interfaces/definition'; +import {BindingStore, BindingType, Player, PlayerBuilder, PlayerFactory, PlayerIndex} from '../interfaces/player'; import {Renderer3, RendererStyleFlags3, isProceduralRenderer} from '../interfaces/renderer'; import {InitialStyles, StylingContext, StylingFlags, StylingIndex} from '../interfaces/styling'; +import {LViewData, RootContext} from '../interfaces/view'; +import {getRootContext} from '../util'; + +import {BoundPlayerFactory} from './player_factory'; +import {addPlayerInternal, allocPlayerContext, createEmptyStylingContext, getPlayerContext} from './util'; + +const EMPTY_ARR: any[] = []; +const EMPTY_OBJ: {[key: string]: any} = {}; -import {EMPTY_ARR, EMPTY_OBJ, createEmptyStylingContext} from './util'; /** * Creates a styling context template where styling information is stored. @@ -124,12 +131,14 @@ export function createStylingContextTemplate( setFlag(context, indexForSingle, pointers(initialFlag, indexForInitial, indexForMulti)); setProp(context, indexForSingle, prop); setValue(context, indexForSingle, null); + setPlayerBuilderIndex(context, indexForSingle, 0); const flagForMulti = initialFlag | (initialValue !== null ? StylingFlags.Dirty : StylingFlags.None); setFlag(context, indexForMulti, pointers(flagForMulti, indexForInitial, indexForSingle)); setProp(context, indexForMulti, prop); setValue(context, indexForMulti, null); + setPlayerBuilderIndex(context, indexForMulti, 0); } // there is no initial value flag for the master index since it doesn't @@ -142,7 +151,7 @@ export function createStylingContextTemplate( /** * Sets and resolves all `multi` styling on an `StylingContext` so that they can be - * applied to the element once `renderStyling` is called. + * applied to the element once `renderStyleAndClassBindings` is called. * * All missing styles/class (any values that are not provided in the new `styles` * or `classes` params) will resolve to `null` within their respective positions @@ -150,43 +159,73 @@ export function createStylingContextTemplate( * * @param context The styling context that will be updated with the * newly provided style values. - * @param classes The key/value map of CSS class names that will be used for the update. - * @param styles The key/value map of CSS styles that will be used for the update. + * @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. */ export function updateStylingMap( - context: StylingContext, classes: {[key: string]: any} | string | null, - styles?: {[key: string]: any} | null): void { - styles = styles || null; + context: StylingContext, classesInput: {[key: string]: any} | string | + BoundPlayerFactory| null, + stylesInput?: {[key: string]: any} | BoundPlayerFactory| + null): void { + stylesInput = stylesInput || null; + + const element = context[StylingIndex.ElementPosition] !as HTMLElement; + const classesPlayerBuilder = classesInput instanceof BoundPlayerFactory ? + new ClassAndStylePlayerBuilder(classesInput as any, element, BindingType.Class) : + null; + const stylesPlayerBuilder = stylesInput instanceof BoundPlayerFactory ? + new ClassAndStylePlayerBuilder(stylesInput as any, element, BindingType.Style) : + null; + + const classesValue = classesPlayerBuilder ? + (classesInput as BoundPlayerFactory<{[key: string]: any}|string>) !.value : + classesInput; + const stylesValue = stylesPlayerBuilder ? stylesInput !.value : stylesInput; + // early exit (this is what's done to avoid using ctx.bind() to cache the value) - const ignoreAllClassUpdates = classes === context[StylingIndex.PreviousMultiClassValue]; - const ignoreAllStyleUpdates = styles === context[StylingIndex.PreviousMultiStyleValue]; + const ignoreAllClassUpdates = classesValue === context[StylingIndex.PreviousMultiClassValue]; + const ignoreAllStyleUpdates = stylesValue === context[StylingIndex.PreviousMultiStyleValue]; if (ignoreAllClassUpdates && ignoreAllStyleUpdates) return; + context[StylingIndex.PreviousMultiClassValue] = classesValue; + context[StylingIndex.PreviousMultiStyleValue] = stylesValue; + let classNames: string[] = EMPTY_ARR; let applyAllClasses = false; + let playerBuildersAreDirty = false; + + const classesPlayerBuilderIndex = + classesPlayerBuilder ? PlayerIndex.ClassMapPlayerBuilderPosition : 0; + if (hasPlayerBuilderChanged( + context, classesPlayerBuilder, PlayerIndex.ClassMapPlayerBuilderPosition)) { + setPlayerBuilder(context, classesPlayerBuilder, PlayerIndex.ClassMapPlayerBuilderPosition); + playerBuildersAreDirty = true; + } + + const stylesPlayerBuilderIndex = + stylesPlayerBuilder ? PlayerIndex.StyleMapPlayerBuilderPosition : 0; + if (hasPlayerBuilderChanged( + context, stylesPlayerBuilder, PlayerIndex.StyleMapPlayerBuilderPosition)) { + setPlayerBuilder(context, stylesPlayerBuilder, PlayerIndex.StyleMapPlayerBuilderPosition); + playerBuildersAreDirty = true; + } // each time a string-based value pops up then it shouldn't require a deep // check of what's changed. if (!ignoreAllClassUpdates) { - context[StylingIndex.PreviousMultiClassValue] = classes; - if (typeof classes == 'string') { - classNames = classes.split(/\s+/); + if (typeof classesValue == 'string') { + classNames = classesValue.split(/\s+/); // this boolean is used to avoid having to create a key/value map of `true` values // since a classname string implies that all those classes are added applyAllClasses = true; } else { - classNames = classes ? Object.keys(classes) : EMPTY_ARR; + classNames = classesValue ? Object.keys(classesValue) : EMPTY_ARR; } } - classes = (classes || EMPTY_OBJ) as{[key: string]: any}; - - if (!ignoreAllStyleUpdates) { - context[StylingIndex.PreviousMultiStyleValue] = styles; - } - - const styleProps = styles ? Object.keys(styles) : EMPTY_ARR; - styles = styles || EMPTY_OBJ; + const classes = (classesValue || EMPTY_OBJ) as{[key: string]: any}; + const styleProps = stylesValue ? Object.keys(stylesValue) : EMPTY_ARR; + const styles = stylesValue || EMPTY_OBJ; const classesStartIndex = styleProps.length; const multiStartIndex = getMultiStartIndex(context); @@ -213,13 +252,18 @@ export function updateStylingMap( isClassBased ? classNames[adjustedPropIndex] : styleProps[adjustedPropIndex]; const newValue: string|boolean = isClassBased ? (applyAllClasses ? true : classes[newProp]) : styles[newProp]; + const playerBuilderIndex = + isClassBased ? classesPlayerBuilderIndex : stylesPlayerBuilderIndex; const prop = getProp(context, ctxIndex); if (prop === newProp) { const value = getValue(context, ctxIndex); const flag = getPointers(context, ctxIndex); + setPlayerBuilderIndex(context, ctxIndex, playerBuilderIndex); + if (hasValueChanged(flag, value, newValue)) { setValue(context, ctxIndex, newValue); + playerBuildersAreDirty = playerBuildersAreDirty || !!playerBuilderIndex; const initialValue = getInitialValue(context, flag); @@ -242,13 +286,16 @@ export function updateStylingMap( setValue(context, ctxIndex, newValue); if (hasValueChanged(flagToCompare, initialValue, newValue)) { setDirty(context, ctxIndex, true); + playerBuildersAreDirty = playerBuildersAreDirty || !!playerBuilderIndex; dirty = true; } } } else { // we only care to do this if the insertion is in the middle const newFlag = prepareInitialFlag(newProp, isClassBased, getStyleSanitizer(context)); - insertNewMultiProperty(context, ctxIndex, isClassBased, newProp, newFlag, newValue); + playerBuildersAreDirty = playerBuildersAreDirty || !!playerBuilderIndex; + insertNewMultiProperty( + context, ctxIndex, isClassBased, newProp, newFlag, newValue, playerBuilderIndex); dirty = true; } } @@ -272,6 +319,13 @@ export function updateStylingMap( if (doRemoveValue) { setDirty(context, ctxIndex, true); setValue(context, ctxIndex, null); + + // we keep the player factory the same so that the `nulled` value can + // be instructed into the player because removing a style and/or a class + // is a valid animation player instruction. + const playerBuilderIndex = + isClassBased ? classesPlayerBuilderIndex : stylesPlayerBuilderIndex; + setPlayerBuilderIndex(context, ctxIndex, playerBuilderIndex); dirty = true; } } @@ -292,7 +346,9 @@ export function updateStylingMap( const value: string|boolean = isClassBased ? (applyAllClasses ? true : classes[prop]) : styles[prop]; const flag = prepareInitialFlag(prop, isClassBased, sanitizer) | StylingFlags.Dirty; - context.push(flag, prop, value); + const playerBuilderIndex = + isClassBased ? classesPlayerBuilderIndex : stylesPlayerBuilderIndex; + context.push(flag, prop, value, playerBuilderIndex); dirty = true; } propIndex++; @@ -301,11 +357,15 @@ export function updateStylingMap( if (dirty) { setContextDirty(context, true); } + + if (playerBuildersAreDirty) { + setContextPlayersDirty(context, true); + } } /** * Sets and resolves a single styling property/value on the provided `StylingContext` so - * that they can be applied to the element once `renderStyling` is called. + * that they can be applied to the element once `renderStyleAndClassBindings` is called. * * Note that prop-level styling values are considered higher priority than any styling that * has been applied using `updateStylingMap`, therefore, when styling values are rendered @@ -318,13 +378,34 @@ export function updateStylingMap( * @param value The CSS style value that will be assigned */ export function updateStyleProp( - context: StylingContext, index: number, value: string | boolean | null): void { + context: StylingContext, index: number, + input: string | boolean | null | BoundPlayerFactory): void { const singleIndex = StylingIndex.SingleStylesStartPosition + index * StylingIndex.Size; const currValue = getValue(context, singleIndex); const currFlag = getPointers(context, singleIndex); + const value: string|boolean|null = (input instanceof BoundPlayerFactory) ? input.value : input; // didn't change ... nothing to make a note of if (hasValueChanged(currFlag, currValue, value)) { + const isClassBased = (currFlag & StylingFlags.Class) === StylingFlags.Class; + const element = context[StylingIndex.ElementPosition] !as HTMLElement; + const playerBuilder = input instanceof BoundPlayerFactory ? + new ClassAndStylePlayerBuilder( + input as any, element, isClassBased ? BindingType.Class : BindingType.Style) : + null; + const value = (playerBuilder ? (input as BoundPlayerFactory).value : input) as string | + boolean | null; + const currPlayerIndex = getPlayerBuilderIndex(context, singleIndex); + + let playerBuildersAreDirty = false; + let playerBuilderIndex = playerBuilder ? currPlayerIndex : 0; + if (hasPlayerBuilderChanged(context, playerBuilder, currPlayerIndex)) { + const newIndex = setPlayerBuilder(context, playerBuilder, currPlayerIndex); + playerBuilderIndex = playerBuilder ? newIndex : 0; + setPlayerBuilderIndex(context, singleIndex, playerBuilderIndex); + playerBuildersAreDirty = true; + } + // the value will always get updated (even if the dirty flag is skipped) setValue(context, singleIndex, value); const indexForMulti = getMultiOrSingleIndex(currFlag); @@ -335,8 +416,6 @@ export function updateStyleProp( let multiDirty = false; let singleDirty = true; - const isClassBased = (currFlag & StylingFlags.Class) === StylingFlags.Class; - // only when the value is set to `null` should the multi-value get flagged if (!valueExists(value, isClassBased) && valueExists(valueForMulti, isClassBased)) { multiDirty = true; @@ -347,6 +426,10 @@ export function updateStyleProp( setDirty(context, singleIndex, singleDirty); setContextDirty(context, true); } + + if (playerBuildersAreDirty) { + setContextPlayersDirty(context, true); + } } } @@ -360,7 +443,8 @@ export function updateStyleProp( * @param addOrRemove Whether or not to add or remove the CSS class */ export function updateClassProp( - context: StylingContext, index: number, addOrRemove: boolean): void { + context: StylingContext, index: number, + addOrRemove: boolean | BoundPlayerFactory): void { const adjustedIndex = index + context[StylingIndex.ClassOffsetPosition]; updateStyleProp(context, adjustedIndex, addOrRemove); } @@ -378,15 +462,19 @@ export function updateClassProp( * @param context The styling context that will be used to determine * what styles will be rendered * @param renderer the renderer that will be used to apply the styling - * @param styleStore if provided, the updated style values will be applied + * @param classesStore if provided, the updated class values will be applied * to this key/value map instead of being renderered via the renderer. - * @param classStore if provided, the updated class values will be applied + * @param stylesStore if provided, the updated style values will be applied * to this key/value map instead of being renderered via the renderer. + * @returns number the total amount of players that got queued for animation (if any) */ -export function renderStyling( - context: StylingContext, renderer: Renderer3, styleStore?: {[key: string]: any}, - classStore?: {[key: string]: boolean}) { +export function renderStyleAndClassBindings( + context: StylingContext, renderer: Renderer3, rootOrView: RootContext | LViewData, + classesStore?: BindingStore | null, stylesStore?: BindingStore | null): number { + let totalPlayersQueued = 0; if (isContextDirty(context)) { + const flushPlayerBuilders: any = + context[StylingIndex.MasterFlagPosition] & StylingFlags.PlayerBuildersDirty; const native = context[StylingIndex.ElementPosition] !; const multiStartIndex = getMultiStartIndex(context); const styleSanitizer = getStyleSanitizer(context); @@ -397,6 +485,7 @@ export function renderStyling( const prop = getProp(context, i); const value = getValue(context, i); const flag = getPointers(context, i); + const playerBuilder = getPlayerBuilder(context, i); const isClassBased = flag & StylingFlags.Class ? true : false; const isInSingleRegion = i < multiStartIndex; @@ -422,17 +511,52 @@ export function renderStyling( } if (isClassBased) { - setClass(native, prop, valueToApply ? true : false, renderer, classStore); + setClass( + native, prop, valueToApply ? true : false, renderer, classesStore, playerBuilder); } else { const sanitizer = (flag & StylingFlags.Sanitize) ? styleSanitizer : null; - setStyle(native, prop, valueToApply as string | null, renderer, sanitizer, styleStore); + setStyle( + native, prop, valueToApply as string | null, renderer, sanitizer, stylesStore, + playerBuilder); } setDirty(context, i, false); } } + 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); + 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; } /** @@ -449,10 +573,16 @@ export function renderStyling( */ function setStyle( native: any, prop: string, value: string | null, renderer: Renderer3, - sanitizer: StyleSanitizeFn | null, store?: {[key: string]: any}) { + sanitizer: StyleSanitizeFn | null, store?: BindingStore | null, + playerBuilder?: ClassAndStylePlayerBuilder| null) { value = sanitizer && value ? sanitizer(prop, value) : value; - if (store) { - store[prop] = value; + if (store || playerBuilder) { + if (store) { + store.setValue(prop, value); + } + if (playerBuilder) { + playerBuilder.setValue(prop, value); + } } else if (value) { ngDevMode && ngDevMode.rendererSetStyle++; isProceduralRenderer(renderer) ? @@ -479,10 +609,15 @@ function setStyle( * @param store an optional key/value map that will be used as a context to render styles on */ function setClass( - native: any, className: string, add: boolean, renderer: Renderer3, - store?: {[key: string]: boolean}) { - if (store) { - store[className] = add; + native: any, className: string, add: boolean, renderer: Renderer3, store?: BindingStore | null, + playerBuilder?: ClassAndStylePlayerBuilder| null) { + if (store || playerBuilder) { + if (store) { + store.setValue(className, add); + } + if (playerBuilder) { + playerBuilder.setValue(className, add); + } } else if (add) { ngDevMode && ngDevMode.rendererAddClass++; isProceduralRenderer(renderer) ? renderer.addClass(native, className) : @@ -558,6 +693,54 @@ function setValue(context: StylingContext, index: number, value: string | null | context[index + StylingIndex.ValueOffset] = value; } +function hasPlayerBuilderChanged( + context: StylingContext, builder: ClassAndStylePlayerBuilder| null, index: number) { + const playerContext = context[StylingIndex.PlayerContext] !; + if (builder) { + if (!playerContext || index === 0) { + return true; + } + } else if (!playerContext) { + return false; + } + return playerContext[index] !== builder; +} + +function setPlayerBuilder( + context: StylingContext, builder: ClassAndStylePlayerBuilder| null, + insertionIndex: number): number { + let playerContext = context[StylingIndex.PlayerContext] || allocPlayerContext(context); + if (insertionIndex > 0) { + playerContext[insertionIndex] = builder; + } else { + insertionIndex = playerContext[PlayerIndex.NonBuilderPlayersStart]; + playerContext.splice(insertionIndex, 0, builder, null); + playerContext[PlayerIndex.NonBuilderPlayersStart] += + PlayerIndex.PlayerAndPlayerBuildersTupleSize; + } + return insertionIndex; +} + +function setPlayerBuilderIndex(context: StylingContext, index: number, playerBuilderIndex: number) { + context[index + StylingIndex.PlayerBuilderIndexOffset] = playerBuilderIndex; +} + +function getPlayerBuilderIndex(context: StylingContext, index: number): number { + return (context[index + StylingIndex.PlayerBuilderIndexOffset] as number) || 0; +} + +function getPlayerBuilder(context: StylingContext, index: number): ClassAndStylePlayerBuilder| + null { + const playerBuilderIndex = getPlayerBuilderIndex(context, index); + if (playerBuilderIndex) { + const playerContext = context[StylingIndex.PlayerContext]; + if (playerContext) { + return playerContext[playerBuilderIndex] as ClassAndStylePlayerBuilder| null; + } + } + return null; +} + function setFlag(context: StylingContext, index: number, flag: number) { const adjustedIndex = index === StylingIndex.MasterFlagPosition ? index : (index + StylingIndex.FlagsOffset); @@ -586,6 +769,14 @@ export function setContextDirty(context: StylingContext, isDirtyYes: boolean): v setDirty(context, StylingIndex.MasterFlagPosition, isDirtyYes); } +export function setContextPlayersDirty(context: StylingContext, isDirtyYes: boolean): void { + if (isDirtyYes) { + (context[StylingIndex.MasterFlagPosition] as number) |= StylingFlags.PlayerBuildersDirty; + } else { + (context[StylingIndex.MasterFlagPosition] as number) &= ~StylingFlags.PlayerBuildersDirty; + } +} + function findEntryPositionByProp( context: StylingContext, prop: string, startIndex?: number): number { for (let i = (startIndex || 0) + StylingIndex.PropertyOffset; i < context.length; @@ -602,6 +793,7 @@ function swapMultiContextEntries(context: StylingContext, indexA: number, indexB const tmpValue = getValue(context, indexA); const tmpProp = getProp(context, indexA); const tmpFlag = getPointers(context, indexA); + const tmpPlayerBuilderIndex = getPlayerBuilderIndex(context, indexA); let flagA = tmpFlag; let flagB = getPointers(context, indexB); @@ -623,10 +815,12 @@ function swapMultiContextEntries(context: StylingContext, indexA: number, indexB setValue(context, indexA, getValue(context, indexB)); setProp(context, indexA, getProp(context, indexB)); setFlag(context, indexA, getPointers(context, indexB)); + setPlayerBuilderIndex(context, indexA, getPlayerBuilderIndex(context, indexB)); setValue(context, indexB, tmpValue); setProp(context, indexB, tmpProp); setFlag(context, indexB, tmpFlag); + setPlayerBuilderIndex(context, indexB, tmpPlayerBuilderIndex); } function updateSinglePointerValues(context: StylingContext, indexStartPosition: number) { @@ -647,13 +841,13 @@ function updateSinglePointerValues(context: StylingContext, indexStartPosition: function insertNewMultiProperty( context: StylingContext, index: number, classBased: boolean, name: string, flag: number, - value: string | boolean): void { + value: string | boolean, playerIndex: number): void { const doShift = index < context.length; // prop does not exist in the list, add it in context.splice( index, 0, flag | StylingFlags.Dirty | (classBased ? StylingFlags.Class : StylingFlags.None), - name, value); + name, value, playerIndex); if (doShift) { // because the value was inserted midway into the array then we @@ -696,3 +890,35 @@ function hasValueChanged( // everything else is safe to check with a normal equality check return a !== b; } + +export class ClassAndStylePlayerBuilder implements PlayerBuilder { + private _values: {[key: string]: string | null} = {}; + private _dirty = false; + private _factory: BoundPlayerFactory; + + constructor(factory: PlayerFactory, private _element: HTMLElement, private _type: BindingType) { + this._factory = factory as any; + } + + setValue(prop: string, value: any) { + if (this._values[prop] !== value) { + this._values[prop] = value; + this._dirty = true; + } + } + + buildPlayer(currentPlayer?: Player|null): Player|undefined|null { + // if no values have been set here then this means the binding didn't + // change and therefore the binding values were not updated through + // `setValue` which means no new player will be provided. + if (this._dirty) { + const player = + this._factory.fn(this._element, this._type, this._values !, currentPlayer || null); + this._values = {}; + this._dirty = false; + return player; + } + + return undefined; + } +} diff --git a/packages/core/src/render3/styling/core_player_handler.ts b/packages/core/src/render3/styling/core_player_handler.ts index 36866dda90..135327fc4f 100644 --- a/packages/core/src/render3/styling/core_player_handler.ts +++ b/packages/core/src/render3/styling/core_player_handler.ts @@ -5,7 +5,7 @@ * 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 {Player, PlayerHandler} from '../interfaces/player'; +import {PlayState, Player, PlayerHandler} from '../interfaces/player'; export class CorePlayerHandler implements PlayerHandler { private _players: Player[] = []; @@ -13,7 +13,7 @@ export class CorePlayerHandler implements PlayerHandler { flushPlayers() { for (let i = 0; i < this._players.length; i++) { const player = this._players[i]; - if (!player.parent) { + if (!player.parent && player.state === PlayState.Pending) { player.play(); } } diff --git a/packages/core/src/render3/styling/player_factory.ts b/packages/core/src/render3/styling/player_factory.ts new file mode 100644 index 0000000000..dac6daf9bc --- /dev/null +++ b/packages/core/src/render3/styling/player_factory.ts @@ -0,0 +1,33 @@ +/** + * @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 {PlayerFactory, PlayerFactoryBuildFn} from '../interfaces/player'; + +/** + * Combines the binding value and a factory for an animation player. + * + * Used to bind a player to an element template binding (currently only + * `[style]`, `[style.prop]`, `[class]` and `[class.name]` bindings + * supported). The provided `factoryFn` function will be run once all + * the associated bindings have been evaluated on the element and is + * designed to return a player which will then be placed on the element. + * + * @param factoryFn The function that is used to create a player + * once all the rendering-related (styling values) have been + * processed for the element binding. + * @param value The raw value that will be exposed to the binding + * so that the binding can update its internal values when + * any changes are evaluated. + */ +export function bindPlayerFactory(factoryFn: PlayerFactoryBuildFn, value: T): PlayerFactory { + return new BoundPlayerFactory(factoryFn, value) as any; +} + +export class BoundPlayerFactory { + '__brand__': 'Brand for PlayerFactory that nothing will match'; + constructor(public fn: PlayerFactoryBuildFn, public value: T) {} +} diff --git a/packages/core/src/render3/styling/util.ts b/packages/core/src/render3/styling/util.ts index 19d86e2434..0d6909e569 100644 --- a/packages/core/src/render3/styling/util.ts +++ b/packages/core/src/render3/styling/util.ts @@ -5,19 +5,19 @@ * 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 '../ng_dev_mode'; import {StyleSanitizeFn} from '../../sanitization/style_sanitizer'; import {getContext} from '../context_discovery'; import {ACTIVE_INDEX, LContainer} from '../interfaces/container'; import {LContext} from '../interfaces/context'; -import {PlayerContext} from '../interfaces/player'; +import {PlayState, Player, PlayerContext, PlayerIndex} from '../interfaces/player'; import {RElement} from '../interfaces/renderer'; import {InitialStyles, StylingContext, StylingIndex} from '../interfaces/styling'; -import {FLAGS, HEADER_OFFSET, HOST, LViewData} from '../interfaces/view'; +import {FLAGS, HEADER_OFFSET, HOST, LViewData, RootContext} from '../interfaces/view'; import {getTNode} from '../util'; -export const EMPTY_ARR: any[] = []; -export const EMPTY_OBJ: {[key: string]: any} = {}; +import {CorePlayerHandler} from './core_player_handler'; export function createEmptyStylingContext( element?: RElement | null, sanitizer?: StyleSanitizeFn | null, @@ -75,7 +75,10 @@ export function getStylingContext(index: number, viewData: LViewData): StylingCo // This is an LViewData or an LContainer const stylingTemplate = getTNode(index, viewData).stylingTemplate; - if (wrapper !== viewData) storageIndex = HOST; + if (wrapper !== viewData) { + storageIndex = HOST; + } + return wrapper[storageIndex] = stylingTemplate ? allocStylingContext(slotValue, stylingTemplate) : createEmptyStylingContext(slotValue); @@ -87,18 +90,88 @@ function isStylingContext(value: LViewData | LContainer | StylingContext) { return typeof value[FLAGS] !== 'number' && typeof value[ACTIVE_INDEX] !== 'number'; } -export function getOrCreatePlayerContext(target: {}, context?: LContext | null): PlayerContext { +export function addPlayerInternal( + playerContext: PlayerContext, rootContext: RootContext, element: HTMLElement, + player: Player | null, playerContextIndex: number, ref?: any): boolean { + ref = ref || element; + if (playerContextIndex) { + playerContext[playerContextIndex] = player; + } else { + playerContext.push(player); + } + + if (player) { + player.addEventListener(PlayState.Destroyed, () => { + const index = playerContext.indexOf(player); + const nonFactoryPlayerIndex = playerContext[PlayerIndex.NonBuilderPlayersStart]; + + // if the player is being removed from the factory side of the context + // (which is where the [style] and [class] bindings do their thing) then + // that side of the array cannot be resized since the respective bindings + // have pointer index values that point to the associated factory instance + if (index) { + if (index < nonFactoryPlayerIndex) { + playerContext[index] = null; + } else { + playerContext.splice(index, 1); + } + } + player.destroy(); + }); + + const playerHandler = + rootContext.playerHandler || (rootContext.playerHandler = new CorePlayerHandler()); + playerHandler.queuePlayer(player, ref); + return true; + } + + return false; +} + +export function getPlayersInternal(playerContext: PlayerContext): Player[] { + const players: Player[] = []; + const nonFactoryPlayersStart = playerContext[PlayerIndex.NonBuilderPlayersStart]; + + // add all factory-based players (which are apart of [style] and [class] bindings) + for (let i = PlayerIndex.PlayerBuildersStartPosition + PlayerIndex.PlayerOffsetPosition; + i < nonFactoryPlayersStart; i += PlayerIndex.PlayerAndPlayerBuildersTupleSize) { + const player = playerContext[i] as Player | null; + if (player) { + players.push(player); + } + } + + // add all custom players (not apart of [style] and [class] bindings) + for (let i = nonFactoryPlayersStart; i < playerContext.length; i++) { + players.push(playerContext[i] as Player); + } + + return players; +} + + +export function getOrCreatePlayerContext(target: {}, context?: LContext | null): PlayerContext| + null { context = context || getContext(target) !; - if (ngDevMode && !context) { - throw new Error( - 'Only elements that exist in an Angular application can be used for player access'); + if (!context) { + ngDevMode && throwInvalidRefError(); + return null; } const {lViewData, nodeIndex} = context; const stylingContext = getStylingContext(nodeIndex - HEADER_OFFSET, lViewData); - return stylingContext[StylingIndex.PlayerContext] || allocPlayerContext(stylingContext); + return getPlayerContext(stylingContext) || allocPlayerContext(stylingContext); } -function allocPlayerContext(data: StylingContext): PlayerContext { - return data[StylingIndex.PlayerContext] = []; +export function getPlayerContext(stylingContext: StylingContext): PlayerContext|null { + return stylingContext[StylingIndex.PlayerContext]; +} + +export function allocPlayerContext(data: StylingContext): PlayerContext { + return data[StylingIndex.PlayerContext] = + [PlayerIndex.SinglePlayerBuildersStartPosition, null, null, null, null]; +} + +export function throwInvalidRefError() { + throw new Error('Only elements that exist in an Angular application can be used for animations'); } diff --git a/packages/core/test/bundling/animation_world/BUILD.bazel b/packages/core/test/bundling/animation_world/BUILD.bazel index aff48487e9..8a2bb98da1 100644 --- a/packages/core/test/bundling/animation_world/BUILD.bazel +++ b/packages/core/test/bundling/animation_world/BUILD.bazel @@ -8,6 +8,7 @@ ng_module( name = "animation_world", srcs = ["index.ts"], tags = ["ivy-only"], + type_check = False, # see #26462 deps = [ "//packages/common", "//packages/core", diff --git a/packages/core/test/bundling/animation_world/bundle.golden_symbols.json b/packages/core/test/bundling/animation_world/bundle.golden_symbols.json index b56e86b8af..da907b93dd 100644 --- a/packages/core/test/bundling/animation_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/animation_world/bundle.golden_symbols.json @@ -6,7 +6,7 @@ "name": "AnimationWorldComponent" }, { - "name": "AnimationWorldComponent_div_Template_4" + "name": "AnimationWorldComponent_div_Template_6" }, { "name": "BINDING_INDEX" @@ -14,6 +14,9 @@ { "name": "BLOOM_MASK" }, + { + "name": "BoundPlayerFactory" + }, { "name": "CIRCULAR$1" }, @@ -32,6 +35,9 @@ { "name": "ChangeDetectionStrategy" }, + { + "name": "ClassAndStylePlayerBuilder" + }, { "name": "CorePlayerHandler" }, @@ -212,6 +218,9 @@ { "name": "ViewRef" }, + { + "name": "WebAnimationsPlayer" + }, { "name": "_CLEAN_PROMISE" }, @@ -251,6 +260,9 @@ { "name": "_c3" }, + { + "name": "_c4" + }, { "name": "_currentInjector" }, @@ -278,6 +290,9 @@ { "name": "addPlayer" }, + { + "name": "addPlayerInternal" + }, { "name": "addRemoveViewFromContainer" }, @@ -290,6 +305,9 @@ { "name": "allocStylingContext" }, + { + "name": "animateStyleFactory" + }, { "name": "appendChild" }, @@ -302,6 +320,9 @@ { "name": "bind" }, + { + "name": "bindPlayerFactory" + }, { "name": "bindingRootIndex" }, @@ -644,6 +665,15 @@ { "name": "getPipeDef" }, + { + "name": "getPlayerBuilder" + }, + { + "name": "getPlayerBuilderIndex" + }, + { + "name": "getPlayerContext" + }, { "name": "getPointers" }, @@ -671,9 +701,15 @@ { "name": "getRootContext" }, + { + "name": "getRootContext$2" + }, { "name": "getRootView" }, + { + "name": "getRootView$1" + }, { "name": "getStyleSanitizer" }, @@ -698,6 +734,9 @@ { "name": "getValue" }, + { + "name": "hasPlayerBuilderChanged" + }, { "name": "hasValueChanged" }, @@ -797,6 +836,9 @@ { "name": "listener" }, + { + "name": "loadContext" + }, { "name": "locateHostElement" }, @@ -809,6 +851,9 @@ { "name": "makeParamDecorator" }, + { + "name": "markDirty" + }, { "name": "markDirtyIfOnPush" }, @@ -900,7 +945,7 @@ "name": "renderEmbeddedTemplate" }, { - "name": "renderStyling" + "name": "renderStyleAndClassBindings" }, { "name": "resetComponentState" @@ -932,6 +977,9 @@ { "name": "setContextDirty" }, + { + "name": "setContextPlayersDirty" + }, { "name": "setCurrentInjector" }, @@ -953,6 +1001,12 @@ { "name": "setInputsFromAttrs" }, + { + "name": "setPlayerBuilder" + }, + { + "name": "setPlayerBuilderIndex" + }, { "name": "setProp" }, diff --git a/packages/core/test/bundling/animation_world/index.ts b/packages/core/test/bundling/animation_world/index.ts index 300337232e..01c07774e1 100644 --- a/packages/core/test/bundling/animation_world/index.ts +++ b/packages/core/test/bundling/animation_world/index.ts @@ -9,16 +9,19 @@ import '@angular/core/test/bundling/util/src/reflect_metadata'; import {CommonModule} from '@angular/common'; -import {Component, ElementRef, NgModule, ɵPlayState as PlayState, ɵPlayer as Player, ɵPlayerHandler as PlayerHandler, ɵaddPlayer as addPlayer, ɵrenderComponent as renderComponent} from '@angular/core'; +import {Component, ElementRef, NgModule, ɵPlayState as PlayState, ɵPlayer as Player, ɵPlayerHandler as PlayerHandler, ɵaddPlayer as addPlayer, ɵbindPlayerFactory as bindPlayerFactory, ɵmarkDirty as markDirty, ɵrenderComponent as renderComponent} from '@angular/core'; @Component({ selector: 'animation-world', template: `
-
+
{{ item }}
@@ -27,12 +30,18 @@ import {Component, ElementRef, NgModule, ɵPlayState as PlayState, ɵPlayer as P class AnimationWorldComponent { items: any[] = [1, 2, 3, 4, 5, 6, 7, 8, 9]; private _hostElement: HTMLElement; + public styles: {[key: string]: any}|null = null; constructor(element: ElementRef) { this._hostElement = element.nativeElement; } makeClass(index: number) { return `record-${index}`; } - doAnimate() { + animateWithStyles() { + this.styles = animateStyleFactory([{opacity: 0}, {opacity: 1}], 300, 'ease-out'); + markDirty(this); + } + + animateWithCustomPlayer() { const elements = this._hostElement.querySelectorAll('div.record') as any as HTMLElement[]; for (let i = 0; i < elements.length; i++) { const element = elements[i]; @@ -55,13 +64,13 @@ function buildAnimationPlayer(element: HTMLElement, animationName: string, time: class SimpleKeyframePlayer implements Player { state = PlayState.Pending; parent: Player|null = null; - private _animationStyle: string; + private _animationStyle: string = ''; private _listeners: {[stateName: string]: (() => any)[]} = {}; constructor(private _element: HTMLElement, private _animationName: string, time: string) { this._animationStyle = `${time} ${_animationName}`; } private _start() { - this._element.style.animation = this._animationStyle; + (this._element as any).style.animation = this._animationStyle; const animationFn = (event: AnimationEvent) => { if (event.animationName == this._animationName) { this._element.removeEventListener('animationend', animationFn); @@ -134,3 +143,66 @@ class AnimationDebugger implements PlayerHandler { const playerHandler = new AnimationDebugger(); renderComponent(AnimationWorldComponent, {playerHandler}); + +function animateStyleFactory(keyframes: any[], duration: number, easing: string) { + const limit = keyframes.length - 1; + const finalKeyframe = keyframes[limit]; + return bindPlayerFactory((element: HTMLElement, type: number, values: {[key: string]: any}) => { + const kf = keyframes.slice(0, limit); + kf.push(values); + return new WebAnimationsPlayer(element, keyframes, duration, easing); + }, finalKeyframe); +} + +class WebAnimationsPlayer implements Player { + state = PlayState.Pending; + parent: Player|null = null; + private _listeners: {[stateName: string]: (() => any)[]} = {}; + constructor( + private _element: HTMLElement, private _keyframes: {[key: string]: any}[], + private _duration: number, private _easing: string) {} + private _start() { + const player = this._element.animate( + this._keyframes as any[], {duration: this._duration, easing: this._easing, fill: 'both'}); + player.addEventListener('finish', e => { this.finish(); }); + } + addEventListener(state: PlayState|string, cb: () => any): void { + const key = state.toString(); + const arr = this._listeners[key] = (this._listeners[key] || []); + arr.push(cb); + } + play(): void { + if (this.state <= PlayState.Pending) { + this._start(); + } + if (this.state != PlayState.Running) { + this.state = PlayState.Running; + this._emit(this.state); + } + } + pause(): void { + if (this.state != PlayState.Paused) { + this.state = PlayState.Paused; + this._emit(this.state); + } + } + finish(): void { + if (this.state < PlayState.Finished) { + this._element.style.animation = ''; + this.state = PlayState.Finished; + this._emit(this.state); + } + } + destroy(): void { + if (this.state < PlayState.Destroyed) { + this.finish(); + this.state = PlayState.Destroyed; + this._emit(this.state); + } + } + capture(): any {} + private _emit(state: PlayState) { + const arr = this._listeners[state.toString()] || []; + arr.forEach(cb => cb()); + } +} diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 67a3688d3d..af5f2b5c29 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -8,6 +8,9 @@ { "name": "BLOOM_MASK" }, + { + "name": "BoundPlayerFactory" + }, { "name": "CIRCULAR$1" }, @@ -26,6 +29,12 @@ { "name": "ChangeDetectionStrategy" }, + { + "name": "ClassAndStylePlayerBuilder" + }, + { + "name": "CorePlayerHandler" + }, { "name": "DECLARATION_VIEW" }, @@ -344,12 +353,18 @@ { "name": "addComponentLogic" }, + { + "name": "addPlayerInternal" + }, { "name": "addRemoveViewFromContainer" }, { "name": "addToViewTree" }, + { + "name": "allocPlayerContext" + }, { "name": "allocStylingContext" }, @@ -686,6 +701,15 @@ { "name": "getPipeDef" }, + { + "name": "getPlayerBuilder" + }, + { + "name": "getPlayerBuilderIndex" + }, + { + "name": "getPlayerContext" + }, { "name": "getPointers" }, @@ -710,6 +734,12 @@ { "name": "getRendererFactory" }, + { + "name": "getRootContext" + }, + { + "name": "getRootView" + }, { "name": "getStyleSanitizer" }, @@ -734,6 +764,9 @@ { "name": "getValue" }, + { + "name": "hasPlayerBuilderChanged" + }, { "name": "hasValueChanged" }, @@ -930,7 +963,7 @@ "name": "renderEmbeddedTemplate" }, { - "name": "renderStyling" + "name": "renderStyleAndClassBindings" }, { "name": "resetComponentState" @@ -962,6 +995,9 @@ { "name": "setContextDirty" }, + { + "name": "setContextPlayersDirty" + }, { "name": "setCurrentInjector" }, @@ -983,6 +1019,12 @@ { "name": "setInputsFromAttrs" }, + { + "name": "setPlayerBuilder" + }, + { + "name": "setPlayerBuilderIndex" + }, { "name": "setProp" }, diff --git a/packages/core/test/bundling/todo_r2/bundle.golden_symbols.json b/packages/core/test/bundling/todo_r2/bundle.golden_symbols.json index 311d1be5df..fdef7a16eb 100644 --- a/packages/core/test/bundling/todo_r2/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo_r2/bundle.golden_symbols.json @@ -59,6 +59,9 @@ { "name": "BROWSER_SANITIZATION_PROVIDERS" }, + { + "name": "BoundPlayerFactory" + }, { "name": "BrowserDomAdapter" }, @@ -128,6 +131,9 @@ { "name": "ChangeDetectorRef" }, + { + "name": "ClassAndStylePlayerBuilder" + }, { "name": "CommonModule" }, @@ -164,6 +170,9 @@ { "name": "Console" }, + { + "name": "CorePlayerHandler" + }, { "name": "CurrencyPipe" }, @@ -1169,6 +1178,9 @@ { "name": "addDateMinutes" }, + { + "name": "addPlayerInternal" + }, { "name": "addRemoveViewFromContainer" }, @@ -1178,6 +1190,9 @@ { "name": "adjustBlueprintForNewNode" }, + { + "name": "allocPlayerContext" + }, { "name": "allocStylingContext" }, @@ -1793,6 +1808,15 @@ { "name": "getPlatform" }, + { + "name": "getPlayerBuilder" + }, + { + "name": "getPlayerBuilderIndex" + }, + { + "name": "getPlayerContext" + }, { "name": "getPluralCategory" }, @@ -1823,6 +1847,12 @@ { "name": "getRendererFactory" }, + { + "name": "getRootContext" + }, + { + "name": "getRootView" + }, { "name": "getStyleSanitizer" }, @@ -1868,6 +1898,9 @@ { "name": "hasOnDestroy" }, + { + "name": "hasPlayerBuilderChanged" + }, { "name": "hasValueChanged" }, @@ -2262,7 +2295,7 @@ "name": "renderEmbeddedTemplate" }, { - "name": "renderStyling" + "name": "renderStyleAndClassBindings" }, { "name": "resetComponentState" @@ -2315,6 +2348,9 @@ { "name": "setContextDirty" }, + { + "name": "setContextPlayersDirty" + }, { "name": "setCurrentInjector" }, @@ -2336,6 +2372,12 @@ { "name": "setInputsFromAttrs" }, + { + "name": "setPlayerBuilder" + }, + { + "name": "setPlayerBuilderIndex" + }, { "name": "setProp" }, 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 c0b4e445b8..65af1babf7 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 @@ -5,19 +5,38 @@ * 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 {elementEnd, elementStart, elementStyleProp, elementStyling, elementStylingApply, elementStylingMap} from '../../../src/render3/instructions'; +import {createRootContext} from '../../../src/render3/component'; +import {getContext} from '../../../src/render3/context_discovery'; +import {defineComponent} from '../../../src/render3/index'; +import {createLViewData, createTView, elementClassProp, elementEnd, elementStart, elementStyleProp, elementStyling, elementStylingApply, elementStylingMap} from '../../../src/render3/instructions'; import {InitialStylingFlags, RenderFlags} from '../../../src/render3/interfaces/definition'; -import {RElement, Renderer3} from '../../../src/render3/interfaces/renderer'; +import {BindingStore, BindingType, PlayState, Player, PlayerFactory, PlayerHandler} from '../../../src/render3/interfaces/player'; +import {RElement, Renderer3, domRendererFactory3} from '../../../src/render3/interfaces/renderer'; import {StylingContext, StylingFlags, StylingIndex} from '../../../src/render3/interfaces/styling'; -import {createStylingContextTemplate, isContextDirty, renderStyling as _renderStyling, setContextDirty, updateClassProp, updateStyleProp, updateStylingMap} from '../../../src/render3/styling/class_and_style_bindings'; +import {CONTEXT, LViewData, LViewFlags, RootContext} from '../../../src/render3/interfaces/view'; +import {addPlayer, getPlayers} from '../../../src/render3/players'; +import {ClassAndStylePlayerBuilder, createStylingContextTemplate, isContextDirty, renderStyleAndClassBindings as _renderStyling, setContextDirty, updateClassProp, updateStyleProp, updateStylingMap} from '../../../src/render3/styling/class_and_style_bindings'; +import {CorePlayerHandler} from '../../../src/render3/styling/core_player_handler'; +import {BoundPlayerFactory, bindPlayerFactory} from '../../../src/render3/styling/player_factory'; import {allocStylingContext} from '../../../src/render3/styling/util'; import {defaultStyleSanitizer} from '../../../src/sanitization/sanitization'; import {StyleSanitizeFn} from '../../../src/sanitization/style_sanitizer'; -import {renderToHtml} from '../render_util'; +import {ComponentFixture, renderToHtml} from '../render_util'; -describe('styling', () => { +import {MockPlayer} from './mock_player'; + +describe('style and class based bindings', () => { let element: RElement|null = null; - beforeEach(() => { element = {} as any; }); + beforeEach(() => { element = document.createElement('div') as any; }); + + function createMockViewData(playerHandler: PlayerHandler, context: StylingContext): LViewData { + const rootContext = + createRootContext(requestAnimationFrame.bind(window), playerHandler || null); + const lViewData = createLViewData( + domRendererFactory3.createRenderer(element, null), + createTView(-1, null, 1, 0, null, null, null), rootContext, LViewFlags.IsRoot); + return lViewData; + } function initContext( styles?: (number | string)[] | null, classes?: (string | number | boolean)[] | null, @@ -25,34 +44,51 @@ describe('styling', () => { return allocStylingContext(element, createStylingContextTemplate(classes, styles, sanitizer)); } - function renderStyles(context: StylingContext, renderer?: Renderer3) { - const styles: {[key: string]: any} = {}; - _renderStyling(context, (renderer || {}) as Renderer3, styles); - return styles; + function getRootContextInternal(lViewData: LViewData) { + return lViewData[CONTEXT] as RootContext; + } + + function renderStyles(context: StylingContext, renderer?: Renderer3, lViewData?: LViewData) { + const store = new MockStylingStore(element as HTMLElement, BindingType.Style); + const handler = new CorePlayerHandler(); + _renderStyling( + context, (renderer || {}) as Renderer3, + getRootContextInternal(lViewData || createMockViewData(handler, context)), null, store); + return store.getValues(); } function trackStylesFactory() { - const styles: {[key: string]: any} = {}; + const store = new MockStylingStore(element as HTMLElement, BindingType.Style); + const handler = new CorePlayerHandler(); return function(context: StylingContext, renderer?: Renderer3): {[key: string]: any} { - _renderStyling(context, (renderer || {}) as Renderer3, styles); - return styles; + const lViewData = createMockViewData(handler, context); + _renderStyling( + context, (renderer || {}) as Renderer3, getRootContextInternal(lViewData), null, store); + return store.getValues(); }; } function trackClassesFactory() { - const classes: {[className: string]: boolean} = {}; + const store = new MockStylingStore(element as HTMLElement, BindingType.Class); + const handler = new CorePlayerHandler(); return function(context: StylingContext, renderer?: Renderer3): {[key: string]: any} { - _renderStyling(context, (renderer || {}) as Renderer3, {}, classes); - return classes; + const lViewData = createMockViewData(handler, context); + _renderStyling( + context, (renderer || {}) as Renderer3, getRootContextInternal(lViewData), store); + return store.getValues(); }; } function trackStylesAndClasses() { - const classes: {[className: string]: boolean} = {}; - const styles: {[prop: string]: any} = {}; + const classStore = new MockStylingStore(element as HTMLElement, BindingType.Class); + const styleStore = new MockStylingStore(element as HTMLElement, BindingType.Style); + const handler = new CorePlayerHandler(); return function(context: StylingContext, renderer?: Renderer3): {[key: string]: any} { - _renderStyling(context, (renderer || {}) as Renderer3, styles, classes); - return [styles, classes]; + const lViewData = createMockViewData(handler, context); + _renderStyling( + context, (renderer || {}) as Renderer3, getRootContextInternal(lViewData), classStore, + styleStore); + return [classStore.getValues(), styleStore.getValues()]; }; } @@ -105,6 +141,13 @@ describe('styling', () => { function dirtyClass(a: number, b: number) { return _dirty(a, b, true); } + function makePlayerBuilder( + factory: PlayerFactory, isClassBased?: boolean, elm?: HTMLElement) { + return new ClassAndStylePlayerBuilder( + factory, (elm || element) as HTMLElement, + isClassBased ? BindingType.Class : BindingType.Style); + } + describe('styles', () => { describe('createStylingContextTemplate', () => { it('should initialize empty template', () => { @@ -119,31 +162,35 @@ describe('styling', () => { null, null, [null, 'red', '10px'], - dirtyStyle(0, 14), // + dirtyStyle(0, 16), // 0, element, null, null, // #8 - cleanStyle(1, 14), + cleanStyle(1, 16), 'color', null, + 0, - // #11 - cleanStyle(2, 17), + // #12 + cleanStyle(2, 20), 'width', null, + 0, - // #14 + // #16 dirtyStyle(1, 8), 'color', null, + 0, - // #17 - dirtyStyle(2, 11), + // #20 + dirtyStyle(2, 12), 'width', null, + 0, ]); }); }); @@ -323,31 +370,35 @@ describe('styling', () => { null, null, [null], - dirtyStyle(0, 14), // + dirtyStyle(0, 16), // 2, element, null, {width: '100px', height: '100px'}, // #8 - cleanStyle(0, 14), + cleanStyle(0, 16), 'width', null, + 0, - // #11 - cleanStyle(0, 17), + // #12 + cleanStyle(0, 20), 'height', null, + 0, - // #14 + // #16 dirtyStyle(0, 8), 'width', '100px', + 0, - // #17 - dirtyStyle(0, 11), + // #20 + dirtyStyle(0, 12), 'height', '100px', + 0, ]); getStyles(stylingContext); @@ -357,36 +408,41 @@ describe('styling', () => { null, null, [null], - dirtyStyle(0, 14), // + dirtyStyle(0, 16), // 2, element, null, {width: '200px', opacity: '0'}, // #8 - cleanStyle(0, 14), + cleanStyle(0, 16), 'width', null, + 0, - // #11 - cleanStyle(0, 20), + // #12 + cleanStyle(0, 24), 'height', null, + 0, - // #14 + // #16 dirtyStyle(0, 8), 'width', '200px', + 0, - // #17 + // #20 dirtyStyle(), 'opacity', '0', + 0, - // #20 - dirtyStyle(0, 11), + // #23 + dirtyStyle(0, 12), 'height', null, + 0, ]); getStyles(stylingContext); @@ -394,36 +450,41 @@ describe('styling', () => { null, null, [null], - cleanStyle(0, 14), // + cleanStyle(0, 16), // 2, element, null, {width: '200px', opacity: '0'}, // #8 - cleanStyle(0, 14), + cleanStyle(0, 16), 'width', null, + 0, - // #11 - cleanStyle(0, 20), + // #12 + cleanStyle(0, 24), 'height', null, + 0, - // #14 + // #16 cleanStyle(0, 8), 'width', '200px', + 0, - // #17 + // #20 cleanStyle(), 'opacity', '0', + 0, - // #20 - cleanStyle(0, 11), + // #23 + cleanStyle(0, 12), 'height', null, + 0, ]); updateStyles(stylingContext, {width: null}); @@ -433,36 +494,41 @@ describe('styling', () => { null, null, [null], - dirtyStyle(0, 14), // + dirtyStyle(0, 16), // 2, element, null, {width: null}, // #8 - dirtyStyle(0, 14), + dirtyStyle(0, 16), 'width', '300px', + 0, - // #11 - cleanStyle(0, 20), + // #12 + cleanStyle(0, 24), 'height', null, + 0, - // #14 + // #16 cleanStyle(0, 8), 'width', null, + 0, - // #17 + // #20 dirtyStyle(), 'opacity', null, + 0, - // #20 - cleanStyle(0, 11), + // #23 + cleanStyle(0, 12), 'height', null, + 0, ]); getStyles(stylingContext); @@ -472,36 +538,41 @@ describe('styling', () => { null, null, [null], - dirtyStyle(0, 14), // + dirtyStyle(0, 16), // 2, element, null, {width: null}, // #8 - dirtyStyle(0, 14), + dirtyStyle(0, 16), 'width', null, + 0, - // #11 - cleanStyle(0, 20), + // #12 + cleanStyle(0, 24), 'height', null, + 0, - // #14 + // #16 cleanStyle(0, 8), 'width', null, + 0, - // #17 + // #20 cleanStyle(), 'opacity', null, + 0, - // #20 - cleanStyle(0, 11), + // #23 + cleanStyle(0, 12), 'height', null, + 0, ]); }); @@ -516,36 +587,41 @@ describe('styling', () => { null, null, [null], - dirtyStyle(0, 11), // + dirtyStyle(0, 12), // 1, element, null, {width: '100px', height: '100px', opacity: '0.5'}, // #8 - cleanStyle(0, 20), + cleanStyle(0, 24), 'lineHeight', null, + 0, - // #11 + // #12 dirtyStyle(), 'width', '100px', + 0, - // #14 + // #16 dirtyStyle(), 'height', '100px', + 0, - // #17 + // #20 dirtyStyle(), 'opacity', '0.5', + 0, - // #20 + // #23 cleanStyle(0, 8), 'lineHeight', null, + 0, ]); getStyles(stylingContext); @@ -555,36 +631,41 @@ describe('styling', () => { null, null, [null], - dirtyStyle(0, 11), // + dirtyStyle(0, 12), // 1, element, null, {}, // #8 - cleanStyle(0, 20), + cleanStyle(0, 24), 'lineHeight', null, + 0, - // #11 + // #12 dirtyStyle(), 'width', null, + 0, - // #14 + // #16 dirtyStyle(), 'height', null, + 0, - // #17 + // #20 dirtyStyle(), 'opacity', null, + 0, - // #20 + // #23 cleanStyle(0, 8), 'lineHeight', null, + 0, ]); getStyles(stylingContext); @@ -596,41 +677,47 @@ describe('styling', () => { null, null, [null], - dirtyStyle(0, 11), // + dirtyStyle(0, 12), // 1, element, null, {borderWidth: '5px'}, // #8 - cleanStyle(0, 23), + cleanStyle(0, 28), 'lineHeight', null, + 0, - // #11 + // #12 dirtyStyle(), 'borderWidth', '5px', + 0, - // #14 + // #16 cleanStyle(), 'width', null, - - // #17 - cleanStyle(), - 'height', - null, + 0, // #20 cleanStyle(), - 'opacity', + 'height', null, + 0, // #23 + cleanStyle(), + 'opacity', + null, + 0, + + // #28 cleanStyle(0, 8), 'lineHeight', null, + 0, ]); updateStyleProp(stylingContext, 0, '200px'); @@ -639,41 +726,47 @@ describe('styling', () => { null, null, [null], - dirtyStyle(0, 11), // + dirtyStyle(0, 12), // 1, element, null, {borderWidth: '5px'}, // #8 - dirtyStyle(0, 23), + dirtyStyle(0, 28), 'lineHeight', '200px', + 0, - // #11 + // #12 dirtyStyle(), 'borderWidth', '5px', + 0, - // #14 + // #16 cleanStyle(), 'width', null, - - // #17 - cleanStyle(), - 'height', - null, + 0, // #20 cleanStyle(), - 'opacity', + 'height', null, + 0, // #23 + cleanStyle(), + 'opacity', + null, + 0, + + // #28 cleanStyle(0, 8), 'lineHeight', null, + 0, ]); updateStyles(stylingContext, {borderWidth: '15px', borderColor: 'red'}); @@ -682,46 +775,53 @@ describe('styling', () => { null, null, [null], - dirtyStyle(0, 11), // + dirtyStyle(0, 12), // 1, element, null, {borderWidth: '15px', borderColor: 'red'}, // #8 - dirtyStyle(0, 26), + dirtyStyle(0, 32), 'lineHeight', '200px', + 0, - // #11 + // #12 dirtyStyle(), 'borderWidth', '15px', + 0, - // #14 + // #16 dirtyStyle(), 'borderColor', 'red', - - // #17 - cleanStyle(), - 'width', - null, + 0, // #20 cleanStyle(), - 'height', + 'width', null, + 0, // #23 cleanStyle(), + 'height', + null, + 0, + + // #28 + cleanStyle(), 'opacity', null, + 0, - // #26 + // #32 cleanStyle(0, 8), 'lineHeight', null, + 0, ]); }); @@ -739,26 +839,29 @@ describe('styling', () => { null, null, [null], - dirtyStyle(0, 11), // + dirtyStyle(0, 12), // 1, element, null, {width: '100px'}, // #8 - dirtyStyle(0, 14), + dirtyStyle(0, 16), 'height', '200px', + 0, - // #8 + // #12 dirtyStyle(), 'width', '100px', + 0, - // #14 + // #16 cleanStyle(0, 8), 'height', null, + 0, ]); getStyles(stylingContext); @@ -767,26 +870,29 @@ describe('styling', () => { null, null, [null], - cleanStyle(0, 11), // + cleanStyle(0, 12), // 1, element, null, {width: '100px'}, // #8 - cleanStyle(0, 14), + cleanStyle(0, 16), 'height', '200px', + 0, - // #8 + // #12 cleanStyle(), 'width', '100px', + 0, - // #14 + // #16 cleanStyle(0, 8), 'height', null, + 0, ]); }); @@ -804,31 +910,35 @@ describe('styling', () => { null, styleSanitizer, [null], - dirtyStyle(0, 14), // + dirtyStyle(0, 16), // 2, element, null, null, // #8 - dirtyStyleWithSanitization(0, 14), + dirtyStyleWithSanitization(0, 16), 'border-image', 'url(foo.jpg)', + 0, - // #11 - dirtyStyle(0, 17), + // #12 + dirtyStyle(0, 20), 'border-width', '100px', + 0, - // #14 + // #16 cleanStyleWithSanitization(0, 8), 'border-image', null, + 0, - // #17 - cleanStyle(0, 11), + // #20 + cleanStyle(0, 12), 'border-width', null, + 0, ]); updateStyles(stylingContext, {'background-image': 'unsafe'}); @@ -837,36 +947,41 @@ describe('styling', () => { null, styleSanitizer, [null], - dirtyStyle(0, 14), // + dirtyStyle(0, 16), // 2, element, null, {'background-image': 'unsafe'}, // #8 - dirtyStyleWithSanitization(0, 17), + dirtyStyleWithSanitization(0, 20), 'border-image', 'url(foo.jpg)', + 0, - // #11 - dirtyStyle(0, 20), + // #12 + dirtyStyle(0, 24), 'border-width', '100px', + 0, - // #14 + // #16 dirtyStyleWithSanitization(0, 0), 'background-image', 'unsafe', + 0, - // #17 + // #20 cleanStyleWithSanitization(0, 8), 'border-image', null, + 0, - // #20 - cleanStyle(0, 11), + // #23 + cleanStyle(0, 12), 'border-width', null, + 0, ]); getStyles(stylingContext); @@ -875,39 +990,45 @@ describe('styling', () => { null, styleSanitizer, [null], - cleanStyle(0, 14), // + cleanStyle(0, 16), // 2, element, null, {'background-image': 'unsafe'}, // #8 - cleanStyleWithSanitization(0, 17), + cleanStyleWithSanitization(0, 20), 'border-image', 'url(foo.jpg)', + 0, - // #11 - cleanStyle(0, 20), + // #12 + cleanStyle(0, 24), 'border-width', '100px', + 0, - // #14 + // #16 cleanStyleWithSanitization(0, 0), 'background-image', 'unsafe', + 0, - // #17 + // #20 cleanStyleWithSanitization(0, 8), 'border-image', null, + 0, - // #20 - cleanStyle(0, 11), + // #23 + cleanStyle(0, 12), 'border-width', null, + 0, ]); }); }); + }); describe('classes', () => { @@ -915,20 +1036,38 @@ describe('styling', () => { const template = initContext(null, [InitialStylingFlags.VALUES_MODE, 'one', true, 'two', true]); expect(template).toEqual([ - null, null, [null, true, true], dirtyStyle(0, 14), // - 0, element, null, null, + null, + null, + [null, true, true], + dirtyStyle(0, 16), // + 0, + element, + null, + null, // #8 - cleanClass(1, 14), 'one', null, + cleanClass(1, 16), + 'one', + null, + 0, - // #11 - cleanClass(2, 17), 'two', null, + // #12 + cleanClass(2, 20), + 'two', + null, + 0, - // #14 - dirtyClass(1, 8), 'one', null, + // #16 + dirtyClass(1, 8), + 'one', + null, + 0, - // #17 - dirtyClass(2, 11), 'two', null + // #20 + dirtyClass(2, 12), + 'two', + null, + 0, ]); }); @@ -981,119 +1120,138 @@ describe('styling', () => { null, null, [null, '100px', true], - dirtyStyle(0, 20), // + dirtyStyle(0, 24), // 2, element, null, null, // #8 - cleanStyle(1, 20), + cleanStyle(1, 24), 'width', null, + 0, - // #11 - cleanStyle(0, 23), + // #12 + cleanStyle(0, 28), 'height', null, + 0, - // #14 - cleanClass(2, 26), + // #16 + cleanClass(2, 32), 'wide', null, - - // #17 - cleanClass(0, 29), - 'tall', - null, + 0, // #20 + cleanClass(0, 36), + 'tall', + null, + 0, + + // #23 dirtyStyle(1, 8), 'width', null, + 0, - // #23 - cleanStyle(0, 11), + // #28 + cleanStyle(0, 12), 'height', null, + 0, - // #26 - dirtyClass(2, 14), + // #32 + dirtyClass(2, 16), 'wide', null, + 0, - // #29 - cleanClass(0, 17), + // #36 + cleanClass(0, 20), 'tall', null, + 0, ]); - expect(getStylesAndClasses(stylingContext)).toEqual([{width: '100px'}, {wide: true}]); + expect(getStylesAndClasses(stylingContext)).toEqual([{wide: true}, {width: '100px'}]); updateStylingMap(stylingContext, 'tall round', {width: '200px', opacity: '0.5'}); expect(stylingContext).toEqual([ null, null, [null, '100px', true], - dirtyStyle(0, 20), // + dirtyStyle(0, 24), // 2, element, 'tall round', {width: '200px', opacity: '0.5'}, // #8 - cleanStyle(1, 20), + cleanStyle(1, 24), 'width', null, + 0, - // #11 - cleanStyle(0, 35), + // #12 + cleanStyle(0, 44), 'height', null, + 0, - // #14 - cleanClass(2, 32), + // #16 + cleanClass(2, 40), 'wide', null, - - // #17 - cleanClass(0, 26), - 'tall', - null, + 0, // #20 + cleanClass(0, 32), + 'tall', + null, + 0, + + // #23 dirtyStyle(1, 8), 'width', '200px', + 0, - // #23 + // #28 dirtyStyle(0, 0), 'opacity', '0.5', + 0, - // #26 - dirtyClass(0, 17), + // #32 + dirtyClass(0, 20), 'tall', true, + 0, - // #29 + // #36 dirtyClass(0, 0), 'round', true, + 0, - // #32 - cleanClass(2, 14), + // #40 + cleanClass(2, 16), 'wide', null, + 0, - // #35 - cleanStyle(0, 11), + // #44 + cleanStyle(0, 12), 'height', null, + 0, ]); expect(getStylesAndClasses(stylingContext)).toEqual([ - {width: '200px', opacity: '0.5'}, {tall: true, round: true, wide: true} + {tall: true, round: true, wide: true}, + {width: '200px', opacity: '0.5'}, ]); updateStylingMap(stylingContext, {tall: true, wide: true}, {width: '500px'}); @@ -1103,172 +1261,980 @@ describe('styling', () => { null, null, [null, '100px', true], - dirtyStyle(0, 20), // + dirtyStyle(0, 24), // 2, element, {tall: true, wide: true}, {width: '500px'}, // #8 - dirtyStyle(1, 20), + dirtyStyle(1, 24), 'width', '300px', + 0, - // #11 - cleanStyle(0, 35), + // #12 + cleanStyle(0, 44), 'height', null, + 0, - // #14 - cleanClass(2, 26), + // #16 + cleanClass(2, 32), 'wide', null, - - // #17 - cleanClass(0, 23), - 'tall', - null, + 0, // #20 + cleanClass(0, 28), + 'tall', + null, + 0, + + // #23 cleanStyle(1, 8), 'width', '500px', + 0, - // #23 - cleanClass(0, 17), + // #28 + cleanClass(0, 20), 'tall', true, + 0, - // #26 - cleanClass(2, 14), + // #32 + cleanClass(2, 16), 'wide', true, + 0, - // #29 + // #35 dirtyClass(0, 0), 'round', null, + 0, - // #32 + // #39 dirtyStyle(0, 0), 'opacity', null, + 0, - // #35 - cleanStyle(0, 11), + // #43 + cleanStyle(0, 12), 'height', null, + 0, ]); expect(getStylesAndClasses(stylingContext)).toEqual([ - {width: '300px', opacity: null}, {tall: true, round: false, wide: true} + {tall: true, round: false, wide: true}, + {width: '300px', opacity: null}, ]); }); + + it('should skip updating multi classes and styles if the input identity has not changed', + () => { + const stylingContext = initContext(); + const getStylesAndClasses = trackStylesAndClasses(); + + const stylesMap = {width: '200px'}; + const classesMap = {foo: true}; + updateStylingMap(stylingContext, classesMap, stylesMap); + + // apply the styles + getStylesAndClasses(stylingContext); + + expect(stylingContext).toEqual([ + null, + null, + [null], + cleanStyle(0, 8), // + 0, + element, + {foo: true}, + {width: '200px'}, + + // #8 + cleanStyle(0, 0), + 'width', + '200px', + 0, + + // #11 + cleanClass(0, 0), + 'foo', + true, + 0, + ]); + + stylesMap.width = '300px'; + classesMap.foo = false; + + updateStylingMap(stylingContext, classesMap, stylesMap); + + // apply the styles + getStylesAndClasses(stylingContext); + + expect(stylingContext).toEqual([ + null, + null, + [null], + cleanStyle(0, 8), // + 0, + element, + {foo: false}, + {width: '300px'}, + + // #8 + cleanStyle(0, 0), + 'width', + '200px', + 0, + + // #11 + cleanClass(0, 0), + 'foo', + true, + 0, + ]); + }); + + it('should skip updating multi classes if the string-based identity has not changed', () => { + const stylingContext = initContext(); + const getClasses = trackClassesFactory(); + + const classes = 'apple orange banana'; + updateStylingMap(stylingContext, classes); + + // apply the styles + expect(getClasses(stylingContext)).toEqual({apple: true, orange: true, banana: true}); + + expect(stylingContext).toEqual([ + null, + null, + [null], + cleanStyle(0, 8), // + 0, + element, + 'apple orange banana', + null, + + // #8 + cleanClass(0, 0), + 'apple', + true, + 0, + + // #12 + cleanClass(0, 0), + 'orange', + true, + 0, + + // #16 + cleanClass(0, 0), + 'banana', + true, + 0, + ]); + + stylingContext[13] = false; // no orange + stylingContext[16] = false; // no banana + updateStylingMap(stylingContext, classes); + + // apply the styles + expect(getClasses(stylingContext)).toEqual({apple: true, orange: true, banana: true}); + }); }); - it('should skip updating multi classes and styles if the input identity has not changed', () => { - const stylingContext = initContext(); - const getStylesAndClasses = trackStylesAndClasses(); + describe('players', () => { + it('should build a player with the computed styles and classes', () => { + const context = initContext(null, []); - const stylesMap = {width: '200px'}; - const classesMap = {foo: true}; - updateStylingMap(stylingContext, classesMap, stylesMap); + const styles = {width: '100px', height: '200px'}; + const classes = 'foo bar'; - // apply the styles - getStylesAndClasses(stylingContext); + let classResult: any; + const classFactory = + bindPlayerFactory((element: HTMLElement, type: BindingType, value: any) => { + const player = new MockPlayer(); + classResult = {player, element, type, value}; + return player; + }, classes); - expect(stylingContext).toEqual([ - null, - null, - [null], - cleanStyle(0, 8), // - 0, - element, - {foo: true}, - {width: '200px'}, + let styleResult: any; + const styleFactory = + bindPlayerFactory((element: HTMLElement, type: BindingType, value: any) => { + const player = new MockPlayer(); + styleResult = {player, element, type, value}; + return player; + }, styles); - // #8 - cleanStyle(0, 0), - 'width', - '200px', + updateStylingMap(context, classFactory, styleFactory); + expect(classResult).toBeFalsy(); - // #11 - cleanClass(0, 0), - 'foo', - true, - ]); + renderStyles(context); - stylesMap.width = '300px'; - classesMap.foo = false; + expect(classResult.element).toBe(element); + expect(classResult.type).toBe(BindingType.Class); + expect(classResult.value).toEqual({foo: true, bar: true}); + expect(classResult.player instanceof MockPlayer).toBeTruthy(); - updateStylingMap(stylingContext, classesMap, stylesMap); + expect(styleResult.element).toBe(element); + expect(styleResult.type).toBe(BindingType.Style); + expect(styleResult.value).toEqual(styles); + expect(styleResult.player instanceof MockPlayer).toBeTruthy(); + }); - // apply the styles - getStylesAndClasses(stylingContext); + it('should only build one player for a given style map', () => { + const context = initContext(null, []); - expect(stylingContext).toEqual([ - null, - null, - [null], - cleanStyle(0, 8), // - 0, - element, - {foo: false}, - {width: '300px'}, + let count = 0; + const buildFn = (element: HTMLElement, type: BindingType, value: any) => { + count++; + return new MockPlayer(); + }; - // #8 - cleanStyle(0, 0), - 'width', - '200px', + updateStylingMap(context, null, bindPlayerFactory(buildFn, {width: '100px'})); + renderStyles(context); + expect(count).toEqual(1); - // #11 - cleanClass(0, 0), - 'foo', - true, - ]); - }); + updateStylingMap(context, null, bindPlayerFactory(buildFn, {height: '100px'})); + renderStyles(context); + expect(count).toEqual(2); - it('should skip updating multi classes if the string-based identity has not changed', () => { - const stylingContext = initContext(); - const getClasses = trackClassesFactory(); + updateStylingMap( + context, null, bindPlayerFactory(buildFn, {height: '200px', width: '200px'})); + renderStyles(context); + expect(count).toEqual(3); + }); - const classes = 'apple orange banana'; - updateStylingMap(stylingContext, classes); + it('should only build one player for a given class map', () => { + const context = initContext(null, []); - // apply the styles - expect(getClasses(stylingContext)).toEqual({apple: true, orange: true, banana: true}); + let count = 0; + const buildFn = (element: HTMLElement, type: BindingType, value: any) => { + count++; + return new MockPlayer(); + }; - expect(stylingContext).toEqual([ - null, - null, - [null], - cleanStyle(0, 8), // - 0, - element, - 'apple orange banana', - null, + updateStylingMap(context, bindPlayerFactory(buildFn, {myClass: true})); + renderStyles(context); + expect(count).toEqual(1); - // #8 - cleanClass(0, 0), - 'apple', - true, + updateStylingMap(context, bindPlayerFactory(buildFn, {otherClass: true})); + renderStyles(context); + expect(count).toEqual(2); - // #11 - cleanClass(0, 0), - 'orange', - true, + updateStylingMap(context, bindPlayerFactory(buildFn, {myClass: false, otherClass: false})); + renderStyles(context); + expect(count).toEqual(3); + }); - // #14 - cleanClass(0, 0), - 'banana', - true, - ]); + it('should store active players in the player context and remove them once destroyed', () => { + const context = initContext(null, []); + const handler = new CorePlayerHandler(); + const lViewData = createMockViewData(handler, context); - stylingContext[13] = false; // no orange - stylingContext[16] = false; // no banana - updateStylingMap(stylingContext, classes); + let currentStylePlayer: Player; + const styleBuildFn = (element: HTMLElement, type: BindingType, value: any) => { + return currentStylePlayer = new MockPlayer(); + }; - // apply the styles - expect(getClasses(stylingContext)).toEqual({apple: true, orange: true, banana: true}); + let currentClassPlayer: Player; + const classBuildFn = (element: HTMLElement, type: BindingType, value: any) => { + return currentClassPlayer = new MockPlayer(); + }; + + expect(context[StylingIndex.PlayerContext]).toEqual(null); + + const styleFactory = bindPlayerFactory(styleBuildFn, {width: '100px'}); + const classFactory = bindPlayerFactory(classBuildFn, 'foo'); + const stylePlayerBuilder = + new ClassAndStylePlayerBuilder(styleFactory, element as HTMLElement, BindingType.Style); + const classPlayerBuilder = + new ClassAndStylePlayerBuilder(classFactory, element as HTMLElement, BindingType.Class); + + updateStylingMap(context, classFactory, styleFactory); + expect(context[StylingIndex.PlayerContext]).toEqual([ + 5, classPlayerBuilder, null, stylePlayerBuilder, null + ]); + + renderStyles(context, undefined, lViewData); + expect(context[StylingIndex.PlayerContext]).toEqual([ + 5, classPlayerBuilder, currentClassPlayer !, stylePlayerBuilder, currentStylePlayer ! + ]); + + expect(currentStylePlayer !.state).toEqual(PlayState.Pending); + expect(currentClassPlayer !.state).toEqual(PlayState.Pending); + handler.flushPlayers(); + + expect(currentStylePlayer !.state).toEqual(PlayState.Running); + expect(currentClassPlayer !.state).toEqual(PlayState.Running); + + expect(context[StylingIndex.PlayerContext]).toEqual([ + 5, classPlayerBuilder, currentClassPlayer !, stylePlayerBuilder, currentStylePlayer ! + ]); + + currentStylePlayer !.finish(); + expect(context[StylingIndex.PlayerContext]).toEqual([ + 5, classPlayerBuilder, currentClassPlayer !, stylePlayerBuilder, currentStylePlayer ! + ]); + + currentStylePlayer !.destroy(); + expect(context[StylingIndex.PlayerContext]).toEqual([ + 5, classPlayerBuilder, currentClassPlayer !, stylePlayerBuilder, null + ]); + + currentClassPlayer !.destroy(); + expect(context[StylingIndex.PlayerContext]).toEqual([ + 5, classPlayerBuilder, null, stylePlayerBuilder, null + ]); + }); + + it('should kick off single property change players alongside map-based ones and remove the players', + () => { + const context = initContext(['width', 'height'], ['foo', 'bar']); + const handler = new CorePlayerHandler(); + const lViewData = createMockViewData(handler, context); + + const capturedStylePlayers: Player[] = []; + const styleBuildFn = (element: HTMLElement, type: BindingType, value: any) => { + const player = new MockPlayer(); + capturedStylePlayers.push(player); + return player; + }; + + const capturedClassPlayers: Player[] = []; + const classBuildFn = (element: HTMLElement, type: BindingType, value: any) => { + const player = new MockPlayer(); + capturedClassPlayers.push(player); + return player; + }; + + expect(context[StylingIndex.PlayerContext]).toEqual(null); + + const styleMapFactory = bindPlayerFactory(styleBuildFn, {opacity: '1'}); + const classMapFactory = bindPlayerFactory(classBuildFn, {map: true}); + const styleMapPlayerBuilder = new ClassAndStylePlayerBuilder( + styleMapFactory, element as HTMLElement, BindingType.Style); + const classMapPlayerBuilder = new ClassAndStylePlayerBuilder( + classMapFactory, element as HTMLElement, BindingType.Class); + updateStylingMap(context, classMapFactory, styleMapFactory); + + const widthFactory = bindPlayerFactory(styleBuildFn, '100px'); + const barFactory = bindPlayerFactory(classBuildFn, true); + const widthPlayerBuilder = new ClassAndStylePlayerBuilder( + widthFactory, element as HTMLElement, BindingType.Style); + const barPlayerBuilder = + new ClassAndStylePlayerBuilder(barFactory, element as HTMLElement, BindingType.Class); + updateStyleProp(context, 0, widthFactory as any); + updateClassProp(context, 0, barFactory as any); + + expect(context[StylingIndex.PlayerContext]).toEqual([ + 9, classMapPlayerBuilder, null, styleMapPlayerBuilder, null, widthPlayerBuilder, null, + barPlayerBuilder, null + ]); + + renderStyles(context, undefined, lViewData); + const classMapPlayer = capturedClassPlayers.shift() !; + const barPlayer = capturedClassPlayers.shift() !; + const styleMapPlayer = capturedStylePlayers.shift() !; + const widthPlayer = capturedStylePlayers.shift() !; + + expect(context[StylingIndex.PlayerContext]).toEqual([ + 9, + classMapPlayerBuilder, + classMapPlayer, + styleMapPlayerBuilder, + styleMapPlayer, + widthPlayerBuilder, + widthPlayer, + barPlayerBuilder, + barPlayer, + ]); + + const heightFactory = bindPlayerFactory(styleBuildFn, '200px') !; + const bazFactory = bindPlayerFactory(classBuildFn, true); + const heightPlayerBuilder = new ClassAndStylePlayerBuilder( + heightFactory, element as HTMLElement, BindingType.Style); + const bazPlayerBuilder = + new ClassAndStylePlayerBuilder(bazFactory, element as HTMLElement, BindingType.Class); + updateStyleProp(context, 1, heightFactory as any); + updateClassProp(context, 1, bazFactory as any); + + expect(context[StylingIndex.PlayerContext]).toEqual([ + 13, classMapPlayerBuilder, classMapPlayer, styleMapPlayerBuilder, styleMapPlayer, + widthPlayerBuilder, widthPlayer, barPlayerBuilder, barPlayer, heightPlayerBuilder, null, + bazPlayerBuilder, null + ]); + + renderStyles(context, undefined, lViewData); + const heightPlayer = capturedStylePlayers.shift() !; + const bazPlayer = capturedClassPlayers.shift() !; + + expect(context[StylingIndex.PlayerContext]).toEqual([ + 13, classMapPlayerBuilder, classMapPlayer, styleMapPlayerBuilder, styleMapPlayer, + widthPlayerBuilder, widthPlayer, barPlayerBuilder, barPlayer, heightPlayerBuilder, + heightPlayer, bazPlayerBuilder, bazPlayer + ]); + + widthPlayer.destroy(); + bazPlayer.destroy(); + expect(context[StylingIndex.PlayerContext]).toEqual([ + 13, classMapPlayerBuilder, classMapPlayer, styleMapPlayerBuilder, styleMapPlayer, + widthPlayerBuilder, null, barPlayerBuilder, barPlayer, heightPlayerBuilder, heightPlayer, + bazPlayerBuilder, null + ]); + }); + + it('should destroy an existing player that was queued before it is flushed once the binding updates', + () => { + const context = initContext(['width']); + const handler = new CorePlayerHandler(); + const lViewData = createMockViewData(handler, context); + + const players: MockPlayer[] = []; + const buildFn = + (element: HTMLElement, type: BindingType, value: any, + oldPlayer: MockPlayer | null) => { + const player = new MockPlayer(value); + players.push(player); + return player; + }; + + expect(context[StylingIndex.PlayerContext]).toEqual(null); + + let mapFactory = bindPlayerFactory(buildFn, {width: '200px'}); + updateStylingMap(context, null, mapFactory); + renderStyles(context, undefined, lViewData); + + expect(players.length).toEqual(1); + const p1 = players.pop() !; + expect(p1.state).toEqual(PlayState.Pending); + + mapFactory = bindPlayerFactory(buildFn, {width: '100px'}); + updateStylingMap(context, null, mapFactory); + renderStyles(context, undefined, lViewData); + + expect(players.length).toEqual(1); + const p2 = players.pop() !; + expect(p1.state).toEqual(PlayState.Destroyed); + expect(p2.state).toEqual(PlayState.Pending); + }); + + it('should nullify style map and style property factories if any follow up expressions not use them', + () => { + const context = initContext(['color'], ['foo']); + const handler = new CorePlayerHandler(); + const lViewData = createMockViewData(handler, context); + + const stylePlayers: Player[] = []; + const buildStyleFn = (element: HTMLElement, type: BindingType, value: any) => { + const player = new MockPlayer(); + stylePlayers.push(player); + return player; + }; + + const classPlayers: Player[] = []; + const buildClassFn = (element: HTMLElement, type: BindingType, value: any) => { + const player = new MockPlayer(); + classPlayers.push(player); + return player; + }; + + expect(context).toEqual([ + null, + null, + [null], + cleanStyle(0, 16), // + 1, + element, + null, + null, + + // #8 + cleanStyle(0, 16), + 'color', + null, + 0, + + // #12 + cleanClass(0, 20), + 'foo', + null, + 0, + + // #16 + cleanStyle(0, 8), + 'color', + null, + 0, + + // #20 + cleanClass(0, 12), + 'foo', + null, + 0, + ]); + + const styleMapWithPlayerFactory = bindPlayerFactory(buildStyleFn, {opacity: '1'}); + const classMapWithPlayerFactory = bindPlayerFactory(buildClassFn, {map: true}); + const styleMapPlayerBuilder = makePlayerBuilder(styleMapWithPlayerFactory, false); + const classMapPlayerBuilder = makePlayerBuilder(classMapWithPlayerFactory, true); + updateStylingMap(context, classMapWithPlayerFactory, styleMapWithPlayerFactory); + + const colorWithPlayerFactory = bindPlayerFactory(buildStyleFn, 'red'); + const fooWithPlayerFactory = bindPlayerFactory(buildClassFn, true); + const colorPlayerBuilder = makePlayerBuilder(colorWithPlayerFactory, false); + const fooPlayerBuilder = makePlayerBuilder(fooWithPlayerFactory, true); + updateStyleProp(context, 0, colorWithPlayerFactory as any); + updateClassProp(context, 0, fooWithPlayerFactory as any); + renderStyles(context, undefined, lViewData); + + const p1 = classPlayers.shift(); + const p2 = stylePlayers.shift(); + const p3 = stylePlayers.shift(); + const p4 = classPlayers.shift(); + expect(context).toEqual([ + ([ + 9, classMapPlayerBuilder, p1, styleMapPlayerBuilder, p2, colorPlayerBuilder, p3, + fooPlayerBuilder, p4 + ] as any), + null, + [null], + cleanStyle(0, 16), // + 1, + element, + {map: true}, + {opacity: '1'}, + + // #8 + cleanStyle(0, 24), + 'color', + 'red', + 5, + + // #12 + cleanClass(0, 28), + 'foo', + true, + 7, + + // #16 + cleanStyle(0, 0), + 'opacity', + '1', + 3, + + // #20 + cleanClass(0, 0), + 'map', + true, + 1, + + // #23 + cleanStyle(0, 8), + 'color', + null, + 0, + + // #28 + cleanClass(0, 12), + 'foo', + null, + 0, + ]); + + const styleMapWithoutPlayerFactory = {opacity: '1'}; + const classMapWithoutPlayerFactory = {map: true}; + updateStylingMap(context, classMapWithoutPlayerFactory, styleMapWithoutPlayerFactory); + + const colorWithoutPlayerFactory = 'blue'; + const fooWithoutPlayerFactory = false; + updateStyleProp(context, 0, colorWithoutPlayerFactory); + updateClassProp(context, 0, fooWithoutPlayerFactory); + renderStyles(context, undefined, lViewData); + + expect(context).toEqual([ + ([9, null, null, null, null, null, null, null, null] as any), + null, + [null], + cleanStyle(0, 16), // + 1, + element, + {map: true}, + {opacity: '1'}, + + // #8 + cleanStyle(0, 24), + 'color', + 'blue', + 0, + + // #12 + cleanClass(0, 28), + 'foo', + false, + 0, + + // #16 + cleanStyle(0, 0), + 'opacity', + '1', + 0, + + // #20 + cleanClass(0, 0), + 'map', + true, + 0, + + // #23 + cleanStyle(0, 8), + 'color', + null, + 0, + + // #28 + cleanClass(0, 12), + 'foo', + null, + 0, + ]); + }); + + it('should not call a factory if no style and/or class values have been updated', () => { + const context = initContext([]); + const handler = new CorePlayerHandler(); + const lViewData = createMockViewData(handler, context); + + let styleCalls = 0; + const buildStyleFn = (element: HTMLElement, type: BindingType, value: any) => { + styleCalls++; + return new MockPlayer(); + }; + + let classCalls = 0; + const buildClassFn = (element: HTMLElement, type: BindingType, value: any) => { + classCalls++; + return new MockPlayer(); + }; + + const styleFactory = + bindPlayerFactory(buildStyleFn, {opacity: '1'}) as BoundPlayerFactory; + const classFactory = bindPlayerFactory(buildClassFn, 'bar') as BoundPlayerFactory; + updateStylingMap(context, classFactory, styleFactory); + expect(styleCalls).toEqual(0); + expect(classCalls).toEqual(0); + + renderStyles(context, undefined, lViewData); + expect(styleCalls).toEqual(1); + expect(classCalls).toEqual(1); + + renderStyles(context, undefined, lViewData); + expect(styleCalls).toEqual(1); + expect(classCalls).toEqual(1); + + styleFactory.value = {opacity: '0.5'}; + updateStylingMap(context, classFactory, styleFactory); + renderStyles(context, undefined, lViewData); + expect(styleCalls).toEqual(2); + expect(classCalls).toEqual(1); + + classFactory.value = 'foo'; + updateStylingMap(context, classFactory, styleFactory); + renderStyles(context, undefined, lViewData); + expect(styleCalls).toEqual(2); + expect(classCalls).toEqual(2); + + updateStylingMap(context, 'foo', {opacity: '0.5'}); + renderStyles(context, undefined, lViewData); + expect(styleCalls).toEqual(2); + expect(classCalls).toEqual(2); + }); + + it('should invoke a single prop player over a multi style player when present and delegate back if not', + () => { + const context = initContext(['color']); + const handler = new CorePlayerHandler(); + const lViewData = createMockViewData(handler, context); + + let propPlayer: Player|null = null; + const propBuildFn = (element: HTMLElement, type: BindingType, value: any) => { + return propPlayer = new MockPlayer(); + }; + + let styleMapPlayer: Player|null = null; + const mapBuildFn = (element: HTMLElement, type: BindingType, value: any) => { + return styleMapPlayer = new MockPlayer(); + }; + + const mapFactory = bindPlayerFactory(mapBuildFn, {color: 'black'}); + updateStylingMap(context, null, mapFactory); + updateStyleProp(context, 0, 'green'); + renderStyles(context, undefined, lViewData); + + expect(propPlayer).toBeFalsy(); + expect(styleMapPlayer).toBeFalsy(); + + const propFactory = bindPlayerFactory(propBuildFn, 'orange'); + updateStyleProp(context, 0, propFactory as any); + renderStyles(context, undefined, lViewData); + + expect(propPlayer).toBeTruthy(); + expect(styleMapPlayer).toBeFalsy(); + + propPlayer = styleMapPlayer = null; + + updateStyleProp(context, 0, null); + renderStyles(context, undefined, lViewData); + + expect(propPlayer).toBeFalsy(); + expect(styleMapPlayer).toBeTruthy(); + + propPlayer = styleMapPlayer = null; + + updateStylingMap(context, null, null); + renderStyles(context, undefined, lViewData); + + expect(propPlayer).toBeFalsy(); + expect(styleMapPlayer).toBeFalsy(); + }); + + it('should return the old player for styles when a follow-up player is instantiated', () => { + const context = initContext([]); + const handler = new CorePlayerHandler(); + const lViewData = createMockViewData(handler, context); + + let previousPlayer: MockPlayer|null = null; + let currentPlayer: MockPlayer|null = null; + const buildFn = + (element: HTMLElement, type: BindingType, value: any, existingPlayer: MockPlayer) => { + previousPlayer = existingPlayer; + return currentPlayer = new MockPlayer(value); + }; + + let factory = bindPlayerFactory<{[key: string]: any}>(buildFn, {width: '200px'}); + updateStylingMap(context, null, factory); + renderStyles(context, undefined, lViewData); + + expect(previousPlayer).toEqual(null); + expect(currentPlayer !.value).toEqual({width: '200px'}); + + factory = bindPlayerFactory(buildFn, {height: '200px'}); + + updateStylingMap(context, null, factory); + renderStyles(context, undefined, lViewData); + + expect(previousPlayer !.value).toEqual({width: '200px'}); + expect(currentPlayer !.value).toEqual({width: null, height: '200px'}); + }); + + it('should return the old player for classes when a follow-up player is instantiated', () => { + const context = initContext([]); + const handler = new CorePlayerHandler(); + const lViewData = createMockViewData(handler, context); + + let currentPlayer: MockPlayer|null = null; + let previousPlayer: MockPlayer|null = null; + const buildFn = + (element: HTMLElement, type: BindingType, value: any, + existingPlayer: MockPlayer | null) => { + previousPlayer = existingPlayer; + return currentPlayer = new MockPlayer(value); + }; + + let factory = bindPlayerFactory(buildFn, {foo: true}); + updateStylingMap(context, null, factory); + renderStyles(context, undefined, lViewData); + + expect(currentPlayer).toBeTruthy(); + expect(previousPlayer).toBeFalsy(); + expect(currentPlayer !.value).toEqual({foo: true}); + + previousPlayer = currentPlayer = null; + + factory = bindPlayerFactory(buildFn, {bar: true}); + updateStylingMap(context, null, factory); + renderStyles(context, undefined, lViewData); + + expect(currentPlayer).toBeTruthy(); + expect(previousPlayer).toBeTruthy(); + expect(currentPlayer !.value).toEqual({foo: null, bar: true}); + expect(previousPlayer !.value).toEqual({foo: true}); + }); + + it('should sanitize styles before they are passed into the player', () => { + const sanitizer = (function(prop: string, value?: string): string | boolean { + if (value === undefined) { + return prop === 'width' || prop === 'height'; + } else { + return `${value}-safe!`; + } + }) as StyleSanitizeFn; + + const context = initContext([], [], sanitizer); + const handler = new CorePlayerHandler(); + const lViewData = createMockViewData(handler, context); + + let values: {[key: string]: any}|null = null; + const buildFn = (element: HTMLElement, type: BindingType, value: any) => { + values = value; + return new MockPlayer(); + }; + + let factory = bindPlayerFactory<{[key: string]: any}>( + buildFn, {width: '200px', height: '100px', opacity: '1'}); + updateStylingMap(context, null, factory); + renderStyles(context, undefined, lViewData); + + expect(values !).toEqual({width: '200px-safe!', height: '100px-safe!', opacity: '1'}); + + factory = bindPlayerFactory(buildFn, {width: 'auto'}); + updateStylingMap(context, null, factory); + renderStyles(context, undefined, lViewData); + + expect(values !).toEqual({width: 'auto-safe!', height: null, opacity: null}); + }); + + it('should automatically destroy existing players when the follow-up binding is not apart of a factory', + () => { + const context = initContext(['width'], ['foo', 'bar']); + const handler = new CorePlayerHandler(); + const lViewData = createMockViewData(handler, context); + + const players: Player[] = []; + const styleBuildFn = (element: HTMLElement, type: BindingType, value: any) => { + const player = new MockPlayer(); + players.push(player); + return player; + }; + + const classBuildFn = (element: HTMLElement, type: BindingType, value: any) => { + const player = new MockPlayer(); + players.push(player); + return player; + }; + + expect(context[StylingIndex.PlayerContext]).toEqual(null); + + const styleMapFactory = bindPlayerFactory(styleBuildFn, {opacity: '1'}); + const classMapFactory = bindPlayerFactory(classBuildFn, {map: true}); + updateStylingMap(context, classMapFactory, styleMapFactory); + updateStyleProp(context, 0, bindPlayerFactory(styleBuildFn, '100px') as any); + updateClassProp(context, 0, bindPlayerFactory(classBuildFn, true) as any); + updateClassProp(context, 1, bindPlayerFactory(classBuildFn, true) as any); + renderStyles(context, undefined, lViewData); + handler.flushPlayers(); + + const [p1, p2, p3, p4, p5] = players; + expect(p1.state).toEqual(PlayState.Running); + expect(p2.state).toEqual(PlayState.Running); + expect(p3.state).toEqual(PlayState.Running); + expect(p4.state).toEqual(PlayState.Running); + + updateStylingMap(context, {bar: true}, {height: '200px'}); + updateStyleProp(context, 0, '200px'); + updateClassProp(context, 0, false); + expect(p1.state).toEqual(PlayState.Running); + expect(p2.state).toEqual(PlayState.Running); + expect(p3.state).toEqual(PlayState.Running); + expect(p4.state).toEqual(PlayState.Running); + expect(p5.state).toEqual(PlayState.Running); + + renderStyles(context, undefined, lViewData); + expect(p1.state).toEqual(PlayState.Destroyed); + expect(p2.state).toEqual(PlayState.Destroyed); + expect(p3.state).toEqual(PlayState.Destroyed); + expect(p4.state).toEqual(PlayState.Destroyed); + expect(p5.state).toEqual(PlayState.Running); + }); + + it('should list all [style] and [class] players alongside custom players in the context', + () => { + const players: Player[] = []; + const styleBuildFn = (element: HTMLElement, type: BindingType, value: any) => { + const player = new MockPlayer(); + players.push(player); + return player; + }; + + const classBuildFn = (element: HTMLElement, type: BindingType, value: any) => { + const player = new MockPlayer(); + players.push(player); + return player; + }; + + const styleMapFactory = bindPlayerFactory(styleBuildFn, {height: '200px'}); + const classMapFactory = bindPlayerFactory(classBuildFn, {bar: true}); + const widthFactory = bindPlayerFactory(styleBuildFn, '100px'); + const fooFactory = bindPlayerFactory(classBuildFn, true); + + class Comp { + static ngComponentDef = defineComponent({ + type: Comp, + selectors: [['comp']], + directives: [Comp], + factory: () => new Comp(), + consts: 1, + vars: 0, + template: (rf: RenderFlags, ctx: Comp) => { + if (rf & RenderFlags.Create) { + elementStart(0, 'div'); + elementStyling(['foo'], ['width']); + elementEnd(); + } + if (rf & RenderFlags.Update) { + elementStylingMap(0, classMapFactory, styleMapFactory); + elementStyleProp(0, 0, widthFactory); + elementClassProp(0, 0, fooFactory); + elementStylingApply(0); + } + } + }); + } + + const fixture = new ComponentFixture(Comp); + fixture.update(); + + const target = fixture.hostElement.querySelector('div') !as any; + const elementContext = getContext(target) !; + const context = elementContext.lViewData[elementContext.nodeIndex] as StylingContext; + + expect(players.length).toEqual(4); + const [p1, p2, p3, p4] = players; + + const playerContext = context[StylingIndex.PlayerContext]; + expect(playerContext).toContain(p1); + expect(playerContext).toContain(p2); + expect(playerContext).toContain(p3); + expect(playerContext).toContain(p4); + + expect(getPlayers(target)).toEqual([p1, p2, p3, p4]); + + const p5 = new MockPlayer(); + const p6 = new MockPlayer(); + addPlayer(target, p5); + addPlayer(target, p6); + + expect(getPlayers(target)).toEqual([p1, p2, p3, p4, p5, p6]); + p3.destroy(); + p5.destroy(); + + expect(getPlayers(target)).toEqual([p1, p2, p4, p6]); + }); }); }); + +class MockStylingStore implements BindingStore { + private _values: {[key: string]: any} = {}; + + constructor(public element: HTMLElement, public type: BindingType) {} + + setValue(prop: string, value: any): void { this._values[prop] = value; } + + getValues() { return this._values; } +} diff --git a/packages/core/test/render3/styling/mock_player.ts b/packages/core/test/render3/styling/mock_player.ts index 5238a943a0..c78ceed635 100644 --- a/packages/core/test/render3/styling/mock_player.ts +++ b/packages/core/test/render3/styling/mock_player.ts @@ -14,6 +14,8 @@ export class MockPlayer implements Player { state: PlayState = PlayState.Pending; private _listeners: {[state: string]: (() => any)[]} = {}; + constructor(public value?: any) {} + play(): void { if (this.state === PlayState.Running) return; diff --git a/packages/core/test/render3/styling/players_spec.ts b/packages/core/test/render3/styling/players_spec.ts index 6f1045f743..13029c149d 100644 --- a/packages/core/test/render3/styling/players_spec.ts +++ b/packages/core/test/render3/styling/players_spec.ts @@ -9,9 +9,9 @@ import {RenderFlags} from '@angular/core/src/render3'; import {defineComponent, getHostElement} from '../../../src/render3/index'; import {element, elementEnd, elementStart, elementStyling, elementStylingApply, load, markDirty} from '../../../src/render3/instructions'; -import {PlayState, Player, PlayerContext, PlayerHandler} from '../../../src/render3/interfaces/player'; +import {PlayState, Player, PlayerHandler} from '../../../src/render3/interfaces/player'; import {RElement} from '../../../src/render3/interfaces/renderer'; -import {addPlayer, getPlayers} from '../../../src/render3/player'; +import {addPlayer, getPlayers} from '../../../src/render3/players'; import {QueryList, query, queryRefresh} from '../../../src/render3/query'; import {getOrCreatePlayerContext} from '../../../src/render3/styling/util'; import {ComponentFixture} from '../render_util'; @@ -56,14 +56,14 @@ describe('animation player access', () => { it('should add a player to the element animation context and remove it once it completes', () => { const element = buildElement(); const context = getOrCreatePlayerContext(element); - expect(context).toEqual([]); + expect(getPlayers(element)).toEqual([]); const player = new MockPlayer(); addPlayer(element, player); - expect(readPlayers(context)).toEqual([player]); + expect(getPlayers(element)).toEqual([player]); player.destroy(); - expect(readPlayers(context)).toEqual([]); + expect(getPlayers(element)).toEqual([]); }); it('should flush all pending animation players after change detection', () => { @@ -226,10 +226,6 @@ function buildElementWithStyling() { return fixture.hostElement.querySelector('div') as RElement; } -function readPlayers(context: PlayerContext): Player[] { - return context; -} - class Comp { static ngComponentDef = defineComponent({ type: Comp,