diff --git a/modules/@angular/platform-webworker/src/web_workers/shared/serializer.ts b/modules/@angular/platform-webworker/src/web_workers/shared/serializer.ts index 52662b9b72b4ea..f16dd856ae5329 100644 --- a/modules/@angular/platform-webworker/src/web_workers/shared/serializer.ts +++ b/modules/@angular/platform-webworker/src/web_workers/shared/serializer.ts @@ -13,8 +13,6 @@ import {isPresent} from '../../facade/lang'; import {RenderStore} from './render_store'; import {LocationType} from './serialized_types'; - - // PRIMITIVE is any type that does not need to be serialized (string, number, boolean) // We set it to String so that it is considered a Type. /** @@ -121,5 +119,6 @@ export class Serializer { } } +export const ANIMATION_WORKER_PLAYER_PREFIX = 'AnimationPlayer.'; export class RenderStoreObject {} diff --git a/modules/@angular/platform-webworker/src/web_workers/ui/event_dispatcher.ts b/modules/@angular/platform-webworker/src/web_workers/ui/event_dispatcher.ts index 2f2d9ee106a37e..4b46ea37131465 100644 --- a/modules/@angular/platform-webworker/src/web_workers/ui/event_dispatcher.ts +++ b/modules/@angular/platform-webworker/src/web_workers/ui/event_dispatcher.ts @@ -5,8 +5,6 @@ * 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 {EventEmitter} from '../../facade/async'; import {RenderStoreObject, Serializer} from '../shared/serializer'; @@ -15,6 +13,15 @@ import {serializeEventWithTarget, serializeGenericEvent, serializeKeyboardEvent, export class EventDispatcher { constructor(private _sink: EventEmitter, private _serializer: Serializer) {} + dispatchAnimationEvent(player: any, phaseName: string, element: any): boolean { + this._sink.emit({ + 'element': this._serializer.serialize(element, RenderStoreObject), + 'animationPlayer': this._serializer.serialize(player, RenderStoreObject), + 'eventName': phaseName + }); + return true; + } + dispatchRenderEvent(element: any, eventTarget: string, eventName: string, event: any): boolean { var serializedEvent: any /** TODO #9100 */; // TODO (jteplitz602): support custom events #3350 diff --git a/modules/@angular/platform-webworker/src/web_workers/ui/renderer.ts b/modules/@angular/platform-webworker/src/web_workers/ui/renderer.ts index ea87241411af78..6d33bc2582f873 100644 --- a/modules/@angular/platform-webworker/src/web_workers/ui/renderer.ts +++ b/modules/@angular/platform-webworker/src/web_workers/ui/renderer.ts @@ -6,12 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ -import {Injectable, RenderComponentType, Renderer, RootRenderer} from '@angular/core'; +import {AnimationPlayer, Injectable, RenderComponentType, Renderer, RootRenderer} from '@angular/core'; import {MessageBus} from '../shared/message_bus'; import {EVENT_CHANNEL, RENDERER_CHANNEL} from '../shared/messaging_api'; import {RenderStore} from '../shared/render_store'; -import {PRIMITIVE, RenderStoreObject, Serializer} from '../shared/serializer'; +import {ANIMATION_WORKER_PLAYER_PREFIX, PRIMITIVE, RenderStoreObject, Serializer} from '../shared/serializer'; import {ServiceMessageBrokerFactory} from '../shared/service_message_broker'; import {EventDispatcher} from '../ui/event_dispatcher'; @@ -86,6 +86,62 @@ export class MessageBasedRenderer { this._listenGlobal.bind(this)); broker.registerMethod( 'listenDone', [RenderStoreObject, RenderStoreObject], this._listenDone.bind(this)); + broker.registerMethod( + 'animate', + [ + RenderStoreObject, PRIMITIVE, PRIMITIVE, PRIMITIVE, PRIMITIVE, PRIMITIVE, PRIMITIVE, + PRIMITIVE + ], + this._animate.bind(this)); + + // + // The events below are available for when a player is created + // from the animate method above + // + broker.registerMethod( + ANIMATION_WORKER_PLAYER_PREFIX + 'play', [RenderStoreObject, RenderStoreObject], + (player: AnimationPlayer, element: any) => player.play()); + + broker.registerMethod( + ANIMATION_WORKER_PLAYER_PREFIX + 'pause', [RenderStoreObject, RenderStoreObject], + (player: AnimationPlayer, element: any) => player.pause()); + + broker.registerMethod( + ANIMATION_WORKER_PLAYER_PREFIX + 'init', [RenderStoreObject, RenderStoreObject], + (player: AnimationPlayer, element: any) => player.init()); + + broker.registerMethod( + ANIMATION_WORKER_PLAYER_PREFIX + 'restart', [RenderStoreObject, RenderStoreObject], + (player: AnimationPlayer, element: any) => player.restart()); + + broker.registerMethod( + ANIMATION_WORKER_PLAYER_PREFIX + 'destroy', [RenderStoreObject, RenderStoreObject], + (player: AnimationPlayer, element: any) => player.destroy()); + + broker.registerMethod( + ANIMATION_WORKER_PLAYER_PREFIX + 'finish', [RenderStoreObject, RenderStoreObject], + (player: AnimationPlayer, element: any) => player.finish()); + + broker.registerMethod( + ANIMATION_WORKER_PLAYER_PREFIX + 'getPosition', [RenderStoreObject, RenderStoreObject], + (player: AnimationPlayer, element: any) => player.getPosition()); + + broker.registerMethod( + ANIMATION_WORKER_PLAYER_PREFIX + 'onStart', + [RenderStoreObject, RenderStoreObject, PRIMITIVE], + (player: AnimationPlayer, element: any) => + this._listenOnAnimationPlayer(player, element, 'onStart')); + + broker.registerMethod( + ANIMATION_WORKER_PLAYER_PREFIX + 'onDone', + [RenderStoreObject, RenderStoreObject, PRIMITIVE], + (player: AnimationPlayer, element: any) => + this._listenOnAnimationPlayer(player, element, 'onDone')); + + broker.registerMethod( + ANIMATION_WORKER_PLAYER_PREFIX + 'setPosition', + [RenderStoreObject, RenderStoreObject, PRIMITIVE], + (player: AnimationPlayer, element: any, position: number) => player.setPosition(position)); } private _renderComponent(renderComponentType: RenderComponentType, rendererId: number) { @@ -187,4 +243,24 @@ export class MessageBasedRenderer { } private _listenDone(renderer: Renderer, unlistenCallback: Function) { unlistenCallback(); } + + private _animate( + renderer: Renderer, element: any, startingStyles: any, keyframes: any[], duration: number, + delay: number, easing: string, playerId: any) { + var player = renderer.animate(element, startingStyles, keyframes, duration, delay, easing); + this._renderStore.store(player, playerId); + } + + private _listenOnAnimationPlayer(player: AnimationPlayer, element: any, phaseName: string) { + const onEventComplete = + () => { this._eventDispatcher.dispatchAnimationEvent(player, phaseName, element); }; + + // there is no need to register a unlistener value here since the + // internal player callbacks are removed when the player is destroyed + if (phaseName == 'onDone') { + player.onDone(() => onEventComplete()); + } else { + player.onStart(() => onEventComplete()); + } + } } diff --git a/modules/@angular/platform-webworker/src/web_workers/worker/animation_worker_player.ts b/modules/@angular/platform-webworker/src/web_workers/worker/animation_worker_player.ts new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/modules/@angular/platform-webworker/src/web_workers/worker/event_deserializer.ts b/modules/@angular/platform-webworker/src/web_workers/worker/event_deserializer.ts index ca0b1b6f6aa3c6..93486c40186116 100644 --- a/modules/@angular/platform-webworker/src/web_workers/worker/event_deserializer.ts +++ b/modules/@angular/platform-webworker/src/web_workers/worker/event_deserializer.ts @@ -5,7 +5,6 @@ * 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 */ - // no deserialization is necessary in TS. // This is only here to match dart interface export function deserializeGenericEvent(serializedEvent: {[key: string]: any}): diff --git a/modules/@angular/platform-webworker/src/web_workers/worker/renderer.ts b/modules/@angular/platform-webworker/src/web_workers/worker/renderer.ts index d55ae476c08b7c..00467c74dc81dc 100644 --- a/modules/@angular/platform-webworker/src/web_workers/worker/renderer.ts +++ b/modules/@angular/platform-webworker/src/web_workers/worker/renderer.ts @@ -15,8 +15,7 @@ import {ClientMessageBrokerFactory, FnArg, UiArguments} from '../shared/client_m import {MessageBus} from '../shared/message_bus'; import {EVENT_CHANNEL, RENDERER_CHANNEL} from '../shared/messaging_api'; import {RenderStore} from '../shared/render_store'; -import {RenderStoreObject, Serializer} from '../shared/serializer'; - +import {ANIMATION_WORKER_PLAYER_PREFIX, RenderStoreObject, Serializer} from '../shared/serializer'; import {deserializeGenericEvent} from './event_deserializer'; @Injectable() @@ -28,7 +27,7 @@ export class WebWorkerRootRenderer implements RootRenderer { constructor( messageBrokerFactory: ClientMessageBrokerFactory, bus: MessageBus, - private _serializer: Serializer, private _renderStore: RenderStore) { + private _serializer: Serializer, public renderStore: RenderStore) { this._messageBroker = messageBrokerFactory.createMessageBroker(RENDERER_CHANNEL); bus.initChannel(EVENT_CHANNEL); var source = bus.from(EVENT_CHANNEL); @@ -37,14 +36,20 @@ export class WebWorkerRootRenderer implements RootRenderer { private _dispatchEvent(message: {[key: string]: any}): void { var eventName = message['eventName']; - var target = message['eventTarget']; - var event = deserializeGenericEvent(message['event']); - if (isPresent(target)) { - this.globalEvents.dispatchEvent(eventNameWithTarget(target, eventName), event); + var element = + this._serializer.deserialize(message['element'], RenderStoreObject); + var playerData = message['animationPlayer']; + if (playerData) { + var player = this._serializer.deserialize(playerData, RenderStoreObject); + element.animationPlayers.dispatchEvent(player, eventName); } else { - var element = - this._serializer.deserialize(message['element'], RenderStoreObject); - element.events.dispatchEvent(eventName, event); + var target = message['eventTarget']; + var event = deserializeGenericEvent(message['event']); + if (isPresent(target)) { + this.globalEvents.dispatchEvent(eventNameWithTarget(target, eventName), event); + } else { + element.events.dispatchEvent(eventName, event); + } } } @@ -53,8 +58,8 @@ export class WebWorkerRootRenderer implements RootRenderer { if (!result) { result = new WebWorkerRenderer(this, componentType); this._componentRenderers.set(componentType.id, result); - var id = this._renderStore.allocateId(); - this._renderStore.store(result, id); + var id = this.renderStore.allocateId(); + this.renderStore.store(result, id); this.runOnService('renderComponent', [ new FnArg(componentType, RenderComponentType), new FnArg(result, RenderStoreObject), @@ -70,16 +75,16 @@ export class WebWorkerRootRenderer implements RootRenderer { allocateNode(): WebWorkerRenderNode { var result = new WebWorkerRenderNode(); - var id = this._renderStore.allocateId(); - this._renderStore.store(result, id); + var id = this.renderStore.allocateId(); + this.renderStore.store(result, id); return result; } - allocateId(): number { return this._renderStore.allocateId(); } + allocateId(): number { return this.renderStore.allocateId(); } destroyNodes(nodes: any[]) { for (var i = 0; i < nodes.length; i++) { - this._renderStore.remove(nodes[i]); + this.renderStore.remove(nodes[i]); } } } @@ -232,10 +237,21 @@ export class WebWorkerRenderer implements Renderer, RenderStoreObject { } animate( - element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], + renderElement: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], duration: number, delay: number, easing: string): AnimationPlayer { - // TODO - return null; + const playerId = this._rootRenderer.allocateId(); + const renderNode = this._rootRenderer.allocateNode(); + + this._runOnService('animate', [ + new FnArg(renderNode, RenderStoreObject), new FnArg(startingStyles, null), + new FnArg(keyframes, null), new FnArg(duration, null), new FnArg(delay, null), + new FnArg(easing, null), new FnArg(playerId, null) + ]); + + const player = new _AnimationWorkerRendererPlayer(this._rootRenderer, renderElement); + this._rootRenderer.renderStore.store(player, playerId); + + return player; } } @@ -268,8 +284,94 @@ export class NamedEventEmitter { } } +export class AnimationPlayerEmitter { + private _listeners: Map; + + private _getListeners(player: AnimationPlayer, phaseName: string): Function[] { + if (!this._listeners) { + this._listeners = new Map(); + } + var phaseMap = this._listeners.get(player); + if (!phaseMap) { + this._listeners.set(player, phaseMap = {}); + } + var phaseFns = phaseMap[phaseName]; + if (!phaseFns) { + phaseFns = phaseMap[phaseName] = []; + } + return phaseFns; + } + + listen(player: AnimationPlayer, phaseName: string, callback: Function) { + this._getListeners(player, phaseName).push(callback); + } + + unlisten(player: AnimationPlayer) { this._listeners.delete(player); } + + dispatchEvent(player: AnimationPlayer, phaseName: string) { + var listeners = this._getListeners(player, phaseName); + for (var i = 0; i < listeners.length; i++) { + listeners[i](); + } + } +} + function eventNameWithTarget(target: string, eventName: string): string { return `${target}:${eventName}`; } -export class WebWorkerRenderNode { events: NamedEventEmitter = new NamedEventEmitter(); } +export class WebWorkerRenderNode { + events = new NamedEventEmitter(); + animationPlayers = new AnimationPlayerEmitter(); +} + +class _AnimationWorkerRendererPlayer implements AnimationPlayer, RenderStoreObject { + public parentPlayer: AnimationPlayer = null; + + private _started = false; + + constructor(private _rootRenderer: WebWorkerRootRenderer, private _renderElement: any) {} + + private _runOnService(fnName: string, fnArgs: FnArg[]) { + var fnArgsWithRenderer = [ + new FnArg(this, RenderStoreObject), new FnArg(this._renderElement, RenderStoreObject) + ].concat(fnArgs); + this._rootRenderer.runOnService(ANIMATION_WORKER_PLAYER_PREFIX + fnName, fnArgsWithRenderer); + } + + onStart(fn: () => void): void { + this._renderElement.animationPlayers.listen(this, 'onStart', fn); + this._runOnService('onStart', []); + } + + onDone(fn: () => void): void { + this._renderElement.animationPlayers.listen(this, 'onDone', fn); + this._runOnService('onDone', []); + } + + hasStarted(): boolean { return this._started; } + + init(): void { this._runOnService('init', []); } + + play(): void { + this._started = true; + this._runOnService('play', []); + } + + pause(): void { this._runOnService('pause', []); } + + restart(): void { this._runOnService('restart', []); } + + finish(): void { this._runOnService('finish', []); } + + destroy(): void { + this._renderElement.animationPlayers.unlisten(this); + this._runOnService('destroy', []); + } + + reset(): void { this._runOnService('reset', []); } + + setPosition(p: number): void { this._runOnService('setPosition', [new FnArg(p, null)]); } + + getPosition(): number { return 0; } +} diff --git a/modules/@angular/platform-webworker/test/web_workers/worker/renderer_animation_integration_spec.ts b/modules/@angular/platform-webworker/test/web_workers/worker/renderer_animation_integration_spec.ts new file mode 100644 index 00000000000000..735276bfcd8659 --- /dev/null +++ b/modules/@angular/platform-webworker/test/web_workers/worker/renderer_animation_integration_spec.ts @@ -0,0 +1,333 @@ +/** + * @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 {AUTO_STYLE, AnimationTransitionEvent, Component, Injector, animate, state, style, transition, trigger} from '@angular/core'; +import {DebugDomRootRenderer} from '@angular/core/src/debug/debug_renderer'; +import {RootRenderer} from '@angular/core/src/render/api'; +import {TestBed} from '@angular/core/testing'; +import {MockAnimationPlayer} from '@angular/core/testing/testing_internal'; +import {AnimationDriver} from '@angular/platform-browser/src/dom/animation_driver'; +import {DomRootRenderer, DomRootRenderer_} from '@angular/platform-browser/src/dom/dom_renderer'; +import {BrowserTestingModule} from '@angular/platform-browser/testing'; +import {expect} from '@angular/platform-browser/testing/matchers'; +import {MockAnimationDriver} from '@angular/platform-browser/testing/mock_animation_driver'; +import {ClientMessageBrokerFactory, ClientMessageBrokerFactory_} from '@angular/platform-webworker/src/web_workers/shared/client_message_broker'; +import {RenderStore} from '@angular/platform-webworker/src/web_workers/shared/render_store'; +import {Serializer} from '@angular/platform-webworker/src/web_workers/shared/serializer'; +import {ServiceMessageBrokerFactory_} from '@angular/platform-webworker/src/web_workers/shared/service_message_broker'; +import {MessageBasedRenderer} from '@angular/platform-webworker/src/web_workers/ui/renderer'; +import {WebWorkerRootRenderer} from '@angular/platform-webworker/src/web_workers/worker/renderer'; + +import {platformBrowserDynamicTesting} from '../../../../platform-browser-dynamic/testing'; +import {PairedMessageBuses, createPairedMessageBuses} from '../shared/web_worker_test_util'; + +export function main() { + function createWebWorkerBrokerFactory( + messageBuses: PairedMessageBuses, workerSerializer: Serializer, uiSerializer: Serializer, + domRootRenderer: DomRootRenderer, uiRenderStore: RenderStore): ClientMessageBrokerFactory { + var uiMessageBus = messageBuses.ui; + var workerMessageBus = messageBuses.worker; + + // set up the worker side + var webWorkerBrokerFactory = + new ClientMessageBrokerFactory_(workerMessageBus, workerSerializer); + + // set up the ui side + var uiMessageBrokerFactory = new ServiceMessageBrokerFactory_(uiMessageBus, uiSerializer); + var renderer = new MessageBasedRenderer( + uiMessageBrokerFactory, uiMessageBus, uiSerializer, uiRenderStore, domRootRenderer); + renderer.start(); + + return webWorkerBrokerFactory; + } + + function createWorkerRenderer( + workerSerializer: Serializer, uiSerializer: Serializer, domRootRenderer: DomRootRenderer, + uiRenderStore: RenderStore, workerRenderStore: RenderStore): RootRenderer { + var messageBuses = createPairedMessageBuses(); + var brokerFactory = createWebWorkerBrokerFactory( + messageBuses, workerSerializer, uiSerializer, domRootRenderer, uiRenderStore); + var workerRootRenderer = new WebWorkerRootRenderer( + brokerFactory, messageBuses.worker, workerSerializer, workerRenderStore); + return new DebugDomRootRenderer(workerRootRenderer); + } + + describe('Web Worker Renderer Animations', () => { + var uiTestBed: TestBed; + var uiRenderStore: RenderStore; + var workerRenderStore: RenderStore; + + beforeEach(() => { + uiRenderStore = new RenderStore(); + uiTestBed = new TestBed(); + uiTestBed.platform = platformBrowserDynamicTesting(); + uiTestBed.ngModule = BrowserTestingModule; + uiTestBed.configureTestingModule({ + providers: [ + {provide: AnimationDriver, useClass: MockAnimationDriver}, Serializer, + {provide: RenderStore, useValue: uiRenderStore}, + {provide: DomRootRenderer, useClass: DomRootRenderer_}, + {provide: RootRenderer, useExisting: DomRootRenderer} + ] + }); + var uiSerializer = uiTestBed.get(Serializer); + var domRootRenderer = uiTestBed.get(DomRootRenderer); + workerRenderStore = new RenderStore(); + + TestBed.configureTestingModule({ + declarations: [AnimationCmp, MultiAnimationCmp], + providers: [ + Serializer, {provide: RenderStore, useValue: workerRenderStore}, { + provide: RootRenderer, + useFactory: (workerSerializer: Serializer) => { + return createWorkerRenderer( + workerSerializer, uiSerializer, domRootRenderer, uiRenderStore, + workerRenderStore); + }, + deps: [Serializer] + } + ] + }); + }); + + var uiDriver: MockAnimationDriver; + beforeEach(() => { uiDriver = uiTestBed.get(AnimationDriver) as MockAnimationDriver; }); + + function retrieveFinalAnimationStepStyles(keyframes: any[]) { return keyframes[1][1]; } + + it('should trigger an animation and animate styles', () => { + const fixture = TestBed.createComponent(AnimationCmp); + const cmp = fixture.componentInstance; + + cmp.state = 'on'; + fixture.detectChanges(); + + var step1 = uiDriver.log.shift(); + var step2 = uiDriver.log.shift(); + + var step1Styles = retrieveFinalAnimationStepStyles(step1['keyframeLookup']); + var step2Styles = retrieveFinalAnimationStepStyles(step2['keyframeLookup']); + + expect(step1Styles).toEqual({fontSize: '20px'}); + expect(step2Styles).toEqual({opacity: '1', fontSize: '50px'}); + + cmp.state = 'off'; + fixture.detectChanges(); + + var step3 = uiDriver.log.shift(); + var step3Styles = retrieveFinalAnimationStepStyles(step3['keyframeLookup']); + + expect(step3Styles).toEqual({opacity: '0', fontSize: AUTO_STYLE}); + }); + + it('should fire the onStart callback when the animation starts', () => { + const fixture = TestBed.createComponent(AnimationCmp); + const cmp = fixture.componentInstance; + + var capturedEvent: AnimationTransitionEvent = null; + cmp.stateStartFn = event => { capturedEvent = event; }; + + cmp.state = 'on'; + + expect(capturedEvent).toBe(null); + + fixture.detectChanges(); + + expect(capturedEvent instanceof AnimationTransitionEvent).toBe(true); + + expect(capturedEvent.toState).toBe('on'); + }); + + it('should fire the onDone callback when the animation ends', () => { + const fixture = TestBed.createComponent(AnimationCmp); + const cmp = fixture.componentInstance; + + var capturedEvent: AnimationTransitionEvent = null; + cmp.stateDoneFn = event => { capturedEvent = event; }; + + cmp.state = 'off'; + + expect(capturedEvent).toBe(null); + + fixture.detectChanges(); + + expect(capturedEvent).toBe(null); + + const step = uiDriver.log.shift(); + step['player'].finish(); + + expect(capturedEvent instanceof AnimationTransitionEvent).toBe(true); + + expect(capturedEvent.toState).toBe('off'); + }); + + it('should handle multiple animations on the same element that contain refs to .start and .done callbacks', + () => { + const fixture = TestBed.createComponent(MultiAnimationCmp); + const cmp = fixture.componentInstance; + + let log: {[triggerName: string]: AnimationTransitionEvent[]} = {}; + cmp.callback = (triggerName: string, event: AnimationTransitionEvent) => { + log[triggerName] = log[triggerName] || []; + log[triggerName].push(event); + }; + + cmp.oneTriggerState = 'a'; + cmp.twoTriggerState = 'c'; + fixture.detectChanges(); + + // clear any animation logs that were collected when + // the component was rendered (void => *) + log = {}; + + cmp.oneTriggerState = 'b'; + cmp.twoTriggerState = 'd'; + fixture.detectChanges(); + + uiDriver.log.shift()['player'].finish(); + const [triggerOneStart, triggerOneDone] = log['one']; + expect(triggerOneStart) + .toEqual(new AnimationTransitionEvent( + {fromState: 'a', toState: 'b', totalTime: 500, phaseName: 'start'})); + + expect(triggerOneDone) + .toEqual(new AnimationTransitionEvent( + {fromState: 'a', toState: 'b', totalTime: 500, phaseName: 'done'})); + + uiDriver.log.shift()['player'].finish(); + const [triggerTwoStart, triggerTwoDone] = log['two']; + expect(triggerTwoStart) + .toEqual(new AnimationTransitionEvent( + {fromState: 'c', toState: 'd', totalTime: 1000, phaseName: 'start'})); + + expect(triggerTwoDone) + .toEqual(new AnimationTransitionEvent( + {fromState: 'c', toState: 'd', totalTime: 1000, phaseName: 'done'})); + }); + + it('should handle .start and .done callbacks for mutliple elements that contain animations that are fired at the same time', + () => { + function logFactory( + log: {[phaseName: string]: AnimationTransitionEvent}, + phaseName: string): (event: AnimationTransitionEvent) => any { + return (event: AnimationTransitionEvent) => { log[phaseName] = event; }; + } + + const f1 = TestBed.createComponent(AnimationCmp); + const f2 = TestBed.createComponent(AnimationCmp); + const cmp1 = f1.componentInstance; + const cmp2 = f2.componentInstance; + + var cmp1Log: {[phaseName: string]: AnimationTransitionEvent} = {}; + var cmp2Log: {[phaseName: string]: AnimationTransitionEvent} = {}; + + cmp1.stateStartFn = logFactory(cmp1Log, 'start'); + cmp1.stateDoneFn = logFactory(cmp1Log, 'done'); + cmp2.stateStartFn = logFactory(cmp2Log, 'start'); + cmp2.stateDoneFn = logFactory(cmp2Log, 'done'); + + cmp1.state = 'off'; + cmp2.state = 'on'; + f1.detectChanges(); + f2.detectChanges(); + + uiDriver.log.shift()['player'].finish(); + + expect(cmp1Log['start']) + .toEqual(new AnimationTransitionEvent( + {fromState: 'void', toState: 'off', totalTime: 500, phaseName: 'start'})); + + expect(cmp1Log['done']) + .toEqual(new AnimationTransitionEvent( + {fromState: 'void', toState: 'off', totalTime: 500, phaseName: 'done'})); + + // the * => on transition has two steps + uiDriver.log.shift()['player'].finish(); + uiDriver.log.shift()['player'].finish(); + + expect(cmp2Log['start']) + .toEqual(new AnimationTransitionEvent( + {fromState: 'void', toState: 'on', totalTime: 1000, phaseName: 'start'})); + + expect(cmp2Log['done']) + .toEqual(new AnimationTransitionEvent( + {fromState: 'void', toState: 'on', totalTime: 1000, phaseName: 'done'})); + }); + + it('should destroy the player when the animation is complete', () => { + const fixture = TestBed.createComponent(AnimationCmp); + const cmp = fixture.componentInstance; + + cmp.state = 'off'; + fixture.detectChanges(); + + var player = uiDriver.log.shift()['player']; + expect(player.log.indexOf('destroy') >= 0).toBe(false); + + cmp.state = 'on'; + fixture.detectChanges(); + + expect(player.log.indexOf('destroy') >= 0).toBe(true); + }); + }); +} + + +@Component({ + selector: 'my-comp', + template: ` +
...
+ `, + animations: [trigger( + 'myTrigger', + [ + state('void, off', style({opacity: '0'})), + state('on', style({opacity: '1', fontSize: '50px'})), + transition('* => on', [animate(500, style({fontSize: '20px'})), animate(500)]), + transition('* => off', [animate(500)]) + ])] +}) +class AnimationCmp { + state = 'off'; + stateStartFn = (event: AnimationTransitionEvent): any => {}; + stateDoneFn = (event: AnimationTransitionEvent): any => {}; +} + +@Component({ + selector: 'my-multi-comp', + template: ` +
...
+
...
+ `, + animations: [ + trigger( + 'one', + [ + state('a', style({width: '0px'})), state('b', style({width: '100px'})), + transition('a => b', animate(500)) + ]), + trigger( + 'two', + [ + state('c', style({height: '0px'})), state('d', style({height: '100px'})), + transition('c => d', animate(1000)) + ]) + ] +}) +class MultiAnimationCmp { + oneTriggerState: string; + twoTriggerState: string; + callback = (triggerName: string, event: AnimationTransitionEvent): any => {}; +}