diff --git a/packages/animations/browser/src/render/transition_animation_engine.ts b/packages/animations/browser/src/render/transition_animation_engine.ts index 0d3f06f820..0e65e9753d 100644 --- a/packages/animations/browser/src/render/transition_animation_engine.ts +++ b/packages/animations/browser/src/render/transition_animation_engine.ts @@ -986,11 +986,15 @@ export class TransitionAnimationEngine { } const allPreviousPlayersMap = new Map(); - let sortedParentElements: any[] = []; + // this map works to tell which element in the DOM tree is contained by + // which animation. Further down below this map will get populated once + // the players are built and in doing so it can efficiently figure out + // if a sub player is skipped due to a parent player having priority. + const animationElementMap = new Map(); queuedInstructions.forEach(entry => { const element = entry.element; if (subTimelines.has(element)) { - sortedParentElements.unshift(element); + animationElementMap.set(element, element); this._beforeAnimationBuild( entry.player.namespaceId, entry.instruction, allPreviousPlayersMap); } @@ -1041,6 +1045,7 @@ export class TransitionAnimationEngine { const rootPlayers: TransitionAnimationPlayer[] = []; const subPlayers: TransitionAnimationPlayer[] = []; + const NO_PARENT_ANIMATION_ELEMENT_DETECTED = {}; queuedInstructions.forEach(entry => { const {element, player, instruction} = entry; // this means that it was never consumed by a parent animation which @@ -1052,29 +1057,41 @@ export class TransitionAnimationEngine { return; } + // this will flow up the DOM and query the map to figure out + // if a parent animation has priority over it. In the situation + // that a parent is detected then it will cancel the loop. If + // nothing is detected, or it takes a few hops to find a parent, + // then it will fill in the missing nodes and signal them as having + // a detected parent (or a NO_PARENT value via a special constant). + let parentWithAnimation: any = NO_PARENT_ANIMATION_ELEMENT_DETECTED; + if (animationElementMap.size > 1) { + let elm = element; + const parentsToAdd: any[] = []; + while (elm = elm.parentNode) { + const detectedParent = animationElementMap.get(elm); + if (detectedParent) { + parentWithAnimation = detectedParent; + break; + } + parentsToAdd.push(elm); + } + parentsToAdd.forEach(parent => animationElementMap.set(parent, parentWithAnimation)); + } + const innerPlayer = this._buildAnimation( player.namespaceId, instruction, allPreviousPlayersMap, skippedPlayersMap, preStylesMap, postStylesMap); + player.setRealPlayer(innerPlayer); - let parentHasPriority: any = null; - for (let i = 0; i < sortedParentElements.length; i++) { - const parent = sortedParentElements[i]; - if (parent === element) break; - if (this.driver.containsElement(parent, element)) { - parentHasPriority = parent; - break; - } - } - - if (parentHasPriority) { - const parentPlayers = this.playersByElement.get(parentHasPriority); + if (parentWithAnimation === NO_PARENT_ANIMATION_ELEMENT_DETECTED) { + rootPlayers.push(player); + } else { + const parentPlayers = this.playersByElement.get(parentWithAnimation); if (parentPlayers && parentPlayers.length) { player.parentPlayer = optimizeGroupPlayer(parentPlayers); } skippedPlayers.push(player); - } else { - rootPlayers.push(player); } } else { eraseStyles(element, instruction.fromStyles); @@ -1105,7 +1122,7 @@ export class TransitionAnimationEngine { // fire the start/done transition callback events skippedPlayers.forEach(player => { if (player.parentPlayer) { - player.parentPlayer.onDestroy(() => player.destroy()); + player.syncPlayerEvents(player.parentPlayer); } else { player.destroy(); } @@ -1366,6 +1383,15 @@ export class TransitionAnimationPlayer implements AnimationPlayer { getRealPlayer() { return this._player; } + syncPlayerEvents(player: AnimationPlayer) { + const p = this._player as any; + if (p.triggerCallback) { + player.onStart(() => p.triggerCallback('start')); + } + player.onDone(() => this.finish()); + player.onDestroy(() => this.destroy()); + } + private _queueEvent(name: string, callback: (event: any) => any): void { getOrSetAsInMap(this._queuedCallbacks, name, []).push(callback); } @@ -1419,6 +1445,14 @@ export class TransitionAnimationPlayer implements AnimationPlayer { getPosition(): number { return this.queued ? 0 : this._player.getPosition(); } get totalTime(): number { return this._player.totalTime; } + + /* @internal */ + triggerCallback(phaseName: string): void { + const p = this._player as any; + if (p.triggerCallback) { + p.triggerCallback(phaseName); + } + } } function deleteOrUnsetInMap(map: Map| {[key: string]: any}, key: any, value: any) { diff --git a/packages/animations/browser/src/render/web_animations/web_animations_player.ts b/packages/animations/browser/src/render/web_animations/web_animations_player.ts index dc7396f4f9..879bcf3f1d 100644 --- a/packages/animations/browser/src/render/web_animations/web_animations_player.ts +++ b/packages/animations/browser/src/render/web_animations/web_animations_player.ts @@ -184,6 +184,13 @@ export class WebAnimationsPlayer implements AnimationPlayer { } this.currentSnapshot = styles; } + + /* @internal */ + triggerCallback(phaseName: string): void { + const methods = phaseName == 'start' ? this._onStartFns : this._onDoneFns; + methods.forEach(fn => fn()); + methods.length = 0; + } } function _computeStyle(element: any, prop: string): string { diff --git a/packages/animations/browser/test/render/web_animations/web_animations_player_spec.ts b/packages/animations/browser/test/render/web_animations/web_animations_player_spec.ts index d7dc7aefe7..a258ca03a0 100644 --- a/packages/animations/browser/test/render/web_animations/web_animations_player_spec.ts +++ b/packages/animations/browser/test/render/web_animations/web_animations_player_spec.ts @@ -45,6 +45,26 @@ export function main() { const p = innerPlayer !; expect(p.log).toEqual(['play']); }); + + it('should fire start/done callbacks manually when called directly', () => { + const log: string[] = []; + + const player = new WebAnimationsPlayer(element, [], {duration: 1000}); + player.onStart(() => log.push('started')); + player.onDone(() => log.push('done')); + + player.triggerCallback('start'); + expect(log).toEqual(['started']); + + player.play(); + expect(log).toEqual(['started']); + + player.triggerCallback('done'); + expect(log).toEqual(['started', 'done']); + + player.finish(); + expect(log).toEqual(['started', 'done']); + }); }); } diff --git a/packages/animations/src/players/animation_group_player.ts b/packages/animations/src/players/animation_group_player.ts index b67c4ef82f..a73ebb2975 100644 --- a/packages/animations/src/players/animation_group_player.ts +++ b/packages/animations/src/players/animation_group_player.ts @@ -140,4 +140,11 @@ export class AnimationGroupPlayer implements AnimationPlayer { } }); } + + /* @internal */ + triggerCallback(phaseName: string): void { + const methods = phaseName == 'start' ? this._onStartFns : this._onDoneFns; + methods.forEach(fn => fn()); + methods.length = 0; + } } diff --git a/packages/animations/src/players/animation_player.ts b/packages/animations/src/players/animation_player.ts index 14d14f35f6..e8b5392be6 100644 --- a/packages/animations/src/players/animation_player.ts +++ b/packages/animations/src/players/animation_player.ts @@ -31,6 +31,8 @@ export interface AnimationPlayer { parentPlayer: AnimationPlayer|null; readonly totalTime: number; beforeDestroy?: () => any; + /* @internal */ + triggerCallback?: (phaseName: string) => void; } /** @@ -91,4 +93,11 @@ export class NoopAnimationPlayer implements AnimationPlayer { reset(): void {} setPosition(p: number): void {} getPosition(): number { return 0; } -} + + /* @internal */ + triggerCallback(phaseName: string): void { + const methods = phaseName == 'start' ? this._onStartFns : this._onDoneFns; + methods.forEach(fn => fn()); + methods.length = 0; + } +} \ No newline at end of file diff --git a/packages/animations/test/animation_player_spec.ts b/packages/animations/test/animation_player_spec.ts index bf86c8f8aa..aef8460d49 100644 --- a/packages/animations/test/animation_player_spec.ts +++ b/packages/animations/test/animation_player_spec.ts @@ -40,5 +40,29 @@ export function main() { player.destroy(); expect(log).toEqual(['started', 'done', 'destroy']); }); + + it('should fire start/done callbacks manually when called directly', fakeAsync(() => { + const log: string[] = []; + + const player = new NoopAnimationPlayer(); + player.onStart(() => log.push('started')); + player.onDone(() => log.push('done')); + flushMicrotasks(); + + player.triggerCallback('start'); + expect(log).toEqual(['started']); + + player.play(); + expect(log).toEqual(['started']); + + player.triggerCallback('done'); + expect(log).toEqual(['started', 'done']); + + player.finish(); + expect(log).toEqual(['started', 'done']); + + flushMicrotasks(); + expect(log).toEqual(['started', 'done']); + })); }); } diff --git a/packages/core/test/animation/animation_query_integration_spec.ts b/packages/core/test/animation/animation_query_integration_spec.ts index c694c2afe5..736e2dc12b 100644 --- a/packages/core/test/animation/animation_query_integration_spec.ts +++ b/packages/core/test/animation/animation_query_integration_spec.ts @@ -409,39 +409,39 @@ export function main() { const fixture = TestBed.createComponent(Cmp); const cmp = fixture.componentInstance; - cmp.exp0 = 0; - cmp.exp1 = 0; cmp.exp2 = 0; cmp.exp3 = 0; cmp.exp4 = 0; cmp.exp5 = 0; fixture.detectChanges(); - engine.flush(); + + cmp.exp0 = 0; + fixture.detectChanges(); let players = engine.players; cancelAllPlayers(players); - cmp.exp0 = 1; - cmp.exp2 = 1; cmp.exp4 = 1; fixture.detectChanges(); - engine.flush(); + + cmp.exp0 = 1; + fixture.detectChanges(); players = engine.players; cancelAllPlayers(players); expect(players.length).toEqual(3); - cmp.exp0 = 2; - cmp.exp1 = 2; cmp.exp2 = 2; cmp.exp3 = 2; cmp.exp4 = 2; cmp.exp5 = 2; fixture.detectChanges(); - engine.flush(); + + cmp.exp0 = 2; + fixture.detectChanges(); players = engine.players; cancelAllPlayers(players); @@ -449,7 +449,6 @@ export function main() { cmp.exp0 = 3; fixture.detectChanges(); - engine.flush(); players = engine.players; cancelAllPlayers(players); @@ -2288,19 +2287,15 @@ export function main() { } TestBed.configureTestingModule({declarations: [ParentCmp, ChildCmp]}); - - const engine = TestBed.get(ɵAnimationEngine); const fixture = TestBed.createComponent(ParentCmp); const cmp = fixture.componentInstance; fixture.detectChanges(); - engine.flush(); const childCmp = cmp.childElm; cmp.exp = false; fixture.detectChanges(); - engine.flush(); flushMicrotasks(); expect(cmp.childEvent.toState).toEqual('void'); @@ -2728,6 +2723,106 @@ export function main() { expect(engine.players[0].getRealPlayer()).toBe(players[1]); }); + it('should fire and synchronize the start/done callbacks on sub triggers even if they are not allowed to animate within the animation', + fakeAsync(() => { + @Component({ + selector: 'parent-cmp', + animations: [ + trigger( + 'parent', + [ + transition( + '* => go', + [ + style({height: '0px'}), + animate(1000, style({height: '100px'})), + ]), + ]), + ], + template: ` +
+ +
+ ` + }) + class ParentCmp { + @ViewChild('child') public childCmp: any; + + public exp: any; + public log: string[] = []; + public remove = false; + + track(event: any) { this.log.push(`${event.triggerName}-${event.phaseName}`); } + } + + @Component({ + selector: 'child-cmp', + animations: [ + trigger( + 'child', + [ + transition( + '* => go', + [ + style({width: '0px'}), + animate(1000, style({width: '100px'})), + ]), + ]), + ], + template: ` +
+ ` + }) + class ChildCmp { + public exp: any; + public log: string[] = []; + track(event: any) { this.log.push(`${event.triggerName}-${event.phaseName}`); } + } + + TestBed.configureTestingModule({declarations: [ParentCmp, ChildCmp]}); + const engine = TestBed.get(ɵAnimationEngine); + const fixture = TestBed.createComponent(ParentCmp); + fixture.detectChanges(); + flushMicrotasks(); + + const cmp = fixture.componentInstance; + const child = cmp.childCmp; + + expect(cmp.log).toEqual(['parent-start', 'parent-done']); + expect(child.log).toEqual(['child-start', 'child-done']); + + cmp.log = []; + child.log = []; + cmp.exp = 'go'; + cmp.childCmp.exp = 'go'; + fixture.detectChanges(); + flushMicrotasks(); + + expect(cmp.log).toEqual(['parent-start']); + expect(child.log).toEqual(['child-start']); + + const players = engine.players; + expect(players.length).toEqual(1); + players[0].finish(); + + expect(cmp.log).toEqual(['parent-start', 'parent-done']); + expect(child.log).toEqual(['child-start', 'child-done']); + + cmp.log = []; + child.log = []; + cmp.remove = true; + fixture.detectChanges(); + flushMicrotasks(); + + expect(cmp.log).toEqual(['parent-start', 'parent-done']); + expect(child.log).toEqual(['child-start', 'child-done']); + })); + it('should stretch the starting keyframe of a child animation queries are issued by the parent', () => { @Component({