fix(animations): ensure all child elements are rendered before running animations

Closes #9402
Closes #9775
Closes #9887
This commit is contained in:
Matias Niemelä
2016-07-01 16:01:57 -07:00
parent daa9da4047
commit c3bdd504d0
20 changed files with 340 additions and 80 deletions
@@ -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(); }
@@ -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;
}
+19 -13
View File
@@ -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';