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

View File

@ -271,7 +271,7 @@ class _AnimationBuilder implements AnimationAstVisitor {
statements.push(_ANIMATION_FACTORY_VIEW_VAR statements.push(_ANIMATION_FACTORY_VIEW_VAR
.callMethod( .callMethod(
'registerAndStartAnimation', 'queueAnimation',
[ [
_ANIMATION_FACTORY_ELEMENT_VAR, o.literal(this.animationName), _ANIMATION_FACTORY_ELEMENT_VAR, o.literal(this.animationName),
_ANIMATION_PLAYER_VAR _ANIMATION_PLAYER_VAR

View File

@ -36,6 +36,8 @@ function createCurrValueExpr(exprIndex: number): o.ReadVarExpr {
return o.variable(`currVal_${exprIndex}`); // fix syntax highlighting: ` return o.variable(`currVal_${exprIndex}`); // fix syntax highlighting: `
} }
const _animationViewCheckedFlagMap = new Map<CompileView, boolean>();
function bind( function bind(
view: CompileView, currValExpr: o.ReadVarExpr, fieldExpr: o.ReadPropExpr, view: CompileView, currValExpr: o.ReadVarExpr, fieldExpr: o.ReadPropExpr,
parsedExpression: cdAst.AST, context: o.Expression, actions: o.Statement[], parsedExpression: cdAst.AST, context: o.Expression, actions: o.Statement[],
@ -171,6 +173,13 @@ function bindAndWriteToRenderer(
animation.fnVariable.callFn([o.THIS_EXPR, renderNode, oldRenderValue, emptyStateValue]) animation.fnVariable.callFn([o.THIS_EXPR, renderNode, oldRenderValue, emptyStateValue])
.toStmt()); .toStmt());
if (!_animationViewCheckedFlagMap.get(view)) {
_animationViewCheckedFlagMap.set(view, true);
var triggerStmt = o.THIS_EXPR.callMethod('triggerQueuedAnimations', []).toStmt();
view.afterViewLifecycleCallbacksMethod.addStmt(triggerStmt);
view.detachMethod.addStmt(triggerStmt);
}
break; break;
} }

View File

@ -14,6 +14,8 @@ import {AnimationPlayer} from './animation_player';
export class AnimationGroupPlayer implements AnimationPlayer { export class AnimationGroupPlayer implements AnimationPlayer {
private _subscriptions: Function[] = []; private _subscriptions: Function[] = [];
private _finished = false; private _finished = false;
private _started = false;
public parentPlayer: AnimationPlayer = null; public parentPlayer: AnimationPlayer = null;
constructor(private _players: AnimationPlayer[]) { 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); } 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()); } pause(): void { this._players.forEach(player => player.pause()); }

View File

@ -15,6 +15,8 @@ import {scheduleMicroTask} from '../facade/lang';
*/ */
export abstract class AnimationPlayer { export abstract class AnimationPlayer {
abstract onDone(fn: Function): void; abstract onDone(fn: Function): void;
abstract init(): void;
abstract hasStarted(): boolean;
abstract play(): void; abstract play(): void;
abstract pause(): void; abstract pause(): void;
abstract restart(): void; abstract restart(): void;
@ -31,6 +33,7 @@ export abstract class AnimationPlayer {
export class NoOpAnimationPlayer implements AnimationPlayer { export class NoOpAnimationPlayer implements AnimationPlayer {
private _subscriptions: any[] /** TODO #9100 */ = []; private _subscriptions: any[] /** TODO #9100 */ = [];
private _started = false;
public parentPlayer: AnimationPlayer = null; public parentPlayer: AnimationPlayer = null;
constructor() { scheduleMicroTask(() => this._onFinish()); } constructor() { scheduleMicroTask(() => this._onFinish()); }
/** @internal */ /** @internal */
@ -39,7 +42,9 @@ export class NoOpAnimationPlayer implements AnimationPlayer {
this._subscriptions = []; this._subscriptions = [];
} }
onDone(fn: Function): void { this._subscriptions.push(fn); } onDone(fn: Function): void { this._subscriptions.push(fn); }
play(): void {} hasStarted(): boolean { return this._started; }
init(): void {}
play(): void { this._started = true; }
pause(): void {} pause(): void {}
restart(): void {} restart(): void {}
finish(): void { this._onFinish(); } finish(): void { this._onFinish(); }

View File

@ -15,6 +15,7 @@ export class AnimationSequencePlayer implements AnimationPlayer {
private _activePlayer: AnimationPlayer; private _activePlayer: AnimationPlayer;
private _subscriptions: Function[] = []; private _subscriptions: Function[] = [];
private _finished = false; private _finished = false;
private _started: boolean = false;
public parentPlayer: AnimationPlayer = null; 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); } 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(); } pause(): void { this._activePlayer.pause(); }

View File

@ -11,7 +11,7 @@ import {isPresent} from '../facade/lang';
import {AnimationPlayer} from './animation_player'; import {AnimationPlayer} from './animation_player';
export class ActiveAnimationPlayersMap { export class ViewAnimationMap {
private _map = new Map<any, {[key: string]: AnimationPlayer}>(); private _map = new Map<any, {[key: string]: AnimationPlayer}>();
private _allPlayers: AnimationPlayer[] = []; private _allPlayers: AnimationPlayer[] = [];
@ -25,9 +25,9 @@ export class ActiveAnimationPlayersMap {
} }
findAllPlayersByElement(element: any): AnimationPlayer[] { findAllPlayersByElement(element: any): AnimationPlayer[] {
var players: any[] /** TODO #9100 */ = []; var players: AnimationPlayer[] = [];
StringMapWrapper.forEach( StringMapWrapper.forEach(
this._map.get(element), (player: any /** TODO #9100 */) => players.push(player)); this._map.get(element), (player: AnimationPlayer) => players.push(player));
return players; return players;
} }

View File

@ -28,7 +28,7 @@ import {AnimationPlayer} from '../animation/animation_player';
import {AnimationGroupPlayer} from '../animation/animation_group_player'; import {AnimationGroupPlayer} from '../animation/animation_group_player';
import {AnimationKeyframe} from '../animation/animation_keyframe'; import {AnimationKeyframe} from '../animation/animation_keyframe';
import {AnimationStyles} from '../animation/animation_styles'; 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)`); var _scope_check: WtfScopeFn = wtfCreateScope(`AppView#check(ascii id)`);
@ -54,7 +54,7 @@ export abstract class AppView<T> {
private _hasExternalHostElement: boolean; private _hasExternalHostElement: boolean;
public activeAnimationPlayers = new ActiveAnimationPlayersMap(); public animationPlayers = new ViewAnimationMap();
public context: T; public context: T;
@ -74,21 +74,27 @@ export abstract class AppView<T> {
cancelActiveAnimation(element: any, animationName: string, removeAllAnimations: boolean = false) { cancelActiveAnimation(element: any, animationName: string, removeAllAnimations: boolean = false) {
if (removeAllAnimations) { if (removeAllAnimations) {
this.activeAnimationPlayers.findAllPlayersByElement(element).forEach( this.animationPlayers.findAllPlayersByElement(element).forEach(player => player.destroy());
player => player.destroy());
} else { } else {
var player = this.activeAnimationPlayers.find(element, animationName); var player = this.animationPlayers.find(element, animationName);
if (isPresent(player)) { if (isPresent(player)) {
player.destroy(); player.destroy();
} }
} }
} }
registerAndStartAnimation(element: any, animationName: string, player: AnimationPlayer): void { queueAnimation(element: any, animationName: string, player: AnimationPlayer): void {
this.activeAnimationPlayers.set(element, animationName, player); this.animationPlayers.set(element, animationName, player);
player.onDone(() => { this.activeAnimationPlayers.remove(element, animationName); }); player.onDone(() => { this.animationPlayers.remove(element, animationName); });
}
triggerQueuedAnimations() {
this.animationPlayers.getAllPlayers().forEach(player => {
if (!player.hasStarted()) {
player.play(); player.play();
} }
});
}
create(context: T, givenProjectableNodes: Array<any|any[]>, rootSelectorOrNode: string|any): create(context: T, givenProjectableNodes: Array<any|any[]>, rootSelectorOrNode: string|any):
AppElement { AppElement {
@ -201,10 +207,10 @@ export abstract class AppView<T> {
this.destroyInternal(); this.destroyInternal();
this.dirtyParentQueriesInternal(); this.dirtyParentQueriesInternal();
if (this.activeAnimationPlayers.length == 0) { if (this.animationPlayers.length == 0) {
this.renderer.destroyView(hostElement, this.allNodes); this.renderer.destroyView(hostElement, this.allNodes);
} else { } else {
var player = new AnimationGroupPlayer(this.activeAnimationPlayers.getAllPlayers()); var player = new AnimationGroupPlayer(this.animationPlayers.getAllPlayers());
player.onDone(() => { this.renderer.destroyView(hostElement, this.allNodes); }); player.onDone(() => { this.renderer.destroyView(hostElement, this.allNodes); });
} }
} }
@ -221,10 +227,10 @@ export abstract class AppView<T> {
detach(): void { detach(): void {
this.detachInternal(); this.detachInternal();
if (this.activeAnimationPlayers.length == 0) { if (this.animationPlayers.length == 0) {
this.renderer.detachView(this.flatRootNodes); this.renderer.detachView(this.flatRootNodes);
} else { } else {
var player = new AnimationGroupPlayer(this.activeAnimationPlayers.getAllPlayers()); var player = new AnimationGroupPlayer(this.animationPlayers.getAllPlayers());
player.onDone(() => { this.renderer.detachView(this.flatRootNodes); }); player.onDone(() => { this.renderer.detachView(this.flatRootNodes); });
} }
} }

View File

@ -9,10 +9,10 @@
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {el} from '@angular/platform-browser/testing/browser_util'; 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 {isPresent} from '../../src/facade/lang';
import {fakeAsync, flushMicrotasks} from '../../testing'; 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'; import {AsyncTestCompleter, beforeEach, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '../../testing/testing_internal';
export function main() { export function main() {
@ -22,7 +22,7 @@ export function main() {
var animationName = 'animationName'; var animationName = 'animationName';
beforeEach(() => { beforeEach(() => {
playersMap = new ActiveAnimationPlayersMap(); playersMap = new ViewAnimationMap();
elementNode = el('<div></div>'); elementNode = el('<div></div>');
}); });

View File

@ -6,10 +6,10 @@
* found in the LICENSE file at https://angular.io/license * 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 {AnimationGroupPlayer} from '../../src/animation/animation_group_player';
import {isPresent} from '../../src/facade/lang'; import {isPresent} from '../../src/facade/lang';
import {fakeAsync, flushMicrotasks} from '../../testing'; 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'; import {AsyncTestCompleter, beforeEach, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '../../testing/testing_internal';
export function main() { export function main() {

View File

@ -12,9 +12,13 @@ import {TestComponentBuilder} from '@angular/compiler/testing';
import {AnimationDriver} from '@angular/platform-browser/src/dom/animation_driver'; import {AnimationDriver} from '@angular/platform-browser/src/dom/animation_driver';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {MockAnimationDriver} from '@angular/platform-browser/testing/mock_animation_driver'; 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 {Component} from '../../index';
import {DEFAULT_STATE} from '../../src/animation/animation_constants'; 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 {AnimationEntryMetadata, animate, group, keyframes, sequence, state, style, transition, trigger} from '../../src/animation/metadata';
import {AUTO_STYLE} from '../../src/animation/metadata'; import {AUTO_STYLE} from '../../src/animation/metadata';
import {IS_DART, isArray, isPresent} from '../../src/facade/lang'; import {IS_DART, isArray, isPresent} from '../../src/facade/lang';
@ -26,7 +30,6 @@ export function main() {
declareTests({useJit: false}); declareTests({useJit: false});
} else { } else {
describe('jit', () => { declareTests({useJit: true}); }); describe('jit', () => { declareTests({useJit: true}); });
describe('no jit', () => { declareTests({useJit: false}); }); 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', () => { describe('animation states', () => {
it('should retain the destination animation state styles once the animation is complete', it('should retain the destination animation state styles once the animation is complete',
inject( inject(
@ -1049,3 +1178,26 @@ class DummyIfCmp {
exp = false; exp = false;
exp2 = 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;
}
}

View File

@ -6,10 +6,10 @@
* found in the LICENSE file at https://angular.io/license * 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 {AnimationSequencePlayer} from '../../src/animation/animation_sequence_player';
import {isPresent} from '../../src/facade/lang'; import {isPresent} from '../../src/facade/lang';
import {fakeAsync, flushMicrotasks} from '../../testing'; 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'; import {AsyncTestCompleter, beforeEach, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '../../testing/testing_internal';
export function main() { export function main() {

View File

@ -13,7 +13,7 @@ import {Math, global, isFunction, isPromise} from '../src/facade/lang';
import {AsyncTestCompleter} from './async_test_completer'; import {AsyncTestCompleter} from './async_test_completer';
import {getTestInjector, inject} from './test_injector'; 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 {AsyncTestCompleter} from './async_test_completer';
export {inject} from './test_injector'; export {inject} from './test_injector';
export {expect} from './testing'; export {expect} from './testing';

View File

@ -11,9 +11,7 @@ import {AUTO_STYLE, BaseException} from '@angular/core';
import {AnimationKeyframe, AnimationPlayer, AnimationStyles, NoOpAnimationPlayer} from '../../core_private'; import {AnimationKeyframe, AnimationPlayer, AnimationStyles, NoOpAnimationPlayer} from '../../core_private';
import {StringMapWrapper} from '../facade/collection'; import {StringMapWrapper} from '../facade/collection';
import {StringWrapper, isNumber, isPresent} from '../facade/lang'; import {StringWrapper, isNumber, isPresent} from '../facade/lang';
import {AnimationDriver} from './animation_driver'; import {AnimationDriver} from './animation_driver';
import {getDOM} from './dom_adapter';
import {DomAnimatePlayer} from './dom_animate_player'; import {DomAnimatePlayer} from './dom_animate_player';
import {dashCaseToCamelCase} from './util'; import {dashCaseToCamelCase} from './util';
import {WebAnimationsPlayer} from './web_animations_player'; import {WebAnimationsPlayer} from './web_animations_player';
@ -21,19 +19,17 @@ import {WebAnimationsPlayer} from './web_animations_player';
export class WebAnimationsDriver implements AnimationDriver { export class WebAnimationsDriver implements AnimationDriver {
animate( animate(
element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[],
duration: number, delay: number, easing: string): AnimationPlayer { duration: number, delay: number, easing: string): WebAnimationsPlayer {
var anyElm = <any>element;
var formattedSteps: {[key: string]: string | number}[] = []; var formattedSteps: {[key: string]: string | number}[] = [];
var startingStyleLookup: {[key: string]: string | number} = {}; var startingStyleLookup: {[key: string]: string | number} = {};
if (isPresent(startingStyles) && startingStyles.styles.length > 0) { if (isPresent(startingStyles) && startingStyles.styles.length > 0) {
startingStyleLookup = _populateStyles(anyElm, startingStyles, {}); startingStyleLookup = _populateStyles(element, startingStyles, {});
startingStyleLookup['offset'] = 0; startingStyleLookup['offset'] = 0;
formattedSteps.push(startingStyleLookup); formattedSteps.push(startingStyleLookup);
} }
keyframes.forEach((keyframe: AnimationKeyframe) => { keyframes.forEach((keyframe: AnimationKeyframe) => {
let data = _populateStyles(anyElm, keyframe.styles, startingStyleLookup); let data = _populateStyles(element, keyframe.styles, startingStyleLookup);
data['offset'] = keyframe.offset; data['offset'] = keyframe.offset;
formattedSteps.push(data); formattedSteps.push(data);
}); });
@ -60,14 +56,7 @@ export class WebAnimationsDriver implements AnimationDriver {
playerOptions['easing'] = easing; playerOptions['easing'] = easing;
} }
var player = this._triggerWebAnimation(anyElm, formattedSteps, playerOptions); return new WebAnimationsPlayer(element, formattedSteps, playerOptions);
return new WebAnimationsPlayer(player, duration);
}
/** @internal */
_triggerWebAnimation(elm: any, keyframes: any[], options: any): DomAnimatePlayer {
return elm.animate(keyframes, options);
} }
} }
@ -78,9 +67,8 @@ function _populateStyles(
styles.styles.forEach((entry) => { styles.styles.forEach((entry) => {
StringMapWrapper.forEach(entry, (val: any, prop: string) => { StringMapWrapper.forEach(entry, (val: any, prop: string) => {
var formattedProp = dashCaseToCamelCase(prop); var formattedProp = dashCaseToCamelCase(prop);
data[formattedProp] = val == AUTO_STYLE ? data[formattedProp] =
_computeStyle(element, formattedProp) : val == AUTO_STYLE ? val : val.toString() + _resolveStyleUnit(val, prop, formattedProp);
val.toString() + _resolveStyleUnit(val, prop, formattedProp);
}); });
}); });
StringMapWrapper.forEach(defaultStyles, (value: string, prop: string) => { StringMapWrapper.forEach(defaultStyles, (value: string, prop: string) => {
@ -154,7 +142,3 @@ function _isPixelDimensionStyle(prop: string): boolean {
return false; return false;
} }
} }
function _computeStyle(element: any, prop: string): string {
return getDOM().getComputedStyle(element)[prop];
}

View File

@ -6,20 +6,29 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {AUTO_STYLE} from '@angular/core';
import {AnimationPlayer} from '../../core_private'; import {AnimationPlayer} from '../../core_private';
import {StringMapWrapper} from '../facade/collection';
import {isPresent} from '../facade/lang'; import {isPresent} from '../facade/lang';
import {getDOM} from './dom_adapter';
import {DomAnimatePlayer} from './dom_animate_player'; import {DomAnimatePlayer} from './dom_animate_player';
export class WebAnimationsPlayer implements AnimationPlayer { export class WebAnimationsPlayer implements AnimationPlayer {
private _subscriptions: Function[] = []; private _subscriptions: Function[] = [];
private _finished = false; private _finished = false;
private _initialized = false;
private _player: DomAnimatePlayer;
private _started: boolean = false;
private _duration: number;
public parentPlayer: AnimationPlayer = null; public parentPlayer: AnimationPlayer = null;
constructor(private _player: DomAnimatePlayer, public totalTime: number) { constructor(
// this is required to make the player startable at a later time public element: any, public keyframes: {[key: string]: string | number}[],
this.reset(); public options: {[key: string]: string | number}) {
this._player.onfinish = () => this._onFinish(); this._duration = <number>options['duration'];
} }
private _onFinish() { private _onFinish() {
@ -33,13 +42,44 @@ export class WebAnimationsPlayer implements AnimationPlayer {
} }
} }
init(): void {
if (this._initialized) return;
this._initialized = true;
var keyframes = this.keyframes.map(styles => {
var formattedKeyframe: {[key: string]: string | number} = {};
StringMapWrapper.forEach(styles, (value: string | number, prop: string) => {
formattedKeyframe[prop] = value == AUTO_STYLE ? _computeStyle(this.element, prop) : value;
});
return formattedKeyframe;
});
this._player = this._triggerWebAnimation(this.element, keyframes, this.options);
// this is required so that the player doesn't start to animate right away
this.reset();
this._player.onfinish = () => this._onFinish();
}
/** @internal */
_triggerWebAnimation(element: any, keyframes: any[], options: any): DomAnimatePlayer {
return <DomAnimatePlayer>element.animate(keyframes, options);
}
onDone(fn: Function): void { this._subscriptions.push(fn); } onDone(fn: Function): void { this._subscriptions.push(fn); }
play(): void { this._player.play(); } play(): void {
this.init();
this._player.play();
}
pause(): void { this._player.pause(); } pause(): void {
this.init();
this._player.pause();
}
finish(): void { finish(): void {
this.init();
this._onFinish(); this._onFinish();
this._player.finish(); this._player.finish();
} }
@ -51,12 +91,20 @@ export class WebAnimationsPlayer implements AnimationPlayer {
this.play(); this.play();
} }
hasStarted(): boolean { return this._started; }
destroy(): void { destroy(): void {
this.reset(); this.reset();
this._onFinish(); this._onFinish();
} }
setPosition(p: any /** TODO #9100 */): void { this._player.currentTime = p * this.totalTime; } get totalTime(): number { return this._duration; }
setPosition(p: number): void { this._player.currentTime = p * this.totalTime; }
getPosition(): number { return this._player.currentTime / this.totalTime; } getPosition(): number { return this._player.currentTime / this.totalTime; }
} }
function _computeStyle(element: any, prop: string): string {
return getDOM().getComputedStyle(element)[prop];
}

View File

@ -12,6 +12,7 @@ import {el} from '@angular/platform-browser/testing/browser_util';
import {AnimationKeyframe, AnimationStyles} from '../../core_private'; import {AnimationKeyframe, AnimationStyles} from '../../core_private';
import {DomAnimatePlayer} from '../../src/dom/dom_animate_player'; import {DomAnimatePlayer} from '../../src/dom/dom_animate_player';
import {WebAnimationsDriver} from '../../src/dom/web_animations_driver'; import {WebAnimationsDriver} from '../../src/dom/web_animations_driver';
import {WebAnimationsPlayer} from '../../src/dom/web_animations_player';
import {StringMapWrapper} from '../../src/facade/collection'; import {StringMapWrapper} from '../../src/facade/collection';
import {MockDomAnimatePlayer} from '../../testing/mock_dom_animate_player'; import {MockDomAnimatePlayer} from '../../testing/mock_dom_animate_player';
@ -51,8 +52,8 @@ export function main() {
_makeKeyframe(1, {'font-size': '555px'}) _makeKeyframe(1, {'font-size': '555px'})
]; ];
driver.animate(elm, startingStyles, styles, 0, 0, 'linear'); var player = driver.animate(elm, startingStyles, styles, 0, 0, 'linear');
var details = driver.log.pop(); var details = _formatOptions(player);
var startKeyframe = details['keyframes'][0]; var startKeyframe = details['keyframes'][0];
var firstKeyframe = details['keyframes'][1]; var firstKeyframe = details['keyframes'][1];
var lastKeyframe = details['keyframes'][2]; var lastKeyframe = details['keyframes'][2];
@ -71,8 +72,8 @@ export function main() {
var startingStyles = _makeStyles({'borderTopWidth': 40}); var startingStyles = _makeStyles({'borderTopWidth': 40});
var styles = [_makeKeyframe(0, {'font-size': 100}), _makeKeyframe(1, {'height': '555em'})]; var styles = [_makeKeyframe(0, {'font-size': 100}), _makeKeyframe(1, {'height': '555em'})];
driver.animate(elm, startingStyles, styles, 0, 0, 'linear'); var player = driver.animate(elm, startingStyles, styles, 0, 0, 'linear');
var details = driver.log.pop(); var details = _formatOptions(player);
var startKeyframe = details['keyframes'][0]; var startKeyframe = details['keyframes'][0];
var firstKeyframe = details['keyframes'][1]; var firstKeyframe = details['keyframes'][1];
var lastKeyframe = details['keyframes'][2]; var lastKeyframe = details['keyframes'][2];
@ -88,8 +89,8 @@ export function main() {
var startingStyles = _makeStyles({}); var startingStyles = _makeStyles({});
var styles = [_makeKeyframe(0, {'color': 'green'}), _makeKeyframe(1, {'color': 'red'})]; var styles = [_makeKeyframe(0, {'color': 'green'}), _makeKeyframe(1, {'color': 'red'})];
driver.animate(elm, startingStyles, styles, 1000, 1000, 'linear'); var player = driver.animate(elm, startingStyles, styles, 1000, 1000, 'linear');
var details = driver.log.pop(); var details = _formatOptions(player);
var options = details['options']; var options = details['options'];
expect(options['fill']).toEqual('both'); expect(options['fill']).toEqual('both');
}); });
@ -98,8 +99,8 @@ export function main() {
var startingStyles = _makeStyles({}); var startingStyles = _makeStyles({});
var styles = [_makeKeyframe(0, {'color': 'green'}), _makeKeyframe(1, {'color': 'red'})]; var styles = [_makeKeyframe(0, {'color': 'green'}), _makeKeyframe(1, {'color': 'red'})];
driver.animate(elm, startingStyles, styles, 1000, 1000, 'ease-out'); var player = driver.animate(elm, startingStyles, styles, 1000, 1000, 'ease-out');
var details = driver.log.pop(); var details = _formatOptions(player);
var options = details['options']; var options = details['options'];
expect(options['easing']).toEqual('ease-out'); expect(options['easing']).toEqual('ease-out');
}); });
@ -108,11 +109,15 @@ export function main() {
var startingStyles = _makeStyles({}); var startingStyles = _makeStyles({});
var styles = [_makeKeyframe(0, {'color': 'green'}), _makeKeyframe(1, {'color': 'red'})]; var styles = [_makeKeyframe(0, {'color': 'green'}), _makeKeyframe(1, {'color': 'red'})];
driver.animate(elm, startingStyles, styles, 1000, 1000, null); var player = driver.animate(elm, startingStyles, styles, 1000, 1000, null);
var details = driver.log.pop(); var details = _formatOptions(player);
var options = details['options']; var options = details['options'];
var keys = StringMapWrapper.keys(options); var keys = StringMapWrapper.keys(options);
expect(keys.indexOf('easing')).toEqual(-1); expect(keys.indexOf('easing')).toEqual(-1);
}); });
}); });
} }
function _formatOptions(player: WebAnimationsPlayer): {[key: string]: any} {
return {'element': player.element, 'keyframes': player.keyframes, 'options': player.options};
}

View File

@ -7,16 +7,32 @@
*/ */
import {AsyncTestCompleter, MockAnimationPlayer, beforeEach, beforeEachProviders, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '@angular/core/testing/testing_internal'; import {AsyncTestCompleter, MockAnimationPlayer, beforeEach, beforeEachProviders, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '@angular/core/testing/testing_internal';
import {el} from '@angular/platform-browser/testing/browser_util';
import {DomAnimatePlayer} from '../../src/dom/dom_animate_player';
import {WebAnimationsPlayer} from '../../src/dom/web_animations_player'; import {WebAnimationsPlayer} from '../../src/dom/web_animations_player';
import {MockDomAnimatePlayer} from '../../testing/mock_dom_animate_player'; import {MockDomAnimatePlayer} from '../../testing/mock_dom_animate_player';
class ExtendedWebAnimationsPlayer extends WebAnimationsPlayer {
public domPlayer = new MockDomAnimatePlayer();
constructor(
public element: HTMLElement, public keyframes: {[key: string]: string | number}[],
public options: {[key: string]: string | number}) {
super(element, keyframes, options);
}
_triggerWebAnimation(elm: any, keyframes: any[], options: any): DomAnimatePlayer {
return this.domPlayer;
}
}
export function main() { export function main() {
function makePlayer(): {[key: string]: any} { function makePlayer(): {[key: string]: any} {
var mockPlayer = new MockDomAnimatePlayer(); var someElm = el('<div></div>');
var c = mockPlayer.captures; var player = new ExtendedWebAnimationsPlayer(someElm, [], {});
var p = new WebAnimationsPlayer(mockPlayer, 0); player.init();
return {'captures': c, 'player': p}; return {'captures': player.domPlayer.captures, 'player': player};
} }
describe('WebAnimationsPlayer', () => { describe('WebAnimationsPlayer', () => {

View File

@ -14,7 +14,7 @@ import {AnimationDriver} from '../src/dom/animation_driver';
import {StringMapWrapper} from '../src/facade/collection'; import {StringMapWrapper} from '../src/facade/collection';
export class MockAnimationDriver extends AnimationDriver { export class MockAnimationDriver extends AnimationDriver {
log: any[] /** TODO #9100 */ = []; public log: {[key: string]: any}[] = [];
animate( animate(
element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[],
duration: number, delay: number, easing: string): AnimationPlayer { duration: number, delay: number, easing: string): AnimationPlayer {
@ -38,11 +38,9 @@ function _serializeKeyframes(keyframes: AnimationKeyframe[]): any[] {
} }
function _serializeStyles(styles: AnimationStyles): {[key: string]: any} { function _serializeStyles(styles: AnimationStyles): {[key: string]: any} {
var flatStyles = {}; var flatStyles: {[key: string]: any} = {};
styles.styles.forEach( styles.styles.forEach(entry => StringMapWrapper.forEach(entry, (val: any, prop: string) => {
entry => StringMapWrapper.forEach( flatStyles[prop] = val;
entry, (val: any /** TODO #9100 */, prop: any /** TODO #9100 */) => {
(flatStyles as any /** TODO #9100 */)[prop] = val;
})); }));
return flatStyles; return flatStyles;
} }

View File

@ -6,13 +6,15 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {AnimationPlayer} from '../../src/animation/animation_player'; import {AnimationPlayer} from '../../core/src/animation/animation_player';
import {isPresent} from '../../src/facade/lang'; import {isPresent} from '../../core/src/facade/lang';
export class MockAnimationPlayer implements AnimationPlayer { export class MockAnimationPlayer implements AnimationPlayer {
private _subscriptions: any[] /** TODO #9100 */ = []; private _subscriptions: any[] /** TODO #9100 */ = [];
private _finished = false; private _finished = false;
private _destroyed = false; private _destroyed = false;
private _started: boolean = false;
public parentPlayer: AnimationPlayer = null; public parentPlayer: AnimationPlayer = null;
public log: any[] /** TODO #9100 */ = []; public log: any[] /** TODO #9100 */ = [];
@ -30,9 +32,16 @@ export class MockAnimationPlayer implements AnimationPlayer {
} }
} }
init(): void { this.log.push('init'); }
onDone(fn: Function): void { this._subscriptions.push(fn); } onDone(fn: Function): void { this._subscriptions.push(fn); }
play(): void { this.log.push('play'); } hasStarted() { return this._started; }
play(): void {
this._started = true;
this.log.push('play');
}
pause(): void { this.log.push('pause'); } pause(): void { this.log.push('pause'); }

View File

@ -32,6 +32,9 @@ import {
<hr /> <hr />
<div *ngFor="let item of items" class="box" [@boxAnimation]="state"> <div *ngFor="let item of items" class="box" [@boxAnimation]="state">
{{ item }} {{ item }}
<div *ngIf="true">
something inside
</div>
</div> </div>
</div> </div>
`, `,

View File

@ -67,6 +67,8 @@ export declare abstract class AnimationPlayer {
abstract destroy(): void; abstract destroy(): void;
abstract finish(): void; abstract finish(): void;
abstract getPosition(): number; abstract getPosition(): number;
abstract hasStarted(): boolean;
abstract init(): void;
abstract onDone(fn: Function): void; abstract onDone(fn: Function): void;
abstract pause(): void; abstract pause(): void;
abstract play(): void; abstract play(): void;