diff --git a/modules/@angular/compiler/src/animation/animation_compiler.ts b/modules/@angular/compiler/src/animation/animation_compiler.ts index 4d14a0d071..d607f19a13 100644 --- a/modules/@angular/compiler/src/animation/animation_compiler.ts +++ b/modules/@angular/compiler/src/animation/animation_compiler.ts @@ -32,6 +32,7 @@ export class AnimationCompiler { var _ANIMATION_FACTORY_ELEMENT_VAR = o.variable('element'); var _ANIMATION_DEFAULT_STATE_VAR = o.variable('defaultStateStyles'); var _ANIMATION_FACTORY_VIEW_VAR = o.variable('view'); +var _ANIMATION_FACTORY_VIEW_CONTEXT = _ANIMATION_FACTORY_VIEW_VAR.prop('animationContext'); var _ANIMATION_FACTORY_RENDERER_VAR = _ANIMATION_FACTORY_VIEW_VAR.prop('renderer'); var _ANIMATION_CURRENT_STATE_VAR = o.variable('currentState'); var _ANIMATION_NEXT_STATE_VAR = o.variable('nextState'); @@ -186,7 +187,7 @@ class _AnimationBuilder implements AnimationAstVisitor { context.stateMap.registerState(DEFAULT_STATE, {}); var statements: o.Statement[] = []; - statements.push(_ANIMATION_FACTORY_VIEW_VAR + statements.push(_ANIMATION_FACTORY_VIEW_CONTEXT .callMethod( 'cancelActiveAnimation', [ @@ -263,13 +264,20 @@ class _AnimationBuilder implements AnimationAstVisitor { .toStmt()])]) .toStmt()); - statements.push(_ANIMATION_FACTORY_VIEW_VAR + var transitionParams = o.literalMap([ + ['toState', _ANIMATION_NEXT_STATE_VAR], ['fromState', _ANIMATION_CURRENT_STATE_VAR], + ['totalTime', _ANIMATION_TIME_VAR] + ]); + + var transitionEvent = o.importExpr(resolveIdentifier(Identifiers.AnimationTransitionEvent)) + .instantiate([transitionParams]); + + statements.push(_ANIMATION_FACTORY_VIEW_CONTEXT .callMethod( 'queueAnimation', [ _ANIMATION_FACTORY_ELEMENT_VAR, o.literal(this.animationName), - _ANIMATION_PLAYER_VAR, _ANIMATION_TIME_VAR, - _ANIMATION_CURRENT_STATE_VAR, _ANIMATION_NEXT_STATE_VAR + _ANIMATION_PLAYER_VAR, transitionEvent ]) .toStmt()); diff --git a/modules/@angular/compiler/src/identifiers.ts b/modules/@angular/compiler/src/identifiers.ts index f1008b45c7..4965bb9cc2 100644 --- a/modules/@angular/compiler/src/identifiers.ts +++ b/modules/@angular/compiler/src/identifiers.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {ANALYZE_FOR_ENTRY_COMPONENTS, ChangeDetectionStrategy, ChangeDetectorRef, ComponentFactory, ComponentFactoryResolver, ElementRef, Injector, LOCALE_ID as LOCALE_ID_, NgModuleFactory, QueryList, RenderComponentType, Renderer, SecurityContext, SimpleChange, TRANSLATIONS_FORMAT as TRANSLATIONS_FORMAT_, TemplateRef, ViewContainerRef, ViewEncapsulation} from '@angular/core'; +import {ANALYZE_FOR_ENTRY_COMPONENTS, AnimationTransitionEvent, ChangeDetectionStrategy, ChangeDetectorRef, ComponentFactory, ComponentFactoryResolver, ElementRef, Injector, LOCALE_ID as LOCALE_ID_, NgModuleFactory, QueryList, RenderComponentType, Renderer, SecurityContext, SimpleChange, TRANSLATIONS_FORMAT as TRANSLATIONS_FORMAT_, TemplateRef, ViewContainerRef, ViewEncapsulation} from '@angular/core'; import {CompileIdentifierMetadata, CompileTokenMetadata} from './compile_metadata'; import {AnimationGroupPlayer, AnimationKeyframe, AnimationSequencePlayer, AnimationStyles, AppElement, AppView, ChangeDetectorStatus, CodegenComponentFactoryResolver, DebugAppView, DebugContext, EMPTY_ARRAY, EMPTY_MAP, NgModuleInjector, NoOpAnimationPlayer, StaticNodeDebugInfo, TemplateRef_, UNINITIALIZED, ValueUnwrapper, ViewType, ViewUtils, balanceAnimationKeyframes, castByValue, checkBinding, clearStyles, collectAndResolveStyles, devModeEqual, flattenNestedViewRenderNodes, interpolate, prepareFinalAnimationStyles, pureProxy1, pureProxy10, pureProxy2, pureProxy3, pureProxy4, pureProxy5, pureProxy6, pureProxy7, pureProxy8, pureProxy9, reflector, registerModuleFactory, renderStyles} from './private_import_core'; @@ -266,6 +266,11 @@ export class Identifiers { moduleUrl: assetUrl('core', 'i18n/tokens'), runtime: TRANSLATIONS_FORMAT_ }; + static AnimationTransitionEvent: IdentifierSpec = { + name: 'AnimationTransitionEvent', + moduleUrl: assetUrl('core', 'animation/animation_transition_event'), + runtime: AnimationTransitionEvent + }; } export function resolveIdentifier(identifier: IdentifierSpec) { diff --git a/modules/@angular/compiler/src/view_compiler/event_binder.ts b/modules/@angular/compiler/src/view_compiler/event_binder.ts index 2a46bde21f..f045cb449e 100644 --- a/modules/@angular/compiler/src/view_compiler/event_binder.ts +++ b/modules/@angular/compiler/src/view_compiler/event_binder.ts @@ -119,9 +119,9 @@ export class CompileEventListener { [o.THIS_EXPR.prop(this._methodName).callMethod(o.BuiltinMethod.Bind, [o.THIS_EXPR])]); // tie the property callback method to the view animations map - var stmt = o.THIS_EXPR + var stmt = o.THIS_EXPR.prop('animationContext') .callMethod( - 'registerAnimationOutput', + 'registerOutputHandler', [ this.compileElement.renderNode, o.literal(this.eventName), o.literal(this.eventPhase), outputListener diff --git a/modules/@angular/core/src/animation/view_animation_map.ts b/modules/@angular/core/src/animation/view_animation_map.ts index 5048b27165..7407498b35 100644 --- a/modules/@angular/core/src/animation/view_animation_map.ts +++ b/modules/@angular/core/src/animation/view_animation_map.ts @@ -15,8 +15,6 @@ export class ViewAnimationMap { private _map = new Map(); private _allPlayers: AnimationPlayer[] = []; - get length(): number { return this.getAllPlayers().length; } - find(element: any, animationName: string): AnimationPlayer { var playersByAnimation = this._map.get(element); if (isPresent(playersByAnimation)) { diff --git a/modules/@angular/core/src/linker/animation_view_context.ts b/modules/@angular/core/src/linker/animation_view_context.ts new file mode 100644 index 0000000000..56db8d5cfd --- /dev/null +++ b/modules/@angular/core/src/linker/animation_view_context.ts @@ -0,0 +1,84 @@ +/** + * @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 {AnimationGroupPlayer} from '../animation/animation_group_player'; +import {AnimationPlayer} from '../animation/animation_player'; +import {queueAnimation as queueAnimationGlobally} from '../animation/animation_queue'; +import {AnimationTransitionEvent} from '../animation/animation_transition_event'; +import {ViewAnimationMap} from '../animation/view_animation_map'; + +export class AnimationViewContext { + private _players = new ViewAnimationMap(); + private _listeners = new Map(); + + onAllActiveAnimationsDone(callback: () => any): void { + var activeAnimationPlayers = this._players.getAllPlayers(); + // we check for the length to avoid having GroupAnimationPlayer + // issue an unnecessary microtask when zero players are passed in + if (activeAnimationPlayers.length) { + new AnimationGroupPlayer(activeAnimationPlayers).onDone(() => callback()); + } else { + callback(); + } + } + + queueAnimation( + element: any, animationName: string, player: AnimationPlayer, + event: AnimationTransitionEvent): void { + queueAnimationGlobally(player); + + this._players.set(element, animationName, player); + player.onDone(() => { + // TODO: add codegen to remove the need to store these values + this._triggerOutputHandler(element, animationName, 'done', event); + this._players.remove(element, animationName); + }); + + player.onStart(() => this._triggerOutputHandler(element, animationName, 'start', event)); + } + + cancelActiveAnimation(element: any, animationName: string, removeAllAnimations: boolean = false): + void { + if (removeAllAnimations) { + this._players.findAllPlayersByElement(element).forEach(player => player.destroy()); + } else { + var player = this._players.find(element, animationName); + if (player) { + player.destroy(); + } + } + } + + registerOutputHandler( + element: any, eventName: string, eventPhase: string, eventHandler: Function): void { + var animations = this._listeners.get(element); + if (!animations) { + this._listeners.set(element, animations = []); + } + animations.push(new _AnimationOutputHandler(eventName, eventPhase, eventHandler)); + } + + private _triggerOutputHandler( + element: any, animationName: string, phase: string, event: AnimationTransitionEvent): void { + const listeners = this._listeners.get(element); + if (listeners && listeners.length) { + for (let i = 0; i < listeners.length; i++) { + let listener = listeners[i]; + // we check for both the name in addition to the phase in the event + // that there may be more than one @trigger on the same element + if (listener.eventName === animationName && listener.eventPhase === phase) { + listener.handler(event); + break; + } + } + } + } +} + +class _AnimationOutputHandler { + constructor(public eventName: string, public eventPhase: string, public handler: Function) {} +} diff --git a/modules/@angular/core/src/linker/view.ts b/modules/@angular/core/src/linker/view.ts index b0e91bfe20..b46b5cea1a 100644 --- a/modules/@angular/core/src/linker/view.ts +++ b/modules/@angular/core/src/linker/view.ts @@ -6,11 +6,6 @@ * found in the LICENSE file at https://angular.io/license */ -import {AnimationGroupPlayer} from '../animation/animation_group_player'; -import {AnimationPlayer} from '../animation/animation_player'; -import {queueAnimation} from '../animation/animation_queue'; -import {AnimationTransitionEvent} from '../animation/animation_transition_event'; -import {ViewAnimationMap} from '../animation/view_animation_map'; import {ChangeDetectorRef, ChangeDetectorStatus} from '../change_detection/change_detection'; import {Injector} from '../di/injector'; import {ListWrapper} from '../facade/collection'; @@ -18,6 +13,7 @@ import {isPresent} from '../facade/lang'; import {WtfScopeFn, wtfCreateScope, wtfLeave} from '../profile/profile'; import {RenderComponentType, RenderDebugInfo, Renderer} from '../render/api'; +import {AnimationViewContext} from './animation_view_context'; import {DebugContext, StaticNodeDebugInfo} from './debug_context'; import {AppElement} from './element'; import {ElementInjector} from './element_injector'; @@ -49,10 +45,7 @@ export abstract class AppView { renderer: Renderer; private _hasExternalHostElement: boolean; - - public animationPlayers = new ViewAnimationMap(); - - private _animationListeners = new Map(); + private _animationContext: AnimationViewContext; public context: T; @@ -68,61 +61,15 @@ export abstract class AppView { } } + get animationContext(): AnimationViewContext { + if (!this._animationContext) { + this._animationContext = new AnimationViewContext(); + } + return this._animationContext; + } + get destroyed(): boolean { return this.cdMode === ChangeDetectorStatus.Destroyed; } - cancelActiveAnimation(element: any, animationName: string, removeAllAnimations: boolean = false) { - if (removeAllAnimations) { - this.animationPlayers.findAllPlayersByElement(element).forEach(player => player.destroy()); - } else { - var player = this.animationPlayers.find(element, animationName); - if (isPresent(player)) { - player.destroy(); - } - } - } - - queueAnimation( - element: any, animationName: string, player: AnimationPlayer, totalTime: number, - fromState: string, toState: string): void { - queueAnimation(player); - var event = new AnimationTransitionEvent( - {'fromState': fromState, 'toState': toState, 'totalTime': totalTime}); - this.animationPlayers.set(element, animationName, player); - - player.onDone(() => { - // TODO: make this into a datastructure for done|start - this.triggerAnimationOutput(element, animationName, 'done', event); - this.animationPlayers.remove(element, animationName); - }); - - player.onStart(() => { this.triggerAnimationOutput(element, animationName, 'start', event); }); - } - - triggerAnimationOutput( - element: any, animationName: string, phase: string, event: AnimationTransitionEvent) { - var listeners = this._animationListeners.get(element); - if (isPresent(listeners) && listeners.length) { - for (let i = 0; i < listeners.length; i++) { - let listener = listeners[i]; - // we check for both the name in addition to the phase in the event - // that there may be more than one @trigger on the same element - if (listener.eventName === animationName && listener.eventPhase === phase) { - listener.handler(event); - break; - } - } - } - } - - registerAnimationOutput( - element: any, eventName: string, eventPhase: string, eventHandler: Function): void { - var animations = this._animationListeners.get(element); - if (!isPresent(animations)) { - this._animationListeners.set(element, animations = []); - } - animations.push(new _AnimationOutputHandler(eventName, eventPhase, eventHandler)); - } - create(context: T, givenProjectableNodes: Array, rootSelectorOrNode: string|any): AppElement { this.context = context; @@ -234,11 +181,11 @@ export abstract class AppView { this.destroyInternal(); this.dirtyParentQueriesInternal(); - if (this.animationPlayers.length == 0) { - this.renderer.destroyView(hostElement, this.allNodes); + if (this._animationContext) { + this._animationContext.onAllActiveAnimationsDone( + () => this.renderer.destroyView(hostElement, this.allNodes)); } else { - var player = new AnimationGroupPlayer(this.animationPlayers.getAllPlayers()); - player.onDone(() => { this.renderer.destroyView(hostElement, this.allNodes); }); + this.renderer.destroyView(hostElement, this.allNodes); } } @@ -254,11 +201,11 @@ export abstract class AppView { detach(): void { this.detachInternal(); - if (this.animationPlayers.length == 0) { - this.renderer.detachView(this.flatRootNodes); + if (this._animationContext) { + this._animationContext.onAllActiveAnimationsDone( + () => this.renderer.detachView(this.flatRootNodes)); } else { - var player = new AnimationGroupPlayer(this.animationPlayers.getAllPlayers()); - player.onDone(() => { this.renderer.detachView(this.flatRootNodes); }); + this.renderer.detachView(this.flatRootNodes); } } @@ -466,7 +413,3 @@ function _findLastRenderNode(node: any): any { } return lastNode; } - -class _AnimationOutputHandler { - constructor(public eventName: string, public eventPhase: string, public handler: Function) {} -} diff --git a/modules/@angular/core/test/animation/active_animations_players_map_spec.ts b/modules/@angular/core/test/animation/active_animations_players_map_spec.ts index c00b88911d..502b35748a 100644 --- a/modules/@angular/core/test/animation/active_animations_players_map_spec.ts +++ b/modules/@angular/core/test/animation/active_animations_players_map_spec.ts @@ -36,17 +36,17 @@ export function main() { expect(playersMap.find(elementNode, animationName)).toBe(player); expect(playersMap.findAllPlayersByElement(elementNode)).toEqual([player]); expect(playersMap.getAllPlayers()).toEqual([player]); - expect(playersMap.length).toEqual(1); + expect(countPlayers(playersMap)).toEqual(1); }); it('should remove a registered player when remove() is called', () => { var player = new MockAnimationPlayer(); playersMap.set(elementNode, animationName, player); expect(playersMap.find(elementNode, animationName)).toBe(player); - expect(playersMap.length).toEqual(1); + expect(countPlayers(playersMap)).toEqual(1); playersMap.remove(elementNode, animationName); expect(playersMap.find(elementNode, animationName)).not.toBe(player); - expect(playersMap.length).toEqual(0); + expect(countPlayers(playersMap)).toEqual(0); }); it('should allow multiple players to be registered on the same element', () => { @@ -54,7 +54,7 @@ export function main() { var player2 = new MockAnimationPlayer(); playersMap.set(elementNode, 'myAnimation1', player1); playersMap.set(elementNode, 'myAnimation2', player2); - expect(playersMap.length).toEqual(2); + expect(countPlayers(playersMap)).toEqual(2); expect(playersMap.findAllPlayersByElement(elementNode)).toEqual([player1, player2]); }); @@ -63,10 +63,14 @@ export function main() { var player2 = new MockAnimationPlayer(); playersMap.set(elementNode, animationName, player1); expect(playersMap.find(elementNode, animationName)).toBe(player1); - expect(playersMap.length).toEqual(1); + expect(countPlayers(playersMap)).toEqual(1); playersMap.set(elementNode, animationName, player2); expect(playersMap.find(elementNode, animationName)).toBe(player2); - expect(playersMap.length).toEqual(1); + expect(countPlayers(playersMap)).toEqual(1); }); }); } + +function countPlayers(map: ViewAnimationMap): number { + return map.getAllPlayers().length; +}