diff --git a/packages/animations/browser/src/render/animation_engine_next.ts b/packages/animations/browser/src/render/animation_engine_next.ts index 84b5ab03f2..410d3a77df 100644 --- a/packages/animations/browser/src/render/animation_engine_next.ts +++ b/packages/animations/browser/src/render/animation_engine_next.ts @@ -86,7 +86,7 @@ export class AnimationEngine { return this._transitionEngine.listen(namespaceId, element, eventName, eventPhase, callback); } - flush(): void { this._transitionEngine.flush(); } + flush(countId: number = -1): void { this._transitionEngine.flush(countId); } get players(): AnimationPlayer[] { return (this._transitionEngine.players as AnimationPlayer[]) diff --git a/packages/animations/browser/src/render/shared.ts b/packages/animations/browser/src/render/shared.ts index d294d0f91f..e8dc7ec8a4 100644 --- a/packages/animations/browser/src/render/shared.ts +++ b/packages/animations/browser/src/render/shared.ts @@ -80,9 +80,14 @@ export function listenOnPlayer( export function copyAnimationEvent( e: AnimationEvent, phaseName?: string, totalTime?: number): AnimationEvent { - return makeAnimationEvent( + const event = makeAnimationEvent( e.element, e.triggerName, e.fromState, e.toState, phaseName || e.phaseName, totalTime == undefined ? e.totalTime : totalTime); + const data = (e as any)['_data']; + if (data != null) { + (event as any)['_data'] = data; + } + return event; } export function makeAnimationEvent( diff --git a/packages/animations/browser/src/render/transition_animation_engine.ts b/packages/animations/browser/src/render/transition_animation_engine.ts index f3d53e9716..4162d30c5c 100644 --- a/packages/animations/browser/src/render/transition_animation_engine.ts +++ b/packages/animations/browser/src/render/transition_animation_engine.ts @@ -382,7 +382,7 @@ export class AnimationTransitionNamespace { insertNode(element: any, parent: any): void { addClass(element, this._hostClassName); } - drainQueuedTransitions(): QueueInstruction[] { + drainQueuedTransitions(countId: number): QueueInstruction[] { const instructions: QueueInstruction[] = []; this._queue.forEach(entry => { const player = entry.player; @@ -395,6 +395,7 @@ export class AnimationTransitionNamespace { if (listener.name == entry.triggerName) { const baseEvent = makeAnimationEvent( element, entry.triggerName, entry.fromState.value, entry.toState.value); + (baseEvent as any)['_data'] = countId; listenOnPlayer(entry.player, listener.phase, baseEvent, listener.callback); } }); @@ -627,7 +628,7 @@ export class TransitionAnimationEngine { }); } - flush() { + flush(countId: number = -1) { let players: AnimationPlayer[] = []; if (this.newHostElements.size) { this.newHostElements.forEach((ns, element) => { this._balanceNamespaceList(ns, element); }); @@ -635,7 +636,7 @@ export class TransitionAnimationEngine { } if (this._namespaceList.length && (this.totalQueuedPlayers || this.queuedRemovals.size)) { - players = this._flushAnimations(); + players = this._flushAnimations(countId); } this.totalQueuedPlayers = 0; @@ -659,7 +660,7 @@ export class TransitionAnimationEngine { } } - private _flushAnimations(): TransitionAnimationPlayer[] { + private _flushAnimations(countId: number): TransitionAnimationPlayer[] { const subTimelines = new ElementInstructionMap(); const skippedPlayers: TransitionAnimationPlayer[] = []; const skippedPlayersMap = new Map(); @@ -677,8 +678,9 @@ export class TransitionAnimationEngine { for (let i = this._namespaceList.length - 1; i >= 0; i--) { const ns = this._namespaceList[i]; - ns.drainQueuedTransitions().forEach(entry => { + ns.drainQueuedTransitions(countId).forEach(entry => { const player = entry.player; + const element = entry.element; if (!bodyNode || !this.driver.containsElement(bodyNode, element)) { player.destroy(); @@ -746,7 +748,7 @@ export class TransitionAnimationEngine { } }); - allPreviousPlayersMap.forEach(players => { players.forEach(player => player.destroy()); }); + allPreviousPlayersMap.forEach(players => players.forEach(player => player.destroy())); const leaveNodes: any[] = bodyNode && allPostStyleElements.size ? listToArray(this.driver.query(bodyNode, LEAVE_SELECTOR, true)) : diff --git a/packages/animations/browser/test/engine/transition_animation_engine_spec.ts b/packages/animations/browser/test/engine/transition_animation_engine_spec.ts index 2878c4ce85..75cd0a1c5a 100644 --- a/packages/animations/browser/test/engine/transition_animation_engine_spec.ts +++ b/packages/animations/browser/test/engine/transition_animation_engine_spec.ts @@ -292,6 +292,7 @@ export function main() { setProperty(element, engine, 'myTrigger', '456'); engine.flush(); + delete (capture as any)['_data']; expect(capture).toEqual({ element, triggerName: 'myTrigger', @@ -305,6 +306,7 @@ export function main() { const player = engine.players.pop() !; player.finish(); + delete (capture as any)['_data']; expect(capture).toEqual({ element, triggerName: 'myTrigger', diff --git a/packages/core/test/animation/animation_integration_spec.ts b/packages/core/test/animation/animation_integration_spec.ts index b96ac7cdc0..9f69208e25 100644 --- a/packages/core/test/animation/animation_integration_spec.ts +++ b/packages/core/test/animation/animation_integration_spec.ts @@ -37,6 +37,51 @@ export function main() { }); }); + describe('fakeAsync testing', () => { + it('should only require one flushMicrotasks call to kick off animation callbacks', + fakeAsync(() => { + @Component({ + selector: 'cmp', + template: ` +
+ `, + animations: [trigger( + 'myAnimation', + [transition('* => on, * => off', [animate(1000, style({opacity: 1}))])])] + }) + class Cmp { + exp: any = false; + status: string = ''; + cb(status: string) { this.status = status; } + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; + cmp.exp = 'on'; + fixture.detectChanges(); + expect(cmp.status).toEqual(''); + + flushMicrotasks(); + expect(cmp.status).toEqual('start'); + + let player = MockAnimationDriver.log.pop() !; + player.finish(); + expect(cmp.status).toEqual('done'); + + cmp.status = ''; + cmp.exp = 'off'; + fixture.detectChanges(); + expect(cmp.status).toEqual(''); + + player = MockAnimationDriver.log.pop() !; + player.finish(); + expect(cmp.status).toEqual(''); + flushMicrotasks(); + expect(cmp.status).toEqual('done'); + })); + }); + describe('component fixture integration', () => { describe('whenRenderingDone', () => { it('should wait until the animations are finished until continuing', fakeAsync(() => { diff --git a/packages/platform-browser/animations/src/animation_renderer.ts b/packages/platform-browser/animations/src/animation_renderer.ts index 81250a3c14..de510acc08 100644 --- a/packages/platform-browser/animations/src/animation_renderer.ts +++ b/packages/platform-browser/animations/src/animation_renderer.ts @@ -12,6 +12,8 @@ import {Injectable, NgZone, Renderer2, RendererFactory2, RendererStyleFlags2, Re @Injectable() export class AnimationRendererFactory implements RendererFactory2 { private _currentId: number = 0; + private _currentFlushId: number = 1; + private _animationCallbacksBuffer: [(e: any) => any, any][] = []; constructor( private delegate: RendererFactory2, private _engine: AnimationEngine, private _zone: NgZone) { @@ -38,7 +40,7 @@ export class AnimationRendererFactory implements RendererFactory2 { animationTriggers.forEach( trigger => this._engine.registerTrigger( componentId, namespaceId, hostElement, trigger.name, trigger)); - return new AnimationRenderer(delegate, this._engine, this._zone, namespaceId); + return new AnimationRenderer(this, delegate, this._engine, this._zone, namespaceId); } begin() { @@ -47,8 +49,38 @@ export class AnimationRendererFactory implements RendererFactory2 { } } + private _scheduleCountTask() { + Zone.current.scheduleMicroTask( + 'incremenet the animation microtask', () => { this._currentFlushId++; }); + } + + /* @internal */ + scheduleListenerCallback(count: number, fn: (e: any) => any, data: any) { + if (count >= 0 && count < this._currentFlushId) { + this._zone.run(() => fn(data)); + return; + } + + if (this._animationCallbacksBuffer.length == 0) { + Promise.resolve(null).then(() => { + this._zone.run(() => { + this._animationCallbacksBuffer.forEach(tuple => { + const [fn, data] = tuple; + fn(data); + }); + this._animationCallbacksBuffer = []; + }); + }); + } + + this._animationCallbacksBuffer.push([fn, data]); + } + end() { - this._zone.runOutsideAngular(() => this._engine.flush()); + this._zone.runOutsideAngular(() => { + this._scheduleCountTask(); + this._engine.flush(this._currentFlushId); + }); if (this.delegate.end) { this.delegate.end(); } @@ -59,11 +91,11 @@ export class AnimationRendererFactory implements RendererFactory2 { export class AnimationRenderer implements Renderer2 { public destroyNode: ((node: any) => any)|null = null; - private _animationCallbacksBuffer: [(e: any) => any, any][] = []; + public microtaskCount: number = 0; constructor( - public delegate: Renderer2, private _engine: AnimationEngine, private _zone: NgZone, - private _namespaceId: string) { + private _factory: AnimationRendererFactory, public delegate: Renderer2, + private _engine: AnimationEngine, private _zone: NgZone, private _namespaceId: string) { this.destroyNode = this.delegate.destroyNode ? (n) => delegate.destroyNode !(n) : null; } @@ -145,27 +177,12 @@ export class AnimationRenderer implements Renderer2 { [name, phase] = parseTriggerCallbackName(name); } return this._engine.listen(this._namespaceId, element, name, phase, event => { - this._bufferMicrotaskIntoZone(callback, event); + const countId = (event as any)['_data'] || -1; + this._factory.scheduleListenerCallback(countId, callback, event); }); } return this.delegate.listen(target, eventName, callback); } - - private _bufferMicrotaskIntoZone(fn: (e: any) => any, data: any) { - if (this._animationCallbacksBuffer.length == 0) { - Promise.resolve(null).then(() => { - this._zone.run(() => { - this._animationCallbacksBuffer.forEach(tuple => { - const [fn, data] = tuple; - fn(data); - }); - this._animationCallbacksBuffer = []; - }); - }) - } - - this._animationCallbacksBuffer.push([fn, data]); - } } function resolveElementFromTarget(target: 'window' | 'document' | 'body' | any): any {