From 82a14dc107d26bbf17dc802c0c29711401c7a25f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Tue, 28 Aug 2018 16:49:52 -0700 Subject: [PATCH] feat(ivy): provide groundwork for animations in core (#25234) PR Close #25234 --- .../core/src/core_render3_private_export.ts | 11 + .../render3/animations/core_player_handler.ts | 24 + .../core/src/render3/animations/interfaces.ts | 56 + .../core/src/render3/animations/players.ts | 72 ++ packages/core/src/render3/component.ts | 23 +- .../core/src/render3/context_discovery.ts | 5 +- packages/core/src/render3/instructions.ts | 47 +- packages/core/src/render3/interfaces/view.ts | 14 + .../core/src/render3/node_manipulation.ts | 4 +- packages/core/src/render3/styling.ts | 47 +- packages/core/src/render3/util.ts | 30 +- .../test/bundling/animation_world/BUILD.bazel | 53 + .../animation_world/animation_world.css | 49 + .../test/bundling/animation_world/base.css | 141 +++ .../bundle.golden_symbols.json | 1019 +++++++++++++++++ .../test/bundling/animation_world/index.html | 32 + .../test/bundling/animation_world/index.ts | 136 +++ .../bundling/todo/bundle.golden_symbols.json | 3 + .../todo_r2/bundle.golden_symbols.json | 3 + .../animations/core_player_handler_spec.ts | 60 + .../test/render3/animations/mock_player.ts | 58 + .../test/render3/animations/players_spec.ts | 311 +++++ packages/core/test/render3/render_util.ts | 13 +- packages/core/test/render3/styling_spec.ts | 435 +++---- 24 files changed, 2379 insertions(+), 267 deletions(-) create mode 100644 packages/core/src/render3/animations/core_player_handler.ts create mode 100644 packages/core/src/render3/animations/interfaces.ts create mode 100644 packages/core/src/render3/animations/players.ts create mode 100644 packages/core/test/bundling/animation_world/BUILD.bazel create mode 100644 packages/core/test/bundling/animation_world/animation_world.css create mode 100644 packages/core/test/bundling/animation_world/base.css create mode 100644 packages/core/test/bundling/animation_world/bundle.golden_symbols.json create mode 100644 packages/core/test/bundling/animation_world/index.html create mode 100644 packages/core/test/bundling/animation_world/index.ts create mode 100644 packages/core/test/render3/animations/core_player_handler_spec.ts create mode 100644 packages/core/test/render3/animations/mock_player.ts create mode 100644 packages/core/test/render3/animations/players_spec.ts diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts index 3a50e6558b..795274f36f 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -166,6 +166,17 @@ export { getContext as ɵgetContext } from './render3/context_discovery'; +export { + Player as ɵPlayer, + PlayState as ɵPlayState, + PlayerHandler as ɵPlayerHandler, +} from './render3/animations/interfaces'; + +export { + addPlayer as ɵaddPlayer, + getPlayers as ɵgetPlayers, +} from './render3/animations/players'; + // we reexport these symbols just so that they are retained during the dead code elimination // performed by rollup while it's creating fesm files. // diff --git a/packages/core/src/render3/animations/core_player_handler.ts b/packages/core/src/render3/animations/core_player_handler.ts new file mode 100644 index 0000000000..d9941b99d9 --- /dev/null +++ b/packages/core/src/render3/animations/core_player_handler.ts @@ -0,0 +1,24 @@ +/** + * @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 {Player, PlayerHandler} from './interfaces'; + +export class CorePlayerHandler implements PlayerHandler { + private _players: Player[] = []; + + flushPlayers() { + for (let i = 0; i < this._players.length; i++) { + const player = this._players[i]; + if (!player.parent) { + player.play(); + } + } + this._players.length = 0; + } + + queuePlayer(player: Player) { this._players.push(player); } +} diff --git a/packages/core/src/render3/animations/interfaces.ts b/packages/core/src/render3/animations/interfaces.ts new file mode 100644 index 0000000000..a55705a19d --- /dev/null +++ b/packages/core/src/render3/animations/interfaces.ts @@ -0,0 +1,56 @@ +/** + * @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 + */ + +/** + * A shared interface which contains an animation player + */ +export interface Player { + parent?: Player|null; + state: PlayState; + play(): void; + pause(): void; + finish(): void; + destroy(): void; + addEventListener(state: PlayState|string, cb: (data?: any) => any): void; +} + +/** + * The state of a given player + * + * Do not change the increasing nature of the numbers since the player + * code may compare state by checking if a number is higher or lower than + * a certain numeric value. + */ +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. + */ +export declare type AnimationContext = Player[]; +export declare type ComponentInstance = {}; +export declare type DirectiveInstance = {}; + +/** + * Designed to be used as an injection service to capture all animation players. + * + * When present all animation players will be passed into the flush method below. + * This feature is designed to service application-wide animation testing, live + * debugging as well as custom animation choreographing tools. + */ +export interface PlayerHandler { + /** + * Designed to kick off the player at the end of change detection + */ + flushPlayers(): void; + + /** + * @param player The player that has been scheduled to run within the application. + * @param context The context as to where the player was bound to + */ + queuePlayer(player: Player, context: ComponentInstance|DirectiveInstance|HTMLElement): void; +} diff --git a/packages/core/src/render3/animations/players.ts b/packages/core/src/render3/animations/players.ts new file mode 100644 index 0000000000..d1362f00ec --- /dev/null +++ b/packages/core/src/render3/animations/players.ts @@ -0,0 +1,72 @@ +/** + * @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 '../ng_dev_mode'; + +import {LContext, getContext} from '../context_discovery'; +import {scheduleTick} from '../instructions'; +import {LElementNode} from '../interfaces/node'; +import {RootContextFlags} from '../interfaces/view'; +import {StylingContext, StylingIndex, createEmptyStylingContext} from '../styling'; +import {getRootContext} from '../util'; + +import {CorePlayerHandler} from './core_player_handler'; +import {AnimationContext, ComponentInstance, DirectiveInstance, PlayState, Player} from './interfaces'; + +export function addPlayer( + ref: ComponentInstance | DirectiveInstance | HTMLElement, player: Player): void { + const elementContext = getContext(ref) !; + const animationContext = getOrCreateAnimationContext(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); + } +} + +export function getPlayers(ref: ComponentInstance | DirectiveInstance | HTMLElement): Player[] { + return getOrCreateAnimationContext(ref); +} + +export function getOrCreateAnimationContext( + target: {}, context?: LContext | null): AnimationContext { + context = context || getContext(target) !; + if (ngDevMode && !context) { + throw new Error( + 'Only elements that exist in an Angular application can be used for animations'); + } + + const {lViewData, lNodeIndex} = context; + const value = lViewData[lNodeIndex]; + let stylingContext = value as StylingContext; + if (!Array.isArray(value)) { + stylingContext = lViewData[lNodeIndex] = createEmptyStylingContext(value as LElementNode); + } + return stylingContext[StylingIndex.AnimationContext] || allocAnimationContext(stylingContext); +} + +function allocAnimationContext(data: StylingContext): AnimationContext { + return data[StylingIndex.AnimationContext] = []; +} diff --git a/packages/core/src/render3/component.ts b/packages/core/src/render3/component.ts index 0250a22a86..d9dd57ba5a 100644 --- a/packages/core/src/render3/component.ts +++ b/packages/core/src/render3/component.ts @@ -12,16 +12,18 @@ import {Type} from '../core'; import {Injector} from '../di/injector'; import {Sanitizer} from '../sanitization/security'; +import {PlayerHandler} from './animations/interfaces'; import {assertComponentType, assertDefined} from './assert'; +import {getLElementFromComponent, readPatchedLViewData} from './context_discovery'; +import {getComponentDef} from './definition'; import {queueInitHooks, queueLifecycleHooks} from './hooks'; -import {CLEAN_PROMISE, baseDirectiveCreate, createLViewData, createTView, detectChangesInternal, enterView, executeInitAndContentHooks, getRootView, hostElement, leaveView, locateHostElement, setHostBindings, queueHostBindingForCheck,} from './instructions'; +import {CLEAN_PROMISE, baseDirectiveCreate, createLViewData, createTView, detectChangesInternal, enterView, executeInitAndContentHooks, hostElement, leaveView, locateHostElement, setHostBindings, queueHostBindingForCheck,} from './instructions'; import {ComponentDef, ComponentDefInternal, ComponentType} from './interfaces/definition'; import {LElementNode} from './interfaces/node'; import {RElement, RendererFactory3, domRendererFactory3} from './interfaces/renderer'; -import {LViewData, LViewFlags, RootContext, INJECTOR, CONTEXT, TVIEW} from './interfaces/view'; -import {stringify} from './util'; -import {getComponentDef} from './definition'; -import {getLElementFromComponent, readPatchedLViewData} from './context_discovery'; +import {CONTEXT, INJECTOR, LViewData, LViewFlags, RootContext, RootContextFlags, TVIEW} from './interfaces/view'; +import {getRootView, stringify} from './util'; + /** Options that control how the component should be bootstrapped. */ @@ -32,6 +34,9 @@ export interface CreateComponentOptions { /** A custom sanitizer instance */ sanitizer?: Sanitizer; + /** A custom animation player handler */ + playerHandler?: PlayerHandler; + /** * Host element on which the component will be bootstrapped. If not specified, * the component definition's `tag` is used to query the existing DOM for the @@ -109,7 +114,8 @@ export function renderComponent( const hostNode = locateHostElement(rendererFactory, opts.host || componentTag); const rootFlags = componentDef.onPush ? LViewFlags.Dirty | LViewFlags.IsRoot : LViewFlags.CheckAlways | LViewFlags.IsRoot; - const rootContext = createRootContext(opts.scheduler || requestAnimationFrame.bind(window)); + const rootContext = createRootContext( + opts.scheduler || requestAnimationFrame.bind(window), opts.playerHandler || null); const rootView: LViewData = createLViewData( rendererFactory.createRenderer(hostNode, componentDef), @@ -157,11 +163,14 @@ export function createRootComponent( } -export function createRootContext(scheduler: (workFn: () => void) => void): RootContext { +export function createRootContext( + scheduler: (workFn: () => void) => void, playerHandler?: PlayerHandler|null): RootContext { return { components: [], scheduler: scheduler, clean: CLEAN_PROMISE, + playerHandler: playerHandler || null, + flags: RootContextFlags.Empty }; } diff --git a/packages/core/src/render3/context_discovery.ts b/packages/core/src/render3/context_discovery.ts index b7e9424207..04deded6de 100644 --- a/packages/core/src/render3/context_discovery.ts +++ b/packages/core/src/render3/context_discovery.ts @@ -11,7 +11,6 @@ import {assertEqual} from './assert'; import {LElementNode, TNode, TNodeFlags} from './interfaces/node'; import {RElement} from './interfaces/renderer'; import {CONTEXT, DIRECTIVES, HEADER_OFFSET, LViewData, TVIEW} from './interfaces/view'; -import {readElementValue} from './util'; /** * This property will be monkey-patched on elements, components and directives @@ -401,3 +400,7 @@ function getDirectiveEndIndex(tNode: TNode, startIndex: number): number { const count = tNode.flags & TNodeFlags.DirectiveCountMask; return count ? (startIndex + count) : -1; } + +export function readElementValue(value: LElementNode | any[]): LElementNode { + return (Array.isArray(value) ? (value as any as any[])[0] : value) as LElementNode; +} diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index cb3397ac7f..d20e18098b 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -13,7 +13,7 @@ import {Sanitizer} from '../sanitization/security'; import {StyleSanitizeFn} from '../sanitization/style_sanitizer'; import {assertDefined, assertEqual, assertLessThan, assertNotEqual} from './assert'; -import {attachPatchData, getLElementFromComponent, readPatchedLViewData} from './context_discovery'; +import {attachPatchData, getLElementFromComponent, readElementValue, readPatchedLViewData} from './context_discovery'; import {throwCyclicDependencyError, throwErrorIfNoChangesMode, throwMultipleComponentError} from './errors'; import {executeHooks, executeInitHooks, queueInitHooks, queueLifecycleHooks} from './hooks'; import {ACTIVE_INDEX, LContainer, RENDER_PARENT, VIEWS} from './interfaces/container'; @@ -23,12 +23,12 @@ import {AttributeMarker, InitialInputData, InitialInputs, LContainerNode, LEleme 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, DIRECTIVES, FLAGS, HEADER_OFFSET, HOST_NODE, INJECTOR, LViewData, LViewFlags, NEXT, OpaqueViewState, PARENT, QUERIES, RENDERER, RootContext, SANITIZER, TAIL, TVIEW, TView} from './interfaces/view'; +import {BINDING_INDEX, CLEANUP, CONTAINER_INDEX, CONTENT_QUERIES, CONTEXT, CurrentMatchesList, DECLARATION_VIEW, DIRECTIVES, FLAGS, HEADER_OFFSET, 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, getContainerNode, getHostElementNode, getLViewChild, getParentOrContainerNode, getRenderParent, insertView, removeView} from './node_manipulation'; import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher'; import {StylingContext, allocStylingContext, createStylingContextTemplate, renderStyling as renderElementStyles, updateClassProp as updateElementClassProp, updateStyleProp as updateElementStyleProp, updateStylingMap} from './styling'; -import {assertDataInRangeInternal, getLNode, isContentQueryHost, isDifferent, loadElementInternal, loadInternal, readElementValue, stringify} from './util'; +import {assertDataInRangeInternal, getLNode, getRootContext, getRootView, isContentQueryHost, isDifferent, loadElementInternal, loadInternal, stringify} from './util'; import {ViewRef} from './view_ref'; @@ -2354,9 +2354,14 @@ export function markViewDirty(view: LViewData): void { } currentView[FLAGS] |= LViewFlags.Dirty; ngDevMode && assertDefined(currentView[CONTEXT], 'rootContext should be defined'); - scheduleTick(currentView[CONTEXT] as RootContext); -} + const rootContext = currentView[CONTEXT] as RootContext; + const nothingScheduled = rootContext.flags === RootContextFlags.Empty; + rootContext.flags |= RootContextFlags.DetectChanges; + if (nothingScheduled) { + scheduleTick(rootContext); + } +} /** * Used to schedule change detection on the whole application. @@ -2374,9 +2379,21 @@ export function scheduleTick(rootContext: RootContext) { let res: null|((val: null) => void); rootContext.clean = new Promise((r) => res = r); rootContext.scheduler(() => { - tickRootContext(rootContext); - res !(null); + if (rootContext.flags & RootContextFlags.DetectChanges) { + rootContext.flags &= ~RootContextFlags.DetectChanges; + tickRootContext(rootContext); + } + + if (rootContext.flags & RootContextFlags.FlushPlayers) { + rootContext.flags &= ~RootContextFlags.FlushPlayers; + const playerHandler = rootContext.playerHandler; + if (playerHandler) { + playerHandler.flushPlayers(); + } + } + rootContext.clean = _CLEAN_PROMISE; + res !(null); }); } } @@ -2406,22 +2423,6 @@ function tickRootContext(rootContext: RootContext) { } } -/** - * Retrieve the root view from any component by walking the parent `LViewData` until - * reaching the root `LViewData`. - * - * @param component any component - */ - -export function getRootView(component: any): LViewData { - ngDevMode && assertDefined(component, 'component'); - let lViewData = readPatchedLViewData(component) !; - while (lViewData && !(lViewData[FLAGS] & LViewFlags.IsRoot)) { - lViewData = lViewData[PARENT] !; - } - return lViewData; -} - /** * Synchronously perform change detection on a component (and possibly its sub-components). * diff --git a/packages/core/src/render3/interfaces/view.ts b/packages/core/src/render3/interfaces/view.ts index 479c9d6f38..a150f773a3 100644 --- a/packages/core/src/render3/interfaces/view.ts +++ b/packages/core/src/render3/interfaces/view.ts @@ -9,6 +9,7 @@ import {Injector} from '../../di/injector'; import {QueryList} from '../../linker'; import {Sanitizer} from '../../sanitization/security'; +import {PlayerHandler} from '../animations/interfaces'; import {LContainer} from './container'; import {ComponentQuery, ComponentTemplate, DirectiveDefInternal, DirectiveDefList, PipeDefInternal, PipeDefList} from './definition'; @@ -499,6 +500,9 @@ export interface TView { contentQueries: number[]|null; } +export const enum RootContextFlags {Empty = 0b00, DetectChanges = 0b01, FlushPlayers = 0b10} + + /** * RootContext contains information which is shared for all components which * were bootstrapped with {@link renderComponent}. @@ -522,6 +526,16 @@ export interface RootContext { * {@link renderComponent}. */ components: {}[]; + + /** + * The player flushing handler to kick off all animations + */ + playerHandler: PlayerHandler|null; + + /** + * What render-related operations to run once a scheduler has been set + */ + flags: RootContextFlags; } /** diff --git a/packages/core/src/render3/node_manipulation.ts b/packages/core/src/render3/node_manipulation.ts index 537a65f81d..ee434159bc 100644 --- a/packages/core/src/render3/node_manipulation.ts +++ b/packages/core/src/render3/node_manipulation.ts @@ -7,7 +7,7 @@ */ import {assertDefined} from './assert'; -import {attachPatchData} from './context_discovery'; +import {attachPatchData, readElementValue} from './context_discovery'; import {callHooks} from './hooks'; import {LContainer, RENDER_PARENT, VIEWS, unusedValueExportToPlacateAjd as unused1} from './interfaces/container'; import {LContainerNode, LElementContainerNode, LElementNode, LTextNode, TContainerNode, TElementNode, TNode, TNodeFlags, TNodeType, TViewNode, unusedValueExportToPlacateAjd as unused2} from './interfaces/node'; @@ -15,7 +15,7 @@ import {unusedValueExportToPlacateAjd as unused3} from './interfaces/projection' import {ProceduralRenderer3, RComment, RElement, RNode, RText, Renderer3, isProceduralRenderer, unusedValueExportToPlacateAjd as unused4} from './interfaces/renderer'; import {CLEANUP, CONTAINER_INDEX, DIRECTIVES, FLAGS, HEADER_OFFSET, HOST_NODE, HookData, LViewData, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, TVIEW, unusedValueExportToPlacateAjd as unused5} from './interfaces/view'; import {assertNodeType} from './node_assert'; -import {getLNode, readElementValue, stringify} from './util'; +import {getLNode, stringify} from './util'; const unusedValueToPlacateAjd = unused1 + unused2 + unused3 + unused4 + unused5; diff --git a/packages/core/src/render3/styling.ts b/packages/core/src/render3/styling.ts index aa46782341..82e9a8ba8f 100644 --- a/packages/core/src/render3/styling.ts +++ b/packages/core/src/render3/styling.ts @@ -7,11 +7,11 @@ */ import {StyleSanitizeFn} from '../sanitization/style_sanitizer'; +import {AnimationContext} from './animations/interfaces'; import {InitialStylingFlags} from './interfaces/definition'; import {LElementNode} from './interfaces/node'; import {Renderer3, RendererStyleFlags3, isProceduralRenderer} from './interfaces/renderer'; - /** * 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` @@ -115,41 +115,47 @@ import {Renderer3, RendererStyleFlags3, isProceduralRenderer} from './interfaces * `updateStylingMap` can include new CSS properties that will be added to the context). */ export interface StylingContext extends - Array { + Array { /** * Location of element that is used as a target for this context. */ - [0]: LElementNode|null; + [StylingIndex.ElementPosition]: LElementNode|null; + + /** + * Location of animation context (which contains the active players) for this element styling + * context. + */ + [StylingIndex.AnimationContext]: AnimationContext|null; /** * The style sanitizer that is used within this context */ - [1]: StyleSanitizeFn|null; + [StylingIndex.StyleSanitizerPosition]: StyleSanitizeFn|null; /** * Location of initial data shared by all instances of this style. */ - [2]: InitialStyles; + [StylingIndex.InitialStylesPosition]: InitialStyles; /** * A numeric value representing the configuration status (whether the context is dirty or not) * mixed together (using bit shifting) with a index value which tells the starting index value * of where the multi style entries begin. */ - [3]: number; + [StylingIndex.MasterFlagPosition]: number; /** * A numeric value representing the class index offset value. Whenever a single class is * applied (using `elementClassProp`) it should have an styling index value that doesn't * need to take into account any style values that exist in the context. */ - [4]: number; + [StylingIndex.ClassOffsetPosition]: number; /** * The last CLASS STRING VALUE that was interpreted by elementStylingMap. This is cached * So that the algorithm can exit early incase the string has not changed. */ - [5]: string|null; + [StylingIndex.CachedCssClassString]: string|null; } /** @@ -185,18 +191,20 @@ export const enum StylingFlags { export const enum StylingIndex { // Position of where the initial styles are stored in the styling context ElementPosition = 0, - // Position of where the style sanitizer is stored within the styling context - StyleSanitizerPosition = 1, // Position of where the initial styles are stored in the styling context - InitialStylesPosition = 2, + AnimationContext = 1, + // Position of where the style sanitizer is stored within the styling context + StyleSanitizerPosition = 2, + // Position of where the initial styles are stored in the styling context + InitialStylesPosition = 3, // Index of location where the start of single properties are stored. (`updateStyleProp`) - MasterFlagPosition = 3, + MasterFlagPosition = 4, // Index of location where the class index offset value is located - ClassOffsetPosition = 4, + ClassOffsetPosition = 5, // Position of where the last string-based CSS class value was stored - CachedCssClassString = 5, + CachedCssClassString = 6, // Location of single (prop) value entries are stored within the context - SingleStylesStartPosition = 6, + SingleStylesStartPosition = 7, // Multi and single entries are stored in `StylingContext` as: Flag; PropertyName; PropertyValue FlagsOffset = 0, PropertyOffset = 1, @@ -223,6 +231,12 @@ export function allocStylingContext( return context; } +export function createEmptyStylingContext( + element?: LElementNode | null, sanitizer?: StyleSanitizeFn | null, + initialStylingValues?: InitialStyles): StylingContext { + return [element || null, null, sanitizer || null, initialStylingValues || [null], 0, 0, null]; +} + /** * Creates a styling context template where styling information is stored. * Any styles that are later referenced using `updateStyleProp` must be @@ -250,7 +264,8 @@ export function createStylingContextTemplate( initialStyleDeclarations?: (string | boolean | InitialStylingFlags)[] | null, styleSanitizer?: StyleSanitizeFn | null): StylingContext { const initialStylingValues: InitialStyles = [null]; - const context: StylingContext = [null, styleSanitizer || null, initialStylingValues, 0, 0, null]; + const context: StylingContext = + createEmptyStylingContext(null, styleSanitizer, initialStylingValues); // we use two maps since a class name might collide with a CSS style prop const stylesLookup: {[key: string]: number} = {}; diff --git a/packages/core/src/render3/util.ts b/packages/core/src/render3/util.ts index e71eaa6981..daea22df88 100644 --- a/packages/core/src/render3/util.ts +++ b/packages/core/src/render3/util.ts @@ -7,9 +7,13 @@ */ import {devModeEqual} from '../change_detection/change_detection_util'; -import {assertLessThan} from './assert'; + +import {assertDefined, assertLessThan} from './assert'; +import {readElementValue, readPatchedLViewData} from './context_discovery'; import {LContainerNode, LElementContainerNode, LElementNode, TNode, TNodeFlags} from './interfaces/node'; -import {HEADER_OFFSET, LViewData, TData} from './interfaces/view'; +import {CONTEXT, FLAGS, HEADER_OFFSET, LViewData, LViewFlags, PARENT, RootContext, TData} from './interfaces/view'; + + /** * Returns whether the values are different from a change detection stand point. @@ -87,10 +91,6 @@ export function loadElementInternal(index: number, arr: LViewData): LElementNode return readElementValue(value); } -export function readElementValue(value: LElementNode | any[]): LElementNode { - return (Array.isArray(value) ? (value as any as any[])[0] : value) as LElementNode; -} - export function getLNode(tNode: TNode, hostView: LViewData): LElementNode|LContainerNode| LElementContainerNode { return readElementValue(hostView[tNode.index]); @@ -103,3 +103,21 @@ export function isContentQueryHost(tNode: TNode): boolean { export function isComponent(tNode: TNode): boolean { return (tNode.flags & TNodeFlags.isComponent) === TNodeFlags.isComponent; } +/** + * Retrieve the root view from any component by walking the parent `LViewData` until + * reaching the root `LViewData`. + * + * @param component any component + */ +export function getRootView(target: LViewData | {}): LViewData { + ngDevMode && assertDefined(target, 'component'); + let lViewData = Array.isArray(target) ? (target as LViewData) : readPatchedLViewData(target) !; + while (lViewData && !(lViewData[FLAGS] & LViewFlags.IsRoot)) { + lViewData = lViewData[PARENT] !; + } + return lViewData; +} + +export function getRootContext(viewOrComponent: LViewData | {}): RootContext { + return getRootView(viewOrComponent)[CONTEXT] as RootContext; +} diff --git a/packages/core/test/bundling/animation_world/BUILD.bazel b/packages/core/test/bundling/animation_world/BUILD.bazel new file mode 100644 index 0000000000..aff48487e9 --- /dev/null +++ b/packages/core/test/bundling/animation_world/BUILD.bazel @@ -0,0 +1,53 @@ +package(default_visibility = ["//visibility:public"]) + +load("//tools:defaults.bzl", "jasmine_node_test", "ng_module", "ng_rollup_bundle", "ts_library") +load("//tools/symbol-extractor:index.bzl", "js_expected_symbol_test") +load("//tools/http-server:http_server.bzl", "http_server") + +ng_module( + name = "animation_world", + srcs = ["index.ts"], + tags = ["ivy-only"], + deps = [ + "//packages/common", + "//packages/core", + "//packages/core/test/bundling/util:reflect_metadata", + ], +) + +ng_rollup_bundle( + name = "bundle", + # TODO(alexeagle): This is inconsistent. + # We try to teach users to always have their workspace at the start of a + # path, to disambiguate from other workspaces. + # Here, the rule implementation is looking in an execroot where the layout + # has an "external" directory for external dependencies. + # This should probably start with "angular/" and let the rule deal with it. + entry_point = "packages/core/test/bundling/animation_world/index.js", + tags = ["ivy-only"], + deps = [ + ":animation_world", + "//packages/core", + ], +) + +js_expected_symbol_test( + name = "symbol_test", + src = ":bundle.min_debug.js", + golden = ":bundle.golden_symbols.json", + tags = [ + "ivy-local", + "ivy-only", + ], +) + +http_server( + name = "devserver", + data = [ + "animation_world.css", + "base.css", + "index.html", + ":bundle.min.js", + ":bundle.min_debug.js", + ], +) diff --git a/packages/core/test/bundling/animation_world/animation_world.css b/packages/core/test/bundling/animation_world/animation_world.css new file mode 100644 index 0000000000..5d96b61926 --- /dev/null +++ b/packages/core/test/bundling/animation_world/animation_world.css @@ -0,0 +1,49 @@ +html, body { + padding:0; + margin:0; + font-family: Verdana; +} + +animation-world { + display:block; + margin: 0 auto; + max-width:1000px; + border:2px solid black; +} + +animation-world nav { + padding:1em; + border-bottom:2px solid black; + background:#eee; +} + +animation-world .list { + display:grid; + grid-template-columns: 33% 33% 34%; + grid-template-rows: 33% 33% 34%; + height:1000px; +} +animation-world .record { + line-height:333px; + text-align:center; + font-weight: bold; + font-size:50px; + vertical-align: middle; +} + +.record-1 { color:white; background: green;} +.record-2 { color:white; background: red;} +.record-3 { color:white; background: blue;} +.record-4 { color:white; background: orange;} +.record-5 { color:white; background: purple;} +.record-6 { color:white; background: black;} +.record-7 { color:white; background: silver;} +.record-8 { color:white; background: teal;} +.record-9 { color:white; background: pink;} + +@keyframes fadeInOut { + from { opacity: 0; background: white; } + 33% { opacity: 1; background: black; } + 66% { opacity: 0.5; background: black; } + 100% { opacity: 1 } +} diff --git a/packages/core/test/bundling/animation_world/base.css b/packages/core/test/bundling/animation_world/base.css new file mode 100644 index 0000000000..9f6ac1bd74 --- /dev/null +++ b/packages/core/test/bundling/animation_world/base.css @@ -0,0 +1,141 @@ +hr { + margin: 20px 0; + border: 0; + border-top: 1px dashed #c5c5c5; + border-bottom: 1px dashed #f7f7f7; +} + +.learn a { + font-weight: normal; + text-decoration: none; + color: #b83f45; +} + +.learn a:hover { + text-decoration: underline; + color: #787e7e; +} + +.learn h3, +.learn h4, +.learn h5 { + margin: 10px 0; + font-weight: 500; + line-height: 1.2; + color: #000; +} + +.learn h3 { + font-size: 24px; +} + +.learn h4 { + font-size: 18px; +} + +.learn h5 { + margin-bottom: 0; + font-size: 14px; +} + +.learn ul { + padding: 0; + margin: 0 0 30px 25px; +} + +.learn li { + line-height: 20px; +} + +.learn p { + font-size: 15px; + font-weight: 300; + line-height: 1.3; + margin-top: 0; + margin-bottom: 0; +} + +#issue-count { + display: none; +} + +.quote { + border: none; + margin: 20px 0 60px 0; +} + +.quote p { + font-style: italic; +} + +.quote p:before { + content: '“'; + font-size: 50px; + opacity: .15; + position: absolute; + top: -20px; + left: 3px; +} + +.quote p:after { + content: '”'; + font-size: 50px; + opacity: .15; + position: absolute; + bottom: -42px; + right: 3px; +} + +.quote footer { + position: absolute; + bottom: -40px; + right: 0; +} + +.quote footer img { + border-radius: 3px; +} + +.quote footer a { + margin-left: 5px; + vertical-align: middle; +} + +.speech-bubble { + position: relative; + padding: 10px; + background: rgba(0, 0, 0, .04); + border-radius: 5px; +} + +.speech-bubble:after { + content: ''; + position: absolute; + top: 100%; + right: 30px; + border: 13px solid transparent; + border-top-color: rgba(0, 0, 0, .04); +} + +.learn-bar > .learn { + position: absolute; + width: 272px; + top: 8px; + left: -300px; + padding: 10px; + border-radius: 5px; + background-color: rgba(255, 255, 255, .6); + transition-property: left; + transition-duration: 500ms; +} + +@media (min-width: 899px) { + .learn-bar { + width: auto; + padding-left: 300px; + } + + .learn-bar > .learn { + left: 8px; + } +} \ No newline at end of file diff --git a/packages/core/test/bundling/animation_world/bundle.golden_symbols.json b/packages/core/test/bundling/animation_world/bundle.golden_symbols.json new file mode 100644 index 0000000000..647efb12f3 --- /dev/null +++ b/packages/core/test/bundling/animation_world/bundle.golden_symbols.json @@ -0,0 +1,1019 @@ +[ + { + "name": "ACTIVE_INDEX" + }, + { + "name": "AnimationWorldComponent" + }, + { + "name": "AnimationWorldComponent_div_Template_4" + }, + { + "name": "BINDING_INDEX" + }, + { + "name": "BLOOM_MASK" + }, + { + "name": "CIRCULAR$1" + }, + { + "name": "CLEANUP" + }, + { + "name": "CLEAN_PROMISE" + }, + { + "name": "CONTAINER_INDEX" + }, + { + "name": "CONTEXT" + }, + { + "name": "ChangeDetectionStrategy" + }, + { + "name": "ChangeDetectorRef" + }, + { + "name": "CorePlayerHandler" + }, + { + "name": "DECLARATION_VIEW" + }, + { + "name": "DIRECTIVES" + }, + { + "name": "DefaultIterableDiffer" + }, + { + "name": "DefaultIterableDifferFactory" + }, + { + "name": "EMPTY$1" + }, + { + "name": "EMPTY_ARR" + }, + { + "name": "EMPTY_ARRAY$1" + }, + { + "name": "EMPTY_OBJ" + }, + { + "name": "ElementRef" + }, + { + "name": "ElementRef$1" + }, + { + "name": "FLAGS" + }, + { + "name": "HEADER_FILLER" + }, + { + "name": "HEADER_OFFSET" + }, + { + "name": "HOST_NODE" + }, + { + "name": "INJECTOR$1" + }, + { + "name": "IterableChangeRecord_" + }, + { + "name": "IterableDiffers" + }, + { + "name": "MONKEY_PATCH_KEY_NAME" + }, + { + "name": "NEXT" + }, + { + "name": "NG_COMPONENT_DEF" + }, + { + "name": "NG_DIRECTIVE_DEF" + }, + { + "name": "NG_ELEMENT_ID" + }, + { + "name": "NG_INJECTABLE_DEF" + }, + { + "name": "NG_PIPE_DEF" + }, + { + "name": "NG_PROJECT_AS_ATTR_NAME" + }, + { + "name": "NO_CHANGE" + }, + { + "name": "NgForOf" + }, + { + "name": "NgForOfContext" + }, + { + "name": "NgModuleRef" + }, + { + "name": "NodeInjector" + }, + { + "name": "NullInjector" + }, + { + "name": "Optional" + }, + { + "name": "PARAMETERS" + }, + { + "name": "PARENT" + }, + { + "name": "PublicFeature" + }, + { + "name": "QUERIES" + }, + { + "name": "RENDERER" + }, + { + "name": "RENDER_PARENT" + }, + { + "name": "RecordViewTuple" + }, + { + "name": "Renderer2" + }, + { + "name": "RendererStyleFlags3" + }, + { + "name": "SANITIZER" + }, + { + "name": "SimpleKeyframePlayer" + }, + { + "name": "SkipSelf" + }, + { + "name": "TAIL" + }, + { + "name": "TVIEW" + }, + { + "name": "TemplateRef" + }, + { + "name": "TemplateRef$1" + }, + { + "name": "VIEWS" + }, + { + "name": "ViewContainerRef" + }, + { + "name": "ViewContainerRef$1" + }, + { + "name": "ViewEncapsulation$1" + }, + { + "name": "ViewRef" + }, + { + "name": "_CLEAN_PROMISE" + }, + { + "name": "_DuplicateItemRecordList" + }, + { + "name": "_DuplicateMap" + }, + { + "name": "_THROW_IF_NOT_FOUND" + }, + { + "name": "__extends" + }, + { + "name": "__read" + }, + { + "name": "__self" + }, + { + "name": "__spread" + }, + { + "name": "__window" + }, + { + "name": "_c0" + }, + { + "name": "_c1" + }, + { + "name": "_c2" + }, + { + "name": "_c3" + }, + { + "name": "_currentInjector" + }, + { + "name": "_currentNamespace" + }, + { + "name": "_devMode" + }, + { + "name": "_getViewData" + }, + { + "name": "_global" + }, + { + "name": "_renderCompCount" + }, + { + "name": "_symbolIterator" + }, + { + "name": "addComponentLogic" + }, + { + "name": "addPlayer" + }, + { + "name": "addRemoveViewFromContainer" + }, + { + "name": "addToViewTree" + }, + { + "name": "allocAnimationContext" + }, + { + "name": "allocStylingContext" + }, + { + "name": "appendChild" + }, + { + "name": "attachPatchData" + }, + { + "name": "baseDirectiveCreate" + }, + { + "name": "bind" + }, + { + "name": "bindingRootIndex" + }, + { + "name": "bindingUpdated" + }, + { + "name": "bloomAdd" + }, + { + "name": "bloomFindPossibleInjector" + }, + { + "name": "bloomHashBit" + }, + { + "name": "buildAnimationPlayer" + }, + { + "name": "cacheMatchingDirectivesForNode" + }, + { + "name": "cacheMatchingLocalNames" + }, + { + "name": "callHooks" + }, + { + "name": "canInsertNativeChildOfElement" + }, + { + "name": "canInsertNativeChildOfView" + }, + { + "name": "canInsertNativeNode" + }, + { + "name": "checkNoChanges" + }, + { + "name": "checkNoChangesMode" + }, + { + "name": "cleanUpView" + }, + { + "name": "componentRefresh" + }, + { + "name": "containerInternal" + }, + { + "name": "contextViewData" + }, + { + "name": "createContainerRef" + }, + { + "name": "createDirectivesAndLocals" + }, + { + "name": "createElementRef" + }, + { + "name": "createEmbeddedViewAndNode" + }, + { + "name": "createEmptyStylingContext" + }, + { + "name": "createLContainer" + }, + { + "name": "createLContext" + }, + { + "name": "createLNodeObject" + }, + { + "name": "createLViewData" + }, + { + "name": "createNodeAtIndex" + }, + { + "name": "createOutput$1" + }, + { + "name": "createRootComponent" + }, + { + "name": "createRootContext" + }, + { + "name": "createStylingContextTemplate" + }, + { + "name": "createTNode" + }, + { + "name": "createTView" + }, + { + "name": "createTemplateRef" + }, + { + "name": "createTextNode" + }, + { + "name": "createViewBlueprint" + }, + { + "name": "createViewQuery" + }, + { + "name": "createViewRef" + }, + { + "name": "defineComponent" + }, + { + "name": "defineDirective" + }, + { + "name": "defineInjectable" + }, + { + "name": "destroyLView" + }, + { + "name": "destroyViewTree" + }, + { + "name": "detachView" + }, + { + "name": "detectChanges" + }, + { + "name": "detectChangesInternal" + }, + { + "name": "diPublic" + }, + { + "name": "diPublicInInjector" + }, + { + "name": "directiveCreate" + }, + { + "name": "directiveInject" + }, + { + "name": "discoverDirectiveIndices" + }, + { + "name": "discoverDirectives" + }, + { + "name": "domRendererFactory3" + }, + { + "name": "elementCreate" + }, + { + "name": "elementEnd" + }, + { + "name": "elementProperty" + }, + { + "name": "elementStart" + }, + { + "name": "elementStyling" + }, + { + "name": "elementStylingApply" + }, + { + "name": "elementStylingMap" + }, + { + "name": "enterView" + }, + { + "name": "executeHooks" + }, + { + "name": "executeInitAndContentHooks" + }, + { + "name": "executeInitHooks" + }, + { + "name": "executeNodeAction" + }, + { + "name": "executeOnDestroys" + }, + { + "name": "executePipeOnDestroys" + }, + { + "name": "extendStatics" + }, + { + "name": "extractDirectiveDef" + }, + { + "name": "extractPipeDef" + }, + { + "name": "findAttrIndexInNode" + }, + { + "name": "findComponentView" + }, + { + "name": "findDirectiveMatches" + }, + { + "name": "findEntryPositionByProp" + }, + { + "name": "findViaComponent" + }, + { + "name": "findViaDirective" + }, + { + "name": "findViaNativeElement" + }, + { + "name": "firstTemplatePass" + }, + { + "name": "generateInitialInputs" + }, + { + "name": "generatePropertyAliases" + }, + { + "name": "getBeforeNodeForView" + }, + { + "name": "getCleanup" + }, + { + "name": "getClosureSafeProperty" + }, + { + "name": "getComponentDef" + }, + { + "name": "getContainerNode" + }, + { + "name": "getContainerRenderParent" + }, + { + "name": "getContext" + }, + { + "name": "getCurrentSanitizer" + }, + { + "name": "getDirectiveDef" + }, + { + "name": "getDirectiveEndIndex" + }, + { + "name": "getDirectiveStartIndex" + }, + { + "name": "getHighestElementContainer" + }, + { + "name": "getHostElementNode" + }, + { + "name": "getInitialIndex" + }, + { + "name": "getInitialValue" + }, + { + "name": "getInjectableDef" + }, + { + "name": "getLElementFromComponent" + }, + { + "name": "getLNode" + }, + { + "name": "getLNodeFromViewData" + }, + { + "name": "getLViewChild" + }, + { + "name": "getMultiOrSingleIndex" + }, + { + "name": "getMultiStartIndex" + }, + { + "name": "getOrCreateAnimationContext" + }, + { + "name": "getOrCreateInjectable" + }, + { + "name": "getOrCreateNodeInjector" + }, + { + "name": "getOrCreateNodeInjectorForNode" + }, + { + "name": "getOrCreateRenderer2" + }, + { + "name": "getOrCreateTView" + }, + { + "name": "getParentLNode" + }, + { + "name": "getParentOrContainerNode" + }, + { + "name": "getParentState" + }, + { + "name": "getPipeDef" + }, + { + "name": "getPointers" + }, + { + "name": "getPreviousIndex" + }, + { + "name": "getPreviousOrParentNode" + }, + { + "name": "getPreviousOrParentTNode" + }, + { + "name": "getProp" + }, + { + "name": "getRenderFlags" + }, + { + "name": "getRenderParent" + }, + { + "name": "getRenderer" + }, + { + "name": "getRendererFactory" + }, + { + "name": "getRootContext" + }, + { + "name": "getRootView" + }, + { + "name": "getStyleSanitizer" + }, + { + "name": "getStylingContext" + }, + { + "name": "getSymbolIterator" + }, + { + "name": "getTNode" + }, + { + "name": "getTViewCleanup" + }, + { + "name": "getTypeNameForDebugging" + }, + { + "name": "getTypeNameForDebugging$1" + }, + { + "name": "getValue" + }, + { + "name": "hasValueChanged" + }, + { + "name": "hostElement" + }, + { + "name": "inject" + }, + { + "name": "injectElementRef" + }, + { + "name": "injectTemplateRef" + }, + { + "name": "injectViewContainerRef" + }, + { + "name": "insertNewMultiProperty" + }, + { + "name": "insertView" + }, + { + "name": "instantiateDirectivesDirectly" + }, + { + "name": "interpolation1" + }, + { + "name": "invertObject" + }, + { + "name": "isClassBased" + }, + { + "name": "isComponent" + }, + { + "name": "isComponentInstance" + }, + { + "name": "isContentQueryHost" + }, + { + "name": "isContextDirty" + }, + { + "name": "isCssClassMatching" + }, + { + "name": "isDevMode" + }, + { + "name": "isDifferent" + }, + { + "name": "isDirectiveInstance" + }, + { + "name": "isDirty" + }, + { + "name": "isJsObject" + }, + { + "name": "isListLikeIterable" + }, + { + "name": "isNodeMatchingSelector" + }, + { + "name": "isNodeMatchingSelectorList" + }, + { + "name": "isPositive" + }, + { + "name": "isProceduralRenderer" + }, + { + "name": "isSanitizable" + }, + { + "name": "iterateListLike" + }, + { + "name": "leaveView" + }, + { + "name": "listener" + }, + { + "name": "load" + }, + { + "name": "loadElement" + }, + { + "name": "loadElementInternal" + }, + { + "name": "loadInternal" + }, + { + "name": "locateHostElement" + }, + { + "name": "looseIdentical" + }, + { + "name": "makeMetadataCtor" + }, + { + "name": "makeParamDecorator" + }, + { + "name": "markDirtyIfOnPush" + }, + { + "name": "markViewDirty" + }, + { + "name": "namespaceHTML" + }, + { + "name": "nativeInsertBefore" + }, + { + "name": "nativeNodeLocalRefExtractor" + }, + { + "name": "nextContext" + }, + { + "name": "nextNgElementId" + }, + { + "name": "pointers" + }, + { + "name": "prepareInitialFlag" + }, + { + "name": "projectionNodeStack" + }, + { + "name": "queueComponentIndexForCheck" + }, + { + "name": "queueContentHooks" + }, + { + "name": "queueDestroyHooks" + }, + { + "name": "queueHostBindingForCheck" + }, + { + "name": "queueInitHooks" + }, + { + "name": "queueLifecycleHooks" + }, + { + "name": "queueViewHooks" + }, + { + "name": "readElementValue" + }, + { + "name": "readPatchedData" + }, + { + "name": "readPatchedLViewData" + }, + { + "name": "refreshChildComponents" + }, + { + "name": "refreshContentQueries" + }, + { + "name": "refreshDescendantViews" + }, + { + "name": "refreshDynamicEmbeddedViews" + }, + { + "name": "removeListeners" + }, + { + "name": "removeView" + }, + { + "name": "renderComponent" + }, + { + "name": "renderComponentOrTemplate" + }, + { + "name": "renderEmbeddedTemplate" + }, + { + "name": "renderStyling" + }, + { + "name": "resetComponentState" + }, + { + "name": "resolveDirective" + }, + { + "name": "sameHostView" + }, + { + "name": "saveNameToExportMap" + }, + { + "name": "saveResolvedLocalsInData" + }, + { + "name": "scheduleTick" + }, + { + "name": "searchMatchesQueuedForCreation" + }, + { + "name": "setAnimationPlayState" + }, + { + "name": "setClass" + }, + { + "name": "setContextDirty" + }, + { + "name": "setCurrentInjector" + }, + { + "name": "setDirty" + }, + { + "name": "setFlag" + }, + { + "name": "setHostBindings" + }, + { + "name": "setInputsForProperty" + }, + { + "name": "setInputsFromAttrs" + }, + { + "name": "setProp" + }, + { + "name": "setStyle" + }, + { + "name": "setUpAttributes" + }, + { + "name": "setValue" + }, + { + "name": "storeCleanupFn" + }, + { + "name": "storeCleanupWithContext" + }, + { + "name": "stringify$1" + }, + { + "name": "stringify$2" + }, + { + "name": "swapMultiContextEntries" + }, + { + "name": "template" + }, + { + "name": "text" + }, + { + "name": "textBinding" + }, + { + "name": "throwCyclicDependencyError" + }, + { + "name": "throwErrorIfNoChangesMode" + }, + { + "name": "throwMultipleComponentError" + }, + { + "name": "tickRootContext" + }, + { + "name": "trackByIdentity" + }, + { + "name": "traverseNextElement" + }, + { + "name": "updateSinglePointerValues" + }, + { + "name": "updateStylingMap" + }, + { + "name": "updateViewQuery" + }, + { + "name": "valueExists" + }, + { + "name": "viewAttached" + }, + { + "name": "walkTNodeTree" + }, + { + "name": "walkUpViews" + }, + { + "name": "wrapListenerWithPreventDefault" + } +] \ No newline at end of file diff --git a/packages/core/test/bundling/animation_world/index.html b/packages/core/test/bundling/animation_world/index.html new file mode 100644 index 0000000000..ce1cabc646 --- /dev/null +++ b/packages/core/test/bundling/animation_world/index.html @@ -0,0 +1,32 @@ + + + + + + Angular Hello World Example + + + + + + + + + diff --git a/packages/core/test/bundling/animation_world/index.ts b/packages/core/test/bundling/animation_world/index.ts new file mode 100644 index 0000000000..300337232e --- /dev/null +++ b/packages/core/test/bundling/animation_world/index.ts @@ -0,0 +1,136 @@ +/** + * @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 '@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'; + +@Component({ + selector: 'animation-world', + template: ` + +
+
+ {{ item }} +
+
+ `, +}) +class AnimationWorldComponent { + items: any[] = [1, 2, 3, 4, 5, 6, 7, 8, 9]; + private _hostElement: HTMLElement; + + constructor(element: ElementRef) { this._hostElement = element.nativeElement; } + + makeClass(index: number) { return `record-${index}`; } + + doAnimate() { + const elements = this._hostElement.querySelectorAll('div.record') as any as HTMLElement[]; + for (let i = 0; i < elements.length; i++) { + const element = elements[i]; + const delay = i * 100; + const player = buildAnimationPlayer(element, 'fadeInOut', `500ms ease-out ${delay}ms both`); + addPlayer(element, player); + } + } +} + +@NgModule({declarations: [AnimationWorldComponent], imports: [CommonModule]}) +class AnimationWorldModule { +} + + +function buildAnimationPlayer(element: HTMLElement, animationName: string, time: string): Player { + return new SimpleKeyframePlayer(element, animationName, time); +} + +class SimpleKeyframePlayer implements Player { + state = PlayState.Pending; + parent: Player|null = null; + 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; + const animationFn = (event: AnimationEvent) => { + if (event.animationName == this._animationName) { + this._element.removeEventListener('animationend', animationFn); + this.finish(); + } + }; + this._element.addEventListener('animationend', animationFn); + } + 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) { + setAnimationPlayState(this._element, 'running'); + this.state = PlayState.Running; + this._emit(this.state); + } + } + pause(): void { + if (this.state != PlayState.Paused) { + setAnimationPlayState(this._element, '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()); + } +} + +function setAnimationPlayState(element: HTMLElement, state: string) { + element.style.animationPlayState = state; +} + +class AnimationDebugger implements PlayerHandler { + private _players: Player[] = []; + + flushPlayers() { + this._players.forEach(player => { + if (!player.parent) { + player.play(); + } + }); + this._players.length = 0; + } + + queuePlayer(player: Player): void { this._players.push(player); } +} + +const playerHandler = new AnimationDebugger(); +renderComponent(AnimationWorldComponent, {playerHandler}); diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 418f627aad..a634c912b7 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -416,6 +416,9 @@ { "name": "createEmbeddedViewAndNode" }, + { + "name": "createEmptyStylingContext" + }, { "name": "createLContainer" }, diff --git a/packages/core/test/bundling/todo_r2/bundle.golden_symbols.json b/packages/core/test/bundling/todo_r2/bundle.golden_symbols.json index 0cad4f25ca..165106c70d 100644 --- a/packages/core/test/bundling/todo_r2/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo_r2/bundle.golden_symbols.json @@ -1286,6 +1286,9 @@ { "name": "createEmbeddedViewAndNode" }, + { + "name": "createEmptyStylingContext" + }, { "name": "createInjector" }, diff --git a/packages/core/test/render3/animations/core_player_handler_spec.ts b/packages/core/test/render3/animations/core_player_handler_spec.ts new file mode 100644 index 0000000000..2119476e88 --- /dev/null +++ b/packages/core/test/render3/animations/core_player_handler_spec.ts @@ -0,0 +1,60 @@ +/** + * @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 {CorePlayerHandler} from '../../../src/render3/animations/core_player_handler'; +import {PlayState} from '../../../src/render3/animations/interfaces'; +import {MockPlayer} from './mock_player'; + +describe('CorePlayerHandler', () => { + it('should kick off any animation players that have been queued once flushed', () => { + const handler = new CorePlayerHandler(); + const p1 = new MockPlayer(); + const p2 = new MockPlayer(); + + expect(p1.state).toEqual(PlayState.Pending); + expect(p2.state).toEqual(PlayState.Pending); + + handler.queuePlayer(p1); + handler.queuePlayer(p2); + + expect(p1.state).toEqual(PlayState.Pending); + expect(p2.state).toEqual(PlayState.Pending); + + handler.flushPlayers(); + + expect(p1.state).toEqual(PlayState.Running); + expect(p2.state).toEqual(PlayState.Running); + }); + + it('should only kick off animation players that have not been adopted by a parent player once flushed', + () => { + const handler = new CorePlayerHandler(); + const pRoot = new MockPlayer(); + const p1 = new MockPlayer(); + const p2 = new MockPlayer(); + + expect(pRoot.state).toEqual(PlayState.Pending); + expect(p1.state).toEqual(PlayState.Pending); + expect(p2.state).toEqual(PlayState.Pending); + + handler.queuePlayer(pRoot); + handler.queuePlayer(p1); + handler.queuePlayer(p2); + + expect(pRoot.state).toEqual(PlayState.Pending); + expect(p1.state).toEqual(PlayState.Pending); + expect(p2.state).toEqual(PlayState.Pending); + + p1.parent = pRoot; + + handler.flushPlayers(); + + expect(pRoot.state).toEqual(PlayState.Running); + expect(p1.state).toEqual(PlayState.Pending); + expect(p2.state).toEqual(PlayState.Running); + }); +}); diff --git a/packages/core/test/render3/animations/mock_player.ts b/packages/core/test/render3/animations/mock_player.ts new file mode 100644 index 0000000000..1836adeff4 --- /dev/null +++ b/packages/core/test/render3/animations/mock_player.ts @@ -0,0 +1,58 @@ +/** + * @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 {PlayState, Player} from '../../../src/render3/animations/interfaces'; + +export class MockPlayer implements Player { + parent: Player|null = null; + + log: string[] = []; + state: PlayState = PlayState.Pending; + private _listeners: {[state: string]: (() => any)[]} = {}; + + play(): void { + if (this.state === PlayState.Running) return; + + this.state = PlayState.Running; + this._emit(PlayState.Running); + } + + pause(): void { + if (this.state === PlayState.Paused) return; + + this.state = PlayState.Paused; + this._emit(PlayState.Paused); + } + + finish(): void { + if (this.state >= PlayState.Finished) return; + + this.state = PlayState.Finished; + this._emit(PlayState.Finished); + } + + destroy(): void { + if (this.state >= PlayState.Destroyed) return; + + this.state = PlayState.Destroyed; + this._emit(PlayState.Destroyed); + } + + addEventListener(state: PlayState|number, cb: () => any): void { + const key = state.toString(); + const arr = this._listeners[key] || (this._listeners[key] = []); + arr.push(cb); + } + + private _emit(state: PlayState) { + const callbacks = this._listeners[state] || []; + for (let i = 0; i < callbacks.length; i++) { + const cb = callbacks[i]; + cb(); + } + } +} diff --git a/packages/core/test/render3/animations/players_spec.ts b/packages/core/test/render3/animations/players_spec.ts new file mode 100644 index 0000000000..4d59be1b6a --- /dev/null +++ b/packages/core/test/render3/animations/players_spec.ts @@ -0,0 +1,311 @@ +/** + * @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 {RenderFlags} from '@angular/core/src/render3'; +import {AnimationContext, PlayState, Player, PlayerHandler} from '../../../src/render3/animations/interfaces'; +import {addPlayer, getOrCreateAnimationContext, getPlayers} from '../../../src/render3/animations/players'; +import {QUERY_READ_FROM_NODE, defineComponent, getHostElement} from '../../../src/render3/index'; +import {element, elementEnd, elementStart, elementStyling, elementStylingApply, load, markDirty} from '../../../src/render3/instructions'; +import {RElement} from '../../../src/render3/interfaces/renderer'; +import {QueryList, query, queryRefresh} from '../../../src/render3/query'; +import {ComponentFixture} from '../render_util'; +import {MockPlayer} from './mock_player'; + +describe('animation player access', () => { + it('should add a player to the element', () => { + const element = buildElement(); + expect(getPlayers(element)).toEqual([]); + + const player = new MockPlayer(); + addPlayer(element, player); + expect(getPlayers(element)).toEqual([player]); + }); + + it('should add a player to the component host element', () => { + const fixture = buildSuperComponent(); + const superComp = fixture.component; + const component = superComp.query.first as Comp; + + expect(component.name).toEqual('child-comp'); + expect(getPlayers(component)).toEqual([]); + + const player = new MockPlayer(); + addPlayer(component, player); + expect(getPlayers(component)).toEqual([player]); + + const hostElement = getHostElement(component); + expect(getPlayers(hostElement)).toEqual([player]); + }); + + it('should add a player to an element that already contains styling', () => { + const element = buildElementWithStyling(); + expect(getPlayers(element)).toEqual([]); + + const player = new MockPlayer(); + addPlayer(element, player); + expect(getPlayers(element)).toEqual([player]); + }); + + it('should add a player to the element animation context and remove it once it completes', () => { + const element = buildElement(); + const context = getOrCreateAnimationContext(element); + expect(context).toEqual([]); + + const player = new MockPlayer(); + addPlayer(element, player); + expect(readPlayers(context)).toEqual([player]); + + player.destroy(); + expect(readPlayers(context)).toEqual([]); + }); + + it('should flush all pending animation players after change detection', () => { + const fixture = buildComponent(); + const element = fixture.hostElement.querySelector('div') !; + + const player = new MockPlayer(); + addPlayer(element, player); + + expect(player.state).toEqual(PlayState.Pending); + fixture.update(); + expect(player.state).toEqual(PlayState.Running); + }); + + it('should flush all animations in the given animation handler is apart of the component', () => { + const handler = new MockPlayerHandler(); + + const fixture = new ComponentFixture(Comp, {playerHandler: handler}); + fixture.update(); + + const element = fixture.hostElement.querySelector('div') !; + + const p1 = new MockPlayer(); + const p2 = new MockPlayer(); + + addPlayer(element, p1); + addPlayer(element, p2); + expect(p1.state).toEqual(PlayState.Pending); + expect(p2.state).toEqual(PlayState.Pending); + + fixture.update(); + expect(p1.state).toEqual(PlayState.Pending); + expect(p2.state).toEqual(PlayState.Pending); + + expect(handler.lastFlushedPlayers).toEqual([p1, p2]); + }); + + it('should only play animation players that are not associated with a parent player', () => { + const fixture = buildComponent(); + const element = fixture.hostElement.querySelector('div') !; + + const p1 = new MockPlayer(); + const p2 = new MockPlayer(); + const pParent = new MockPlayer(); + p1.parent = pParent; + + addPlayer(element, p1); + addPlayer(element, p2); + addPlayer(element, pParent); + + expect(p1.state).toEqual(PlayState.Pending); + expect(p2.state).toEqual(PlayState.Pending); + expect(pParent.state).toEqual(PlayState.Pending); + + fixture.update(); + + expect(p1.state).toEqual(PlayState.Pending); + expect(p2.state).toEqual(PlayState.Running); + expect(pParent.state).toEqual(PlayState.Running); + }); + + it('should not replay any previously queued players once change detection has run', () => { + const fixture = buildComponent(); + const element = fixture.hostElement.querySelector('div') !; + + const p1 = new MockPlayer(); + const p2 = new MockPlayer(); + const p3 = new MockPlayer(); + + addPlayer(element, p1); + addPlayer(element, p2); + + expect(p1.state).toEqual(PlayState.Pending); + expect(p2.state).toEqual(PlayState.Pending); + expect(p3.state).toEqual(PlayState.Pending); + + fixture.update(); + + expect(p1.state).toEqual(PlayState.Running); + expect(p2.state).toEqual(PlayState.Running); + expect(p3.state).toEqual(PlayState.Pending); + + p1.pause(); + p2.pause(); + addPlayer(element, p3); + + expect(p1.state).toEqual(PlayState.Paused); + expect(p2.state).toEqual(PlayState.Paused); + expect(p3.state).toEqual(PlayState.Pending); + + fixture.update(); + + expect(p1.state).toEqual(PlayState.Paused); + expect(p2.state).toEqual(PlayState.Paused); + expect(p3.state).toEqual(PlayState.Running); + }); + + it('should not run change detection on a template if only players are being added', () => { + const fixture = buildComponent(); + const element = fixture.hostElement.querySelector('div') !; + + let dcCount = 0; + fixture.component.logger = () => { dcCount++; }; + + const p1 = new MockPlayer(); + addPlayer(element, p1); + + expect(p1.state).toEqual(PlayState.Pending); + expect(dcCount).toEqual(0); + + fixture.requestAnimationFrame.flush(); + + expect(p1.state).toEqual(PlayState.Running); + expect(dcCount).toEqual(0); + + const p2 = new MockPlayer(); + addPlayer(element, p2); + markDirty(fixture.component); + + expect(p2.state).toEqual(PlayState.Pending); + + fixture.requestAnimationFrame.flush(); + + expect(p2.state).toEqual(PlayState.Running); + expect(p1.state).toEqual(PlayState.Running); + expect(dcCount).toEqual(1); + + const p3 = new MockPlayer(); + addPlayer(element, p3); + + fixture.requestAnimationFrame.flush(); + + expect(p3.state).toEqual(PlayState.Running); + expect(p2.state).toEqual(PlayState.Running); + expect(p1.state).toEqual(PlayState.Running); + + expect(dcCount).toEqual(1); + }); +}); + +function buildElement() { + return buildComponent().hostElement.querySelector('div') as RElement; +} + +function buildComponent() { + const fixture = new ComponentFixture(Comp); + fixture.update(); + return fixture; +} + +function buildSuperComponent() { + const fixture = new ComponentFixture(SuperComp); + fixture.update(); + return fixture; +} + +function buildElementWithStyling() { + const fixture = new ComponentFixture(CompWithStyling); + fixture.update(); + return fixture.hostElement.querySelector('div') as RElement; +} + +function readPlayers(context: AnimationContext): Player[] { + return context; +} + +class Comp { + static ngComponentDef = defineComponent({ + type: Comp, + exportAs: 'child', + selectors: [['child-comp']], + factory: () => new Comp(), + consts: 1, + vars: 0, + template: (rf: RenderFlags, ctx: Comp) => { + if (rf & RenderFlags.Create) { + element(0, 'div'); + } + ctx.logger(); + } + }); + + name = 'child-comp'; + logger: () => any = () => {}; +} + +class CompWithStyling { + static ngComponentDef = defineComponent({ + type: CompWithStyling, + exportAs: 'child-styled', + selectors: [['child-styled-comp']], + factory: () => new CompWithStyling(), + consts: 1, + vars: 0, + template: (rf: RenderFlags, ctx: CompWithStyling) => { + if (rf & RenderFlags.Create) { + elementStart(0, 'div'); + elementStyling(['fooClass']); + elementEnd(); + } + if (rf & RenderFlags.Update) { + elementStylingApply(0); + } + } + }); + + name = 'child-styled-comp'; +} + +class SuperComp { + static ngComponentDef = defineComponent({ + type: SuperComp, + selectors: [['super-comp']], + factory: () => new SuperComp(), + consts: 3, + vars: 0, + template: (rf: RenderFlags, ctx: SuperComp) => { + if (rf & RenderFlags.Create) { + elementStart(1, 'div'); + element(2, 'child-comp', ['child', ''], ['child', 'child']); + elementEnd(); + } + }, + viewQuery: function(rf: RenderFlags, ctx: SuperComp) { + if (rf & RenderFlags.Create) { + query(0, ['child'], true, QUERY_READ_FROM_NODE); + } + if (rf & RenderFlags.Update) { + let tmp: any; + queryRefresh(tmp = load>(0)) && (ctx.query = tmp as QueryList); + } + }, + directives: [Comp] + }); + + name = 'super-comp'; + query !: QueryList; +} + +class MockPlayerHandler implements PlayerHandler { + players: Player[] = []; + lastFlushedPlayers: Player[] = []; + flushPlayers(): void { + this.lastFlushedPlayers = [...this.players]; + this.players = []; + } + queuePlayer(player: Player): void { this.players.push(player); } +} diff --git a/packages/core/test/render3/render_util.ts b/packages/core/test/render3/render_util.ts index b86e0a0b72..71b3e96aab 100644 --- a/packages/core/test/render3/render_util.ts +++ b/packages/core/test/render3/render_util.ts @@ -9,6 +9,7 @@ import {stringifyElement} from '@angular/platform-browser/testing/src/browser_util'; import {Injector} from '../../src/di/injector'; +import {PlayerHandler} from '../../src/render3/animations/interfaces'; import {CreateComponentOptions} from '../../src/render3/component'; import {getContext, isComponentInstance} from '../../src/render3/context_discovery'; import {extractDirectiveDef, extractPipeDef} from '../../src/render3/definition'; @@ -103,9 +104,12 @@ export class ComponentFixture extends BaseFixture { component: T; requestAnimationFrame: {(fn: () => void): void; flush(): void; queue: (() => void)[];}; - constructor( - private componentType: ComponentType, - opts: {injector?: Injector, sanitizer?: Sanitizer, rendererFactory?: RendererFactory3} = {}) { + constructor(private componentType: ComponentType, opts: { + injector?: Injector, + sanitizer?: Sanitizer, + rendererFactory?: RendererFactory3, + playerHandler?: PlayerHandler + } = {}) { super(); this.requestAnimationFrame = function(fn: () => void) { requestAnimationFrame.queue.push(fn); @@ -122,7 +126,8 @@ export class ComponentFixture extends BaseFixture { scheduler: this.requestAnimationFrame, injector: opts.injector, sanitizer: opts.sanitizer, - rendererFactory: opts.rendererFactory || domRendererFactory3 + rendererFactory: opts.rendererFactory || domRendererFactory3, + playerHandler: opts.playerHandler }); } diff --git a/packages/core/test/render3/styling_spec.ts b/packages/core/test/render3/styling_spec.ts index e2d8a52ea9..47c673b57d 100644 --- a/packages/core/test/render3/styling_spec.ts +++ b/packages/core/test/render3/styling_spec.ts @@ -109,7 +109,7 @@ describe('styling', () => { describe('createStylingContextTemplate', () => { it('should initialize empty template', () => { const template = initContext(); - expect(template).toEqual([element, null, [null], cleanStyle(0, 6), 0, null]); + expect(template).toEqual([element, null, null, [null], cleanStyle(0, 7), 0, null]); }); it('should initialize static styles', () => { @@ -118,28 +118,29 @@ describe('styling', () => { expect(template).toEqual([ element, null, + null, [null, 'red', '10px'], - dirtyStyle(0, 12), // + dirtyStyle(0, 13), // 0, null, - // #6 - cleanStyle(1, 12), + // #7 + cleanStyle(1, 13), 'color', null, - // #9 - cleanStyle(2, 15), + // #10 + cleanStyle(2, 16), 'width', null, - // #12 - dirtyStyle(1, 6), + // #13 + dirtyStyle(1, 7), 'color', null, - // #15 - dirtyStyle(2, 9), + // #16 + dirtyStyle(2, 10), 'width', null, ]); @@ -320,28 +321,29 @@ describe('styling', () => { expect(stylingContext).toEqual([ element, null, + null, [null], - dirtyStyle(0, 12), // + dirtyStyle(0, 13), // 2, null, - // #6 - cleanStyle(0, 12), + // #7 + cleanStyle(0, 13), 'width', null, - // #9 - cleanStyle(0, 15), + // #10 + cleanStyle(0, 16), 'height', null, - // #12 - dirtyStyle(0, 6), + // #13 + dirtyStyle(0, 7), 'width', '100px', - // #15 - dirtyStyle(0, 9), + // #16 + dirtyStyle(0, 10), 'height', '100px', ]); @@ -352,33 +354,34 @@ describe('styling', () => { expect(stylingContext).toEqual([ element, null, + null, [null], - dirtyStyle(0, 12), // + dirtyStyle(0, 13), // 2, null, - // #6 - cleanStyle(0, 12), + // #7 + cleanStyle(0, 13), 'width', null, - // #9 - cleanStyle(0, 18), + // #10 + cleanStyle(0, 19), 'height', null, - // #12 - dirtyStyle(0, 6), + // #13 + dirtyStyle(0, 7), 'width', '200px', - // #15 + // #16 dirtyStyle(), 'opacity', '0', - // #18 - dirtyStyle(0, 9), + // #19 + dirtyStyle(0, 10), 'height', null, ]); @@ -387,33 +390,34 @@ describe('styling', () => { expect(stylingContext).toEqual([ element, null, + null, [null], - cleanStyle(0, 12), // + cleanStyle(0, 13), // 2, null, - // #6 - cleanStyle(0, 12), + // #7 + cleanStyle(0, 13), 'width', null, - // #9 - cleanStyle(0, 18), + // #10 + cleanStyle(0, 19), 'height', null, - // #12 - cleanStyle(0, 6), + // #13 + cleanStyle(0, 7), 'width', '200px', - // #15 + // #16 cleanStyle(), 'opacity', '0', - // #18 - cleanStyle(0, 9), + // #19 + cleanStyle(0, 10), 'height', null, ]); @@ -424,33 +428,34 @@ describe('styling', () => { expect(stylingContext).toEqual([ element, null, + null, [null], - dirtyStyle(0, 12), // + dirtyStyle(0, 13), // 2, null, - // #6 - dirtyStyle(0, 12), + // #7 + dirtyStyle(0, 13), 'width', '300px', - // #9 - cleanStyle(0, 18), + // #10 + cleanStyle(0, 19), 'height', null, - // #12 - cleanStyle(0, 6), + // #13 + cleanStyle(0, 7), 'width', null, - // #15 + // #16 dirtyStyle(), 'opacity', null, - // #18 - cleanStyle(0, 9), + // #19 + cleanStyle(0, 10), 'height', null, ]); @@ -461,33 +466,34 @@ describe('styling', () => { expect(stylingContext).toEqual([ element, null, + null, [null], - dirtyStyle(0, 12), // + dirtyStyle(0, 13), // 2, null, - // #6 - dirtyStyle(0, 12), + // #7 + dirtyStyle(0, 13), 'width', null, - // #9 - cleanStyle(0, 18), + // #10 + cleanStyle(0, 19), 'height', null, - // #12 - cleanStyle(0, 6), + // #13 + cleanStyle(0, 7), 'width', null, - // #15 + // #16 cleanStyle(), 'opacity', null, - // #18 - cleanStyle(0, 9), + // #19 + cleanStyle(0, 10), 'height', null, ]); @@ -503,33 +509,34 @@ describe('styling', () => { expect(stylingContext).toEqual([ element, null, + null, [null], - dirtyStyle(0, 9), // + dirtyStyle(0, 10), // 1, null, - // #6 - cleanStyle(0, 18), + // #7 + cleanStyle(0, 19), 'lineHeight', null, - // #9 + // #10 dirtyStyle(), 'width', '100px', - // #12 + // #13 dirtyStyle(), 'height', '100px', - // #15 + // #16 dirtyStyle(), 'opacity', '0.5', - // #18 - cleanStyle(0, 6), + // #19 + cleanStyle(0, 7), 'lineHeight', null, ]); @@ -540,33 +547,34 @@ describe('styling', () => { expect(stylingContext).toEqual([ element, null, + null, [null], - dirtyStyle(0, 9), // + dirtyStyle(0, 10), // 1, null, - // #6 - cleanStyle(0, 18), + // #7 + cleanStyle(0, 19), 'lineHeight', null, - // #9 + // #10 dirtyStyle(), 'width', null, - // #12 + // #13 dirtyStyle(), 'height', null, - // #15 + // #16 dirtyStyle(), 'opacity', null, - // #18 - cleanStyle(0, 6), + // #19 + cleanStyle(0, 7), 'lineHeight', null, ]); @@ -579,38 +587,39 @@ describe('styling', () => { expect(stylingContext).toEqual([ element, null, + null, [null], - dirtyStyle(0, 9), // + dirtyStyle(0, 10), // 1, null, - // #6 - cleanStyle(0, 21), + // #7 + cleanStyle(0, 22), 'lineHeight', null, - // #9 + // #10 dirtyStyle(), 'borderWidth', '5px', - // #12 + // #13 cleanStyle(), 'width', null, - // #15 + // #16 cleanStyle(), 'height', null, - // #18 + // #19 cleanStyle(), 'opacity', null, - // #21 - cleanStyle(0, 6), + // #22 + cleanStyle(0, 7), 'lineHeight', null, ]); @@ -620,38 +629,39 @@ describe('styling', () => { expect(stylingContext).toEqual([ element, null, + null, [null], - dirtyStyle(0, 9), // + dirtyStyle(0, 10), // 1, null, - // #6 - dirtyStyle(0, 21), + // #7 + dirtyStyle(0, 22), 'lineHeight', '200px', - // #9 + // #10 dirtyStyle(), 'borderWidth', '5px', - // #12 + // #13 cleanStyle(), 'width', null, - // #15 + // #16 cleanStyle(), 'height', null, - // #18 + // #19 cleanStyle(), 'opacity', null, - // #21 - cleanStyle(0, 6), + // #22 + cleanStyle(0, 7), 'lineHeight', null, ]); @@ -661,43 +671,44 @@ describe('styling', () => { expect(stylingContext).toEqual([ element, null, + null, [null], - dirtyStyle(0, 9), // + dirtyStyle(0, 10), // 1, null, - // #6 - dirtyStyle(0, 24), + // #7 + dirtyStyle(0, 25), 'lineHeight', '200px', - // #9 + // #10 dirtyStyle(), 'borderWidth', '15px', - // #12 + // #13 dirtyStyle(), 'borderColor', 'red', - // #15 + // #16 cleanStyle(), 'width', null, - // #18 + // #19 cleanStyle(), 'height', null, - // #21 + // #22 cleanStyle(), 'opacity', null, - // #24 - cleanStyle(0, 6), + // #25 + cleanStyle(0, 7), 'lineHeight', null, ]); @@ -716,23 +727,24 @@ describe('styling', () => { expect(stylingContext).toEqual([ element, null, + null, [null], - dirtyStyle(0, 9), // + dirtyStyle(0, 10), // 1, null, - // #6 - dirtyStyle(0, 12), + // #7 + dirtyStyle(0, 13), 'height', '200px', - // #6 + // #7 dirtyStyle(), 'width', '100px', - // #12 - cleanStyle(0, 6), + // #13 + cleanStyle(0, 7), 'height', null, ]); @@ -742,23 +754,24 @@ describe('styling', () => { expect(stylingContext).toEqual([ element, null, + null, [null], - cleanStyle(0, 9), // + cleanStyle(0, 10), // 1, null, - // #6 - cleanStyle(0, 12), + // #7 + cleanStyle(0, 13), 'height', '200px', - // #6 + // #7 cleanStyle(), 'width', '100px', - // #12 - cleanStyle(0, 6), + // #13 + cleanStyle(0, 7), 'height', null, ]); @@ -776,29 +789,30 @@ describe('styling', () => { expect(stylingContext).toEqual([ element, + null, styleSanitizer, [null], - dirtyStyle(0, 12), // + dirtyStyle(0, 13), // 2, null, - // #6 - dirtyStyleWithSanitization(0, 12), + // #7 + dirtyStyleWithSanitization(0, 13), 'border-image', 'url(foo.jpg)', - // #9 - dirtyStyle(0, 15), + // #10 + dirtyStyle(0, 16), 'border-width', '100px', - // #12 - cleanStyleWithSanitization(0, 6), + // #13 + cleanStyleWithSanitization(0, 7), 'border-image', null, - // #15 - cleanStyle(0, 9), + // #16 + cleanStyle(0, 10), 'border-width', null, ]); @@ -807,34 +821,35 @@ describe('styling', () => { expect(stylingContext).toEqual([ element, + null, styleSanitizer, [null], - dirtyStyle(0, 12), // + dirtyStyle(0, 13), // 2, null, - // #6 - dirtyStyleWithSanitization(0, 15), + // #7 + dirtyStyleWithSanitization(0, 16), 'border-image', 'url(foo.jpg)', - // #9 - dirtyStyle(0, 18), + // #10 + dirtyStyle(0, 19), 'border-width', '100px', - // #12 + // #13 dirtyStyleWithSanitization(0, 0), 'background-image', 'unsafe', - // #15 - cleanStyleWithSanitization(0, 6), + // #16 + cleanStyleWithSanitization(0, 7), 'border-image', null, - // #18 - cleanStyle(0, 9), + // #19 + cleanStyle(0, 10), 'border-width', null, ]); @@ -843,34 +858,35 @@ describe('styling', () => { expect(stylingContext).toEqual([ element, + null, styleSanitizer, [null], - cleanStyle(0, 12), // + cleanStyle(0, 13), // 2, null, - // #6 - cleanStyleWithSanitization(0, 15), + // #7 + cleanStyleWithSanitization(0, 16), 'border-image', 'url(foo.jpg)', - // #9 - cleanStyle(0, 18), + // #10 + cleanStyle(0, 19), 'border-width', '100px', - // #12 + // #13 cleanStyleWithSanitization(0, 0), 'background-image', 'unsafe', - // #15 - cleanStyleWithSanitization(0, 6), + // #16 + cleanStyleWithSanitization(0, 7), 'border-image', null, - // #18 - cleanStyle(0, 9), + // #19 + cleanStyle(0, 10), 'border-width', null, ]); @@ -883,20 +899,20 @@ describe('styling', () => { const template = initContext(null, [InitialStylingFlags.VALUES_MODE, 'one', true, 'two', true]); expect(template).toEqual([ - element, null, [null, true, true], dirtyStyle(0, 12), // + element, null, null, [null, true, true], dirtyStyle(0, 13), // 0, null, - // #6 - cleanClass(1, 12), 'one', null, + // #7 + cleanClass(1, 13), 'one', null, - // #9 - cleanClass(2, 15), 'two', null, + // #10 + cleanClass(2, 16), 'two', null, - // #12 - dirtyClass(1, 6), 'one', null, + // #13 + dirtyClass(1, 7), 'one', null, - // #15 - dirtyClass(2, 9), 'two', null + // #16 + dirtyClass(2, 10), 'two', null ]); }); @@ -948,48 +964,49 @@ describe('styling', () => { expect(stylingContext).toEqual([ element, null, + null, [null, '100px', true], - dirtyStyle(0, 18), // + dirtyStyle(0, 19), // 2, null, - // #6 - cleanStyle(1, 18), + // #7 + cleanStyle(1, 19), 'width', null, - // #9 - cleanStyle(0, 21), + // #10 + cleanStyle(0, 22), 'height', null, - // #12 - cleanClass(2, 24), + // #13 + cleanClass(2, 25), 'wide', null, - // #15 - cleanClass(0, 27), + // #16 + cleanClass(0, 28), 'tall', null, - // #18 - dirtyStyle(1, 6), + // #19 + dirtyStyle(1, 7), 'width', null, - // #21 - cleanStyle(0, 9), + // #22 + cleanStyle(0, 10), 'height', null, - // #24 - dirtyClass(2, 12), + // #25 + dirtyClass(2, 13), 'wide', null, - // #27 - cleanClass(0, 15), + // #28 + cleanClass(0, 16), 'tall', null, ]); @@ -1000,58 +1017,59 @@ describe('styling', () => { expect(stylingContext).toEqual([ element, null, + null, [null, '100px', true], - dirtyStyle(0, 18), // + dirtyStyle(0, 19), // 2, 'tall round', - // #6 - cleanStyle(1, 18), + // #7 + cleanStyle(1, 19), 'width', null, - // #9 - cleanStyle(0, 33), + // #10 + cleanStyle(0, 34), 'height', null, - // #12 - cleanClass(2, 30), + // #13 + cleanClass(2, 31), 'wide', null, - // #15 - cleanClass(0, 24), + // #16 + cleanClass(0, 25), 'tall', null, - // #18 - dirtyStyle(1, 6), + // #19 + dirtyStyle(1, 7), 'width', '200px', - // #21 + // #22 dirtyStyle(0, 0), 'opacity', '0.5', - // #24 - dirtyClass(0, 15), + // #25 + dirtyClass(0, 16), 'tall', true, - // #27 + // #28 dirtyClass(0, 0), 'round', true, - // #30 - cleanClass(2, 12), + // #31 + cleanClass(2, 13), 'wide', null, - // #33 - cleanStyle(0, 9), + // #34 + cleanStyle(0, 10), 'height', null, ]); @@ -1066,58 +1084,59 @@ describe('styling', () => { expect(stylingContext).toEqual([ element, null, + null, [null, '100px', true], - dirtyStyle(0, 18), // + dirtyStyle(0, 19), // 2, null, - // #6 - dirtyStyle(1, 18), + // #7 + dirtyStyle(1, 19), 'width', '300px', - // #9 - cleanStyle(0, 33), + // #10 + cleanStyle(0, 34), 'height', null, - // #12 - cleanClass(2, 24), + // #13 + cleanClass(2, 25), 'wide', null, - // #15 - cleanClass(0, 21), + // #16 + cleanClass(0, 22), 'tall', null, - // #18 - cleanStyle(1, 6), + // #19 + cleanStyle(1, 7), 'width', '500px', - // #21 - cleanClass(0, 15), + // #22 + cleanClass(0, 16), 'tall', true, - // #24 - cleanClass(2, 12), + // #25 + cleanClass(2, 13), 'wide', true, - // #27 + // #28 dirtyClass(0, 0), 'round', null, - // #30 + // #31 dirtyStyle(0, 0), 'opacity', null, - // #33 - cleanStyle(0, 9), + // #34 + cleanStyle(0, 10), 'height', null, ]);