Skip to content

Commit

Permalink
feat(ivy): enhance [style] and [class] bindings to be animation aware (
Browse files Browse the repository at this point in the history
…#26096)

PR Close #26096
  • Loading branch information
matsko authored and mhevery committed Oct 18, 2018
1 parent 07a2664 commit 1fafc5c
Show file tree
Hide file tree
Showing 19 changed files with 2,047 additions and 441 deletions.
5 changes: 5 additions & 0 deletions packages/core/src/core_render3_private_export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ export {

export {
Player as ɵPlayer,
PlayerFactory as ɵPlayerFactory,
PlayState as ɵPlayState,
PlayerHandler as ɵPlayerHandler,
} from './render3/interfaces/player';
Expand All @@ -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,
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/render3/discovery_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
43 changes: 26 additions & 17 deletions packages/core/src/render3/instructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,18 @@ 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';
import {BINDING_INDEX, CLEANUP, CONTAINER_INDEX, CONTENT_QUERIES, CONTEXT, CurrentMatchesList, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, HOST_NODE, INJECTOR, LViewData, LViewFlags, NEXT, OpaqueViewState, PARENT, QUERIES, RENDERER, RootContext, RootContextFlags, SANITIZER, TAIL, TVIEW, TView} from './interfaces/view';
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';



Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
}
}

/**
Expand All @@ -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) {
Expand Down Expand Up @@ -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);
}

/**
Expand All @@ -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(() => {
Expand Down
75 changes: 71 additions & 4 deletions packages/core/src/render3/interfaces/player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand All @@ -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.
Expand All @@ -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 = {};
26 changes: 13 additions & 13 deletions packages/core/src/render3/interfaces/styling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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` */
Expand Down Expand Up @@ -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
}
3 changes: 2 additions & 1 deletion packages/core/src/render3/interfaces/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
77 changes: 47 additions & 30 deletions packages/core/src/render3/players.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 './ng_dev_mode';

import {getContext} from './context_discovery';
import {getRootContext} from './discovery_utils';
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 {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) : [];
}

0 comments on commit 1fafc5c

Please sign in to comment.