feat(ivy): enhance [style] and [class] bindings to be animation aware (#26096)
PR Close #26096
This commit is contained in:
		
							parent
							
								
									be337a2e52
								
							
						
					
					
						commit
						fa8e633be5
					
				| @ -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, | ||||
|  | ||||
| @ -74,9 +74,9 @@ export function getHostComponent<T = {}>(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; | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -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<T>( | ||||
|     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<boolean>) : (!!value); | ||||
|   updateElementClassProp(getStylingContext(index, viewData), stylingIndex, val); | ||||
| } | ||||
| 
 | ||||
| /** | ||||
| @ -1534,7 +1538,7 @@ export function elementClassProp<T>( | ||||
|  * @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<T>( | ||||
| export function elementStyling( | ||||
|     classDeclarations?: (string | boolean | InitialStylingFlags)[] | null, | ||||
|     styleDeclarations?: (string | boolean | InitialStylingFlags)[] | null, | ||||
|     styleSanitizer?: StyleSanitizeFn | null): void { | ||||
| @ -1565,8 +1569,13 @@ export function elementStyling<T>( | ||||
|  *        specifically for element styling--the index must be the next index after the element | ||||
|  *        index.) | ||||
|  */ | ||||
| export function elementStylingApply<T>(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<T>(index: number): void { | ||||
|  *        Note that when a suffix is provided then the underlying sanitizer will | ||||
|  *        be ignored. | ||||
|  */ | ||||
| export function elementStyleProp<T>( | ||||
|     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<T>(rootContext: RootContext) { | ||||
|   if (rootContext.clean == _CLEAN_PROMISE) { | ||||
| export function scheduleTick<T>(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<null>((r) => res = r); | ||||
|     rootContext.scheduler(() => { | ||||
|  | ||||
| @ -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<null|number|Player|PlayerBuilder> { | ||||
|   [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 = {}; | ||||
|  | ||||
| @ -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<string|null|boolean> { [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
 | ||||
| } | ||||
|  | ||||
| @ -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; | ||||
| 
 | ||||
|  | ||||
| @ -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) : []; | ||||
| } | ||||
|  | ||||
| @ -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|string|{[key: string]: any}>| null, | ||||
|     stylesInput?: {[key: string]: any} | BoundPlayerFactory<null|{[key: string]: any}>| | ||||
|         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<string|boolean|null>): 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<any>).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<boolean>): 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<any>| 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<any>| 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<any>| 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<any>| 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<any>| 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<any>| | ||||
|     null { | ||||
|   const playerBuilderIndex = getPlayerBuilderIndex(context, index); | ||||
|   if (playerBuilderIndex) { | ||||
|     const playerContext = context[StylingIndex.PlayerContext]; | ||||
|     if (playerContext) { | ||||
|       return playerContext[playerBuilderIndex] as ClassAndStylePlayerBuilder<any>| 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<T> implements PlayerBuilder { | ||||
|   private _values: {[key: string]: string | null} = {}; | ||||
|   private _dirty = false; | ||||
|   private _factory: BoundPlayerFactory<T>; | ||||
| 
 | ||||
|   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; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -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(); | ||||
|       } | ||||
|     } | ||||
|  | ||||
							
								
								
									
										33
									
								
								packages/core/src/render3/styling/player_factory.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								packages/core/src/render3/styling/player_factory.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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<T>(factoryFn: PlayerFactoryBuildFn, value: T): PlayerFactory { | ||||
|   return new BoundPlayerFactory(factoryFn, value) as any; | ||||
| } | ||||
| 
 | ||||
| export class BoundPlayerFactory<T> { | ||||
|   '__brand__': 'Brand for PlayerFactory that nothing will match'; | ||||
|   constructor(public fn: PlayerFactoryBuildFn, public value: T) {} | ||||
| } | ||||
| @ -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'); | ||||
| } | ||||
|  | ||||
| @ -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", | ||||
|  | ||||
| @ -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" | ||||
|   }, | ||||
|  | ||||
| @ -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: ` | ||||
|     <nav> | ||||
|       <button (click)="doAnimate()">Populate List</button> | ||||
|       <button (click)="animateWithCustomPlayer()">Animate List (custom player)</button> | ||||
|       <button (click)="animateWithStyles()">Populate List (style bindings)</button> | ||||
|     </nav> | ||||
|     <div class="list"> | ||||
|       <div *ngFor="let item of items" class="record" [class]="makeClass(item)"> | ||||
|       <div | ||||
|         *ngFor="let item of items" class="record" [class]="makeClass(item)" style="border-radius: 10px" | ||||
|         [style]="styles"> | ||||
|         {{ item }} | ||||
|       </div> | ||||
|     </div> | ||||
| @ -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()); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -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" | ||||
|   }, | ||||
|  | ||||
| @ -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" | ||||
|   }, | ||||
|  | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -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; | ||||
| 
 | ||||
|  | ||||
| @ -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, | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user