fix(animations): ensure all child elements are rendered before running animations
Closes #9402 Closes #9775 Closes #9887
This commit is contained in:
@@ -14,6 +14,8 @@ import {AnimationPlayer} from './animation_player';
|
||||
export class AnimationGroupPlayer implements AnimationPlayer {
|
||||
private _subscriptions: Function[] = [];
|
||||
private _finished = false;
|
||||
private _started = false;
|
||||
|
||||
public parentPlayer: AnimationPlayer = null;
|
||||
|
||||
constructor(private _players: AnimationPlayer[]) {
|
||||
@@ -44,9 +46,19 @@ export class AnimationGroupPlayer implements AnimationPlayer {
|
||||
}
|
||||
}
|
||||
|
||||
init(): void { this._players.forEach(player => player.init()); }
|
||||
|
||||
onDone(fn: Function): void { this._subscriptions.push(fn); }
|
||||
|
||||
play() { this._players.forEach(player => player.play()); }
|
||||
hasStarted() { return this._started; }
|
||||
|
||||
play() {
|
||||
if (!isPresent(this.parentPlayer)) {
|
||||
this.init();
|
||||
}
|
||||
this._started = true;
|
||||
this._players.forEach(player => player.play());
|
||||
}
|
||||
|
||||
pause(): void { this._players.forEach(player => player.pause()); }
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ import {scheduleMicroTask} from '../facade/lang';
|
||||
*/
|
||||
export abstract class AnimationPlayer {
|
||||
abstract onDone(fn: Function): void;
|
||||
abstract init(): void;
|
||||
abstract hasStarted(): boolean;
|
||||
abstract play(): void;
|
||||
abstract pause(): void;
|
||||
abstract restart(): void;
|
||||
@@ -31,6 +33,7 @@ export abstract class AnimationPlayer {
|
||||
|
||||
export class NoOpAnimationPlayer implements AnimationPlayer {
|
||||
private _subscriptions: any[] /** TODO #9100 */ = [];
|
||||
private _started = false;
|
||||
public parentPlayer: AnimationPlayer = null;
|
||||
constructor() { scheduleMicroTask(() => this._onFinish()); }
|
||||
/** @internal */
|
||||
@@ -39,7 +42,9 @@ export class NoOpAnimationPlayer implements AnimationPlayer {
|
||||
this._subscriptions = [];
|
||||
}
|
||||
onDone(fn: Function): void { this._subscriptions.push(fn); }
|
||||
play(): void {}
|
||||
hasStarted(): boolean { return this._started; }
|
||||
init(): void {}
|
||||
play(): void { this._started = true; }
|
||||
pause(): void {}
|
||||
restart(): void {}
|
||||
finish(): void { this._onFinish(); }
|
||||
|
||||
@@ -15,6 +15,7 @@ export class AnimationSequencePlayer implements AnimationPlayer {
|
||||
private _activePlayer: AnimationPlayer;
|
||||
private _subscriptions: Function[] = [];
|
||||
private _finished = false;
|
||||
private _started: boolean = false;
|
||||
|
||||
public parentPlayer: AnimationPlayer = null;
|
||||
|
||||
@@ -54,9 +55,19 @@ export class AnimationSequencePlayer implements AnimationPlayer {
|
||||
}
|
||||
}
|
||||
|
||||
init(): void { this._players.forEach(player => player.init()); }
|
||||
|
||||
onDone(fn: Function): void { this._subscriptions.push(fn); }
|
||||
|
||||
play(): void { this._activePlayer.play(); }
|
||||
hasStarted() { return this._started; }
|
||||
|
||||
play(): void {
|
||||
if (!isPresent(this.parentPlayer)) {
|
||||
this.init();
|
||||
}
|
||||
this._started = true;
|
||||
this._activePlayer.play();
|
||||
}
|
||||
|
||||
pause(): void { this._activePlayer.pause(); }
|
||||
|
||||
|
||||
+3
-3
@@ -11,7 +11,7 @@ import {isPresent} from '../facade/lang';
|
||||
|
||||
import {AnimationPlayer} from './animation_player';
|
||||
|
||||
export class ActiveAnimationPlayersMap {
|
||||
export class ViewAnimationMap {
|
||||
private _map = new Map<any, {[key: string]: AnimationPlayer}>();
|
||||
private _allPlayers: AnimationPlayer[] = [];
|
||||
|
||||
@@ -25,9 +25,9 @@ export class ActiveAnimationPlayersMap {
|
||||
}
|
||||
|
||||
findAllPlayersByElement(element: any): AnimationPlayer[] {
|
||||
var players: any[] /** TODO #9100 */ = [];
|
||||
var players: AnimationPlayer[] = [];
|
||||
StringMapWrapper.forEach(
|
||||
this._map.get(element), (player: any /** TODO #9100 */) => players.push(player));
|
||||
this._map.get(element), (player: AnimationPlayer) => players.push(player));
|
||||
return players;
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ import {AnimationPlayer} from '../animation/animation_player';
|
||||
import {AnimationGroupPlayer} from '../animation/animation_group_player';
|
||||
import {AnimationKeyframe} from '../animation/animation_keyframe';
|
||||
import {AnimationStyles} from '../animation/animation_styles';
|
||||
import {ActiveAnimationPlayersMap} from '../animation/active_animation_players_map';
|
||||
import {ViewAnimationMap} from '../animation/view_animation_map';
|
||||
|
||||
var _scope_check: WtfScopeFn = wtfCreateScope(`AppView#check(ascii id)`);
|
||||
|
||||
@@ -54,7 +54,7 @@ export abstract class AppView<T> {
|
||||
|
||||
private _hasExternalHostElement: boolean;
|
||||
|
||||
public activeAnimationPlayers = new ActiveAnimationPlayersMap();
|
||||
public animationPlayers = new ViewAnimationMap();
|
||||
|
||||
public context: T;
|
||||
|
||||
@@ -74,20 +74,26 @@ export abstract class AppView<T> {
|
||||
|
||||
cancelActiveAnimation(element: any, animationName: string, removeAllAnimations: boolean = false) {
|
||||
if (removeAllAnimations) {
|
||||
this.activeAnimationPlayers.findAllPlayersByElement(element).forEach(
|
||||
player => player.destroy());
|
||||
this.animationPlayers.findAllPlayersByElement(element).forEach(player => player.destroy());
|
||||
} else {
|
||||
var player = this.activeAnimationPlayers.find(element, animationName);
|
||||
var player = this.animationPlayers.find(element, animationName);
|
||||
if (isPresent(player)) {
|
||||
player.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerAndStartAnimation(element: any, animationName: string, player: AnimationPlayer): void {
|
||||
this.activeAnimationPlayers.set(element, animationName, player);
|
||||
player.onDone(() => { this.activeAnimationPlayers.remove(element, animationName); });
|
||||
player.play();
|
||||
queueAnimation(element: any, animationName: string, player: AnimationPlayer): void {
|
||||
this.animationPlayers.set(element, animationName, player);
|
||||
player.onDone(() => { this.animationPlayers.remove(element, animationName); });
|
||||
}
|
||||
|
||||
triggerQueuedAnimations() {
|
||||
this.animationPlayers.getAllPlayers().forEach(player => {
|
||||
if (!player.hasStarted()) {
|
||||
player.play();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
create(context: T, givenProjectableNodes: Array<any|any[]>, rootSelectorOrNode: string|any):
|
||||
@@ -201,10 +207,10 @@ export abstract class AppView<T> {
|
||||
this.destroyInternal();
|
||||
this.dirtyParentQueriesInternal();
|
||||
|
||||
if (this.activeAnimationPlayers.length == 0) {
|
||||
if (this.animationPlayers.length == 0) {
|
||||
this.renderer.destroyView(hostElement, this.allNodes);
|
||||
} else {
|
||||
var player = new AnimationGroupPlayer(this.activeAnimationPlayers.getAllPlayers());
|
||||
var player = new AnimationGroupPlayer(this.animationPlayers.getAllPlayers());
|
||||
player.onDone(() => { this.renderer.destroyView(hostElement, this.allNodes); });
|
||||
}
|
||||
}
|
||||
@@ -221,10 +227,10 @@ export abstract class AppView<T> {
|
||||
|
||||
detach(): void {
|
||||
this.detachInternal();
|
||||
if (this.activeAnimationPlayers.length == 0) {
|
||||
if (this.animationPlayers.length == 0) {
|
||||
this.renderer.detachView(this.flatRootNodes);
|
||||
} else {
|
||||
var player = new AnimationGroupPlayer(this.activeAnimationPlayers.getAllPlayers());
|
||||
var player = new AnimationGroupPlayer(this.animationPlayers.getAllPlayers());
|
||||
player.onDone(() => { this.renderer.detachView(this.flatRootNodes); });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
|
||||
import {el} from '@angular/platform-browser/testing/browser_util';
|
||||
|
||||
import {ActiveAnimationPlayersMap} from '../../src/animation/active_animation_players_map';
|
||||
import {MockAnimationPlayer} from '../../../platform-browser/testing/mock_animation_player';
|
||||
import {ViewAnimationMap} from '../../src/animation/view_animation_map';
|
||||
import {isPresent} from '../../src/facade/lang';
|
||||
import {fakeAsync, flushMicrotasks} from '../../testing';
|
||||
import {MockAnimationPlayer} from '../../testing/animation/mock_animation_player';
|
||||
import {AsyncTestCompleter, beforeEach, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '../../testing/testing_internal';
|
||||
|
||||
export function main() {
|
||||
@@ -22,7 +22,7 @@ export function main() {
|
||||
var animationName = 'animationName';
|
||||
|
||||
beforeEach(() => {
|
||||
playersMap = new ActiveAnimationPlayersMap();
|
||||
playersMap = new ViewAnimationMap();
|
||||
elementNode = el('<div></div>');
|
||||
});
|
||||
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {MockAnimationPlayer} from '../../../platform-browser/testing/mock_animation_player';
|
||||
import {AnimationGroupPlayer} from '../../src/animation/animation_group_player';
|
||||
import {isPresent} from '../../src/facade/lang';
|
||||
import {fakeAsync, flushMicrotasks} from '../../testing';
|
||||
import {MockAnimationPlayer} from '../../testing/animation/mock_animation_player';
|
||||
import {AsyncTestCompleter, beforeEach, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '../../testing/testing_internal';
|
||||
|
||||
export function main() {
|
||||
|
||||
@@ -12,9 +12,13 @@ import {TestComponentBuilder} from '@angular/compiler/testing';
|
||||
import {AnimationDriver} from '@angular/platform-browser/src/dom/animation_driver';
|
||||
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
|
||||
import {MockAnimationDriver} from '@angular/platform-browser/testing/mock_animation_driver';
|
||||
import {MockAnimationPlayer} from '@angular/platform-browser/testing/mock_animation_player';
|
||||
|
||||
import {Component} from '../../index';
|
||||
import {DEFAULT_STATE} from '../../src/animation/animation_constants';
|
||||
import {AnimationKeyframe} from '../../src/animation/animation_keyframe';
|
||||
import {AnimationPlayer} from '../../src/animation/animation_player';
|
||||
import {AnimationStyles} from '../../src/animation/animation_styles';
|
||||
import {AnimationEntryMetadata, animate, group, keyframes, sequence, state, style, transition, trigger} from '../../src/animation/metadata';
|
||||
import {AUTO_STYLE} from '../../src/animation/metadata';
|
||||
import {IS_DART, isArray, isPresent} from '../../src/facade/lang';
|
||||
@@ -26,7 +30,6 @@ export function main() {
|
||||
declareTests({useJit: false});
|
||||
} else {
|
||||
describe('jit', () => { declareTests({useJit: true}); });
|
||||
|
||||
describe('no jit', () => { declareTests({useJit: false}); });
|
||||
}
|
||||
}
|
||||
@@ -748,6 +751,132 @@ function declareTests({useJit}: {useJit: boolean}) {
|
||||
})));
|
||||
});
|
||||
|
||||
describe('DOM order tracking', () => {
|
||||
if (!getDOM().supportsDOMEvents()) return;
|
||||
|
||||
beforeEachProviders(
|
||||
() => [{provide: AnimationDriver, useClass: InnerContentTrackingAnimationDriver}]);
|
||||
|
||||
it('should evaluate all inner children and their bindings before running the animation on a parent',
|
||||
inject(
|
||||
[TestComponentBuilder, AnimationDriver],
|
||||
fakeAsync((tcb: TestComponentBuilder, driver: InnerContentTrackingAnimationDriver) => {
|
||||
makeAnimationCmp(
|
||||
tcb, `<div class="target" [@status]="exp">
|
||||
<div *ngIf="exp2" class="inner">inner child guy</div>
|
||||
</div>`,
|
||||
[trigger(
|
||||
'status',
|
||||
[
|
||||
state('final', style({'height': '*'})),
|
||||
transition('* => *', [animate(1000)])
|
||||
])],
|
||||
(fixture: any /** TODO #9100 */) => {
|
||||
tick();
|
||||
|
||||
var cmp = fixture.debugElement.componentInstance;
|
||||
var node =
|
||||
getDOM().querySelector(fixture.debugElement.nativeElement, '.target');
|
||||
cmp.exp = true;
|
||||
cmp.exp2 = true;
|
||||
fixture.detectChanges();
|
||||
flushMicrotasks();
|
||||
|
||||
var animation = driver.log.pop();
|
||||
var player = <InnerContentTrackingAnimationPlayer>animation['player'];
|
||||
expect(player.capturedInnerText).toEqual('inner child guy');
|
||||
});
|
||||
})));
|
||||
|
||||
it('should run the initialization stage after all children have been evaluated',
|
||||
inject(
|
||||
[TestComponentBuilder, AnimationDriver],
|
||||
fakeAsync((tcb: TestComponentBuilder, driver: InnerContentTrackingAnimationDriver) => {
|
||||
makeAnimationCmp(
|
||||
tcb, `<div class="target" [@status]="exp">
|
||||
<div style="height:20px"></div>
|
||||
<div *ngIf="exp2" style="height:40px;" class="inner">inner child guy</div>
|
||||
</div>`,
|
||||
[trigger('status', [transition('* => *', sequence([
|
||||
animate(1000, style({height: 0})),
|
||||
animate(1000, style({height: '*'}))
|
||||
]))])],
|
||||
(fixture: any /** TODO #9100 */) => {
|
||||
tick();
|
||||
|
||||
var cmp = fixture.debugElement.componentInstance;
|
||||
cmp.exp = true;
|
||||
cmp.exp2 = true;
|
||||
fixture.detectChanges();
|
||||
flushMicrotasks();
|
||||
fixture.detectChanges();
|
||||
|
||||
var animation = driver.log.pop();
|
||||
var player = <InnerContentTrackingAnimationPlayer>animation['player'];
|
||||
|
||||
// this is just to confirm that the player is using the parent element
|
||||
expect(player.element.className).toEqual('target');
|
||||
expect(player.computedHeight).toEqual('60px');
|
||||
});
|
||||
})));
|
||||
|
||||
it('should not trigger animations more than once within a view that contains multiple animation triggers',
|
||||
inject(
|
||||
[TestComponentBuilder, AnimationDriver],
|
||||
fakeAsync((tcb: TestComponentBuilder, driver: InnerContentTrackingAnimationDriver) => {
|
||||
makeAnimationCmp(
|
||||
tcb, `<div *ngIf="exp" @one><div class="inner"></div></div>
|
||||
<div *ngIf="exp2" @two><div class="inner"></div></div>`,
|
||||
[
|
||||
trigger('one', [transition('* => *', [animate(1000)])]),
|
||||
trigger('two', [transition('* => *', [animate(2000)])])
|
||||
],
|
||||
(fixture: any /** TODO #9100 */) => {
|
||||
var cmp = fixture.debugElement.componentInstance;
|
||||
cmp.exp = true;
|
||||
cmp.exp2 = true;
|
||||
fixture.detectChanges();
|
||||
flushMicrotasks();
|
||||
|
||||
expect(driver.log.length).toEqual(2);
|
||||
var animation1 = driver.log.pop();
|
||||
var animation2 = driver.log.pop();
|
||||
var player1 = <InnerContentTrackingAnimationPlayer>animation1['player'];
|
||||
var player2 = <InnerContentTrackingAnimationPlayer>animation2['player'];
|
||||
expect(player1.playAttempts).toEqual(1);
|
||||
expect(player2.playAttempts).toEqual(1);
|
||||
});
|
||||
})));
|
||||
|
||||
it('should trigger animations when animations are detached from the page',
|
||||
inject(
|
||||
[TestComponentBuilder, AnimationDriver],
|
||||
fakeAsync((tcb: TestComponentBuilder, driver: InnerContentTrackingAnimationDriver) => {
|
||||
makeAnimationCmp(
|
||||
tcb, `<div *ngIf="exp" @trigger><div class="inner"></div></div>`,
|
||||
[
|
||||
trigger('trigger', [transition('* => void', [animate(1000)])]),
|
||||
],
|
||||
(fixture: any /** TODO #9100 */) => {
|
||||
var cmp = fixture.debugElement.componentInstance;
|
||||
cmp.exp = true;
|
||||
fixture.detectChanges();
|
||||
flushMicrotasks();
|
||||
|
||||
expect(driver.log.length).toEqual(0);
|
||||
|
||||
cmp.exp = false;
|
||||
fixture.detectChanges();
|
||||
flushMicrotasks();
|
||||
|
||||
expect(driver.log.length).toEqual(1);
|
||||
var animation = driver.log.pop();
|
||||
var player = <InnerContentTrackingAnimationPlayer>animation['player'];
|
||||
expect(player.playAttempts).toEqual(1);
|
||||
});
|
||||
})));
|
||||
});
|
||||
|
||||
describe('animation states', () => {
|
||||
it('should retain the destination animation state styles once the animation is complete',
|
||||
inject(
|
||||
@@ -1049,3 +1178,26 @@ class DummyIfCmp {
|
||||
exp = false;
|
||||
exp2 = false;
|
||||
}
|
||||
|
||||
class InnerContentTrackingAnimationDriver extends MockAnimationDriver {
|
||||
animate(
|
||||
element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[],
|
||||
duration: number, delay: number, easing: string): AnimationPlayer {
|
||||
super.animate(element, startingStyles, keyframes, duration, delay, easing);
|
||||
var player = new InnerContentTrackingAnimationPlayer(element);
|
||||
this.log[this.log.length - 1]['player'] = player;
|
||||
return player;
|
||||
}
|
||||
}
|
||||
|
||||
class InnerContentTrackingAnimationPlayer extends MockAnimationPlayer {
|
||||
constructor(public element: any) { super(); }
|
||||
public computedHeight: number;
|
||||
public capturedInnerText: string;
|
||||
public playAttempts = 0;
|
||||
init() { this.computedHeight = getDOM().getComputedStyle(this.element)['height']; }
|
||||
play() {
|
||||
this.playAttempts++;
|
||||
this.capturedInnerText = this.element.querySelector('.inner').innerText;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {MockAnimationPlayer} from '../../../platform-browser/testing/mock_animation_player';
|
||||
import {AnimationSequencePlayer} from '../../src/animation/animation_sequence_player';
|
||||
import {isPresent} from '../../src/facade/lang';
|
||||
import {fakeAsync, flushMicrotasks} from '../../testing';
|
||||
import {MockAnimationPlayer} from '../../testing/animation/mock_animation_player';
|
||||
import {AsyncTestCompleter, beforeEach, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '../../testing/testing_internal';
|
||||
|
||||
export function main() {
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
/**
|
||||
* @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 {AnimationPlayer} from '../../src/animation/animation_player';
|
||||
import {isPresent} from '../../src/facade/lang';
|
||||
|
||||
export class MockAnimationPlayer implements AnimationPlayer {
|
||||
private _subscriptions: any[] /** TODO #9100 */ = [];
|
||||
private _finished = false;
|
||||
private _destroyed = false;
|
||||
public parentPlayer: AnimationPlayer = null;
|
||||
|
||||
public log: any[] /** TODO #9100 */ = [];
|
||||
|
||||
private _onfinish(): void {
|
||||
if (!this._finished) {
|
||||
this._finished = true;
|
||||
this.log.push('finish');
|
||||
|
||||
this._subscriptions.forEach((entry) => { entry(); });
|
||||
this._subscriptions = [];
|
||||
if (!isPresent(this.parentPlayer)) {
|
||||
this.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onDone(fn: Function): void { this._subscriptions.push(fn); }
|
||||
|
||||
play(): void { this.log.push('play'); }
|
||||
|
||||
pause(): void { this.log.push('pause'); }
|
||||
|
||||
restart(): void { this.log.push('restart'); }
|
||||
|
||||
finish(): void { this._onfinish(); }
|
||||
|
||||
reset(): void { this.log.push('reset'); }
|
||||
|
||||
destroy(): void {
|
||||
if (!this._destroyed) {
|
||||
this._destroyed = true;
|
||||
this.finish();
|
||||
this.log.push('destroy');
|
||||
}
|
||||
}
|
||||
|
||||
setPosition(p: any /** TODO #9100 */): void {}
|
||||
getPosition(): number { return 0; }
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import {Math, global, isFunction, isPromise} from '../src/facade/lang';
|
||||
import {AsyncTestCompleter} from './async_test_completer';
|
||||
import {getTestInjector, inject} from './test_injector';
|
||||
|
||||
export {MockAnimationPlayer} from './animation/mock_animation_player';
|
||||
export {MockAnimationPlayer} from '@angular/platform-browser/testing/mock_animation_player';
|
||||
export {AsyncTestCompleter} from './async_test_completer';
|
||||
export {inject} from './test_injector';
|
||||
export {expect} from './testing';
|
||||
|
||||
Reference in New Issue
Block a user