From c3bdd504d0ea1166d26df359c7bbd9d28df70c99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Fri, 1 Jul 2016 16:01:57 -0700 Subject: [PATCH] fix(animations): ensure all child elements are rendered before running animations Closes #9402 Closes #9775 Closes #9887 --- .../src/animation/animation_compiler.ts | 2 +- .../src/view_compiler/property_binder.ts | 9 + .../src/animation/animation_group_player.ts | 14 +- .../core/src/animation/animation_player.ts | 7 +- .../animation/animation_sequence_player.ts | 13 +- ...n_players_map.ts => view_animation_map.ts} | 6 +- modules/@angular/core/src/linker/view.ts | 32 ++-- .../active_animations_players_map_spec.ts | 6 +- .../animation/animation_group_player_spec.ts | 2 +- .../animation/animation_integration_spec.ts | 154 +++++++++++++++++- .../animation_sequence_player_spec.ts | 2 +- .../@angular/core/testing/testing_internal.ts | 2 +- .../src/dom/web_animations_driver.ts | 28 +--- .../src/dom/web_animations_player.ts | 62 ++++++- .../test/dom/web_animations_driver_spec.ts | 25 +-- .../test/dom/web_animations_player_spec.ts | 24 ++- .../testing/mock_animation_driver.ts | 12 +- .../testing}/mock_animation_player.ts | 15 +- .../playground/src/animate/app/animate-app.ts | 3 + tools/public_api_guard/core/index.d.ts | 2 + 20 files changed, 340 insertions(+), 80 deletions(-) rename modules/@angular/core/src/animation/{active_animation_players_map.ts => view_animation_map.ts} (91%) rename modules/@angular/{core/testing/animation => platform-browser/testing}/mock_animation_player.ts (79%) diff --git a/modules/@angular/compiler/src/animation/animation_compiler.ts b/modules/@angular/compiler/src/animation/animation_compiler.ts index d2b7d6e78f..5c63830fb8 100644 --- a/modules/@angular/compiler/src/animation/animation_compiler.ts +++ b/modules/@angular/compiler/src/animation/animation_compiler.ts @@ -271,7 +271,7 @@ class _AnimationBuilder implements AnimationAstVisitor { statements.push(_ANIMATION_FACTORY_VIEW_VAR .callMethod( - 'registerAndStartAnimation', + 'queueAnimation', [ _ANIMATION_FACTORY_ELEMENT_VAR, o.literal(this.animationName), _ANIMATION_PLAYER_VAR diff --git a/modules/@angular/compiler/src/view_compiler/property_binder.ts b/modules/@angular/compiler/src/view_compiler/property_binder.ts index 6ef701ed8d..733cd96a98 100644 --- a/modules/@angular/compiler/src/view_compiler/property_binder.ts +++ b/modules/@angular/compiler/src/view_compiler/property_binder.ts @@ -36,6 +36,8 @@ function createCurrValueExpr(exprIndex: number): o.ReadVarExpr { return o.variable(`currVal_${exprIndex}`); // fix syntax highlighting: ` } +const _animationViewCheckedFlagMap = new Map(); + function bind( view: CompileView, currValExpr: o.ReadVarExpr, fieldExpr: o.ReadPropExpr, parsedExpression: cdAst.AST, context: o.Expression, actions: o.Statement[], @@ -171,6 +173,13 @@ function bindAndWriteToRenderer( animation.fnVariable.callFn([o.THIS_EXPR, renderNode, oldRenderValue, emptyStateValue]) .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; } diff --git a/modules/@angular/core/src/animation/animation_group_player.ts b/modules/@angular/core/src/animation/animation_group_player.ts index 7ff5ece83a..8782f2d0ba 100644 --- a/modules/@angular/core/src/animation/animation_group_player.ts +++ b/modules/@angular/core/src/animation/animation_group_player.ts @@ -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()); } diff --git a/modules/@angular/core/src/animation/animation_player.ts b/modules/@angular/core/src/animation/animation_player.ts index 9aad73a0e2..1aab6b3143 100644 --- a/modules/@angular/core/src/animation/animation_player.ts +++ b/modules/@angular/core/src/animation/animation_player.ts @@ -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(); } diff --git a/modules/@angular/core/src/animation/animation_sequence_player.ts b/modules/@angular/core/src/animation/animation_sequence_player.ts index 4738441f72..d8e9415e57 100644 --- a/modules/@angular/core/src/animation/animation_sequence_player.ts +++ b/modules/@angular/core/src/animation/animation_sequence_player.ts @@ -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(); } diff --git a/modules/@angular/core/src/animation/active_animation_players_map.ts b/modules/@angular/core/src/animation/view_animation_map.ts similarity index 91% rename from modules/@angular/core/src/animation/active_animation_players_map.ts rename to modules/@angular/core/src/animation/view_animation_map.ts index d8c25c5ae4..2065143dcd 100644 --- a/modules/@angular/core/src/animation/active_animation_players_map.ts +++ b/modules/@angular/core/src/animation/view_animation_map.ts @@ -11,7 +11,7 @@ import {isPresent} from '../facade/lang'; import {AnimationPlayer} from './animation_player'; -export class ActiveAnimationPlayersMap { +export class ViewAnimationMap { private _map = new Map(); 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; } diff --git a/modules/@angular/core/src/linker/view.ts b/modules/@angular/core/src/linker/view.ts index f68fc6f071..d7ca8f0f76 100644 --- a/modules/@angular/core/src/linker/view.ts +++ b/modules/@angular/core/src/linker/view.ts @@ -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 { private _hasExternalHostElement: boolean; - public activeAnimationPlayers = new ActiveAnimationPlayersMap(); + public animationPlayers = new ViewAnimationMap(); public context: T; @@ -74,20 +74,26 @@ export abstract class AppView { 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, rootSelectorOrNode: string|any): @@ -201,10 +207,10 @@ export abstract class AppView { 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 { 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); }); } } diff --git a/modules/@angular/core/test/animation/active_animations_players_map_spec.ts b/modules/@angular/core/test/animation/active_animations_players_map_spec.ts index 2a7409c3ed..df8b44f850 100644 --- a/modules/@angular/core/test/animation/active_animations_players_map_spec.ts +++ b/modules/@angular/core/test/animation/active_animations_players_map_spec.ts @@ -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('
'); }); diff --git a/modules/@angular/core/test/animation/animation_group_player_spec.ts b/modules/@angular/core/test/animation/animation_group_player_spec.ts index b1d9f60983..99b962a10d 100644 --- a/modules/@angular/core/test/animation/animation_group_player_spec.ts +++ b/modules/@angular/core/test/animation/animation_group_player_spec.ts @@ -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() { diff --git a/modules/@angular/core/test/animation/animation_integration_spec.ts b/modules/@angular/core/test/animation/animation_integration_spec.ts index e165fb8f8e..18dd4467d6 100644 --- a/modules/@angular/core/test/animation/animation_integration_spec.ts +++ b/modules/@angular/core/test/animation/animation_integration_spec.ts @@ -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, `
+
inner child guy
+
`, + [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 = 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, `
+
+
inner child guy
+
`, + [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 = 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, `
+
`, + [ + 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 = animation1['player']; + var player2 = 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, `
`, + [ + 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 = 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; + } +} diff --git a/modules/@angular/core/test/animation/animation_sequence_player_spec.ts b/modules/@angular/core/test/animation/animation_sequence_player_spec.ts index 53ebce8be5..589858bfa9 100644 --- a/modules/@angular/core/test/animation/animation_sequence_player_spec.ts +++ b/modules/@angular/core/test/animation/animation_sequence_player_spec.ts @@ -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() { diff --git a/modules/@angular/core/testing/testing_internal.ts b/modules/@angular/core/testing/testing_internal.ts index 57dbb5e9ea..42343e8f75 100644 --- a/modules/@angular/core/testing/testing_internal.ts +++ b/modules/@angular/core/testing/testing_internal.ts @@ -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'; diff --git a/modules/@angular/platform-browser/src/dom/web_animations_driver.ts b/modules/@angular/platform-browser/src/dom/web_animations_driver.ts index d501bf389d..22cdbce444 100644 --- a/modules/@angular/platform-browser/src/dom/web_animations_driver.ts +++ b/modules/@angular/platform-browser/src/dom/web_animations_driver.ts @@ -11,9 +11,7 @@ import {AUTO_STYLE, BaseException} from '@angular/core'; import {AnimationKeyframe, AnimationPlayer, AnimationStyles, NoOpAnimationPlayer} from '../../core_private'; import {StringMapWrapper} from '../facade/collection'; import {StringWrapper, isNumber, isPresent} from '../facade/lang'; - import {AnimationDriver} from './animation_driver'; -import {getDOM} from './dom_adapter'; import {DomAnimatePlayer} from './dom_animate_player'; import {dashCaseToCamelCase} from './util'; import {WebAnimationsPlayer} from './web_animations_player'; @@ -21,19 +19,17 @@ import {WebAnimationsPlayer} from './web_animations_player'; export class WebAnimationsDriver implements AnimationDriver { animate( element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], - duration: number, delay: number, easing: string): AnimationPlayer { - var anyElm = element; - + duration: number, delay: number, easing: string): WebAnimationsPlayer { var formattedSteps: {[key: string]: string | number}[] = []; var startingStyleLookup: {[key: string]: string | number} = {}; if (isPresent(startingStyles) && startingStyles.styles.length > 0) { - startingStyleLookup = _populateStyles(anyElm, startingStyles, {}); + startingStyleLookup = _populateStyles(element, startingStyles, {}); startingStyleLookup['offset'] = 0; formattedSteps.push(startingStyleLookup); } keyframes.forEach((keyframe: AnimationKeyframe) => { - let data = _populateStyles(anyElm, keyframe.styles, startingStyleLookup); + let data = _populateStyles(element, keyframe.styles, startingStyleLookup); data['offset'] = keyframe.offset; formattedSteps.push(data); }); @@ -60,14 +56,7 @@ export class WebAnimationsDriver implements AnimationDriver { playerOptions['easing'] = easing; } - var player = this._triggerWebAnimation(anyElm, formattedSteps, playerOptions); - - return new WebAnimationsPlayer(player, duration); - } - - /** @internal */ - _triggerWebAnimation(elm: any, keyframes: any[], options: any): DomAnimatePlayer { - return elm.animate(keyframes, options); + return new WebAnimationsPlayer(element, formattedSteps, playerOptions); } } @@ -78,9 +67,8 @@ function _populateStyles( styles.styles.forEach((entry) => { StringMapWrapper.forEach(entry, (val: any, prop: string) => { var formattedProp = dashCaseToCamelCase(prop); - data[formattedProp] = val == AUTO_STYLE ? - _computeStyle(element, formattedProp) : - val.toString() + _resolveStyleUnit(val, prop, formattedProp); + data[formattedProp] = + val == AUTO_STYLE ? val : val.toString() + _resolveStyleUnit(val, prop, formattedProp); }); }); StringMapWrapper.forEach(defaultStyles, (value: string, prop: string) => { @@ -154,7 +142,3 @@ function _isPixelDimensionStyle(prop: string): boolean { return false; } } - -function _computeStyle(element: any, prop: string): string { - return getDOM().getComputedStyle(element)[prop]; -} diff --git a/modules/@angular/platform-browser/src/dom/web_animations_player.ts b/modules/@angular/platform-browser/src/dom/web_animations_player.ts index 047d329f60..d58b1bd2dc 100644 --- a/modules/@angular/platform-browser/src/dom/web_animations_player.ts +++ b/modules/@angular/platform-browser/src/dom/web_animations_player.ts @@ -6,20 +6,29 @@ * found in the LICENSE file at https://angular.io/license */ +import {AUTO_STYLE} from '@angular/core'; + import {AnimationPlayer} from '../../core_private'; +import {StringMapWrapper} from '../facade/collection'; import {isPresent} from '../facade/lang'; +import {getDOM} from './dom_adapter'; import {DomAnimatePlayer} from './dom_animate_player'; export class WebAnimationsPlayer implements AnimationPlayer { private _subscriptions: Function[] = []; private _finished = false; + private _initialized = false; + private _player: DomAnimatePlayer; + private _started: boolean = false; + private _duration: number; + public parentPlayer: AnimationPlayer = null; - constructor(private _player: DomAnimatePlayer, public totalTime: number) { - // this is required to make the player startable at a later time - this.reset(); - this._player.onfinish = () => this._onFinish(); + constructor( + public element: any, public keyframes: {[key: string]: string | number}[], + public options: {[key: string]: string | number}) { + this._duration = options['duration']; } 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 element.animate(keyframes, options); + } + 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 { + this.init(); this._onFinish(); this._player.finish(); } @@ -51,12 +91,20 @@ export class WebAnimationsPlayer implements AnimationPlayer { this.play(); } + hasStarted(): boolean { return this._started; } + destroy(): void { this.reset(); 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; } } + +function _computeStyle(element: any, prop: string): string { + return getDOM().getComputedStyle(element)[prop]; +} diff --git a/modules/@angular/platform-browser/test/dom/web_animations_driver_spec.ts b/modules/@angular/platform-browser/test/dom/web_animations_driver_spec.ts index 9e96f75c1e..dc933e05c2 100644 --- a/modules/@angular/platform-browser/test/dom/web_animations_driver_spec.ts +++ b/modules/@angular/platform-browser/test/dom/web_animations_driver_spec.ts @@ -12,6 +12,7 @@ import {el} from '@angular/platform-browser/testing/browser_util'; import {AnimationKeyframe, AnimationStyles} from '../../core_private'; import {DomAnimatePlayer} from '../../src/dom/dom_animate_player'; import {WebAnimationsDriver} from '../../src/dom/web_animations_driver'; +import {WebAnimationsPlayer} from '../../src/dom/web_animations_player'; import {StringMapWrapper} from '../../src/facade/collection'; import {MockDomAnimatePlayer} from '../../testing/mock_dom_animate_player'; @@ -51,8 +52,8 @@ export function main() { _makeKeyframe(1, {'font-size': '555px'}) ]; - driver.animate(elm, startingStyles, styles, 0, 0, 'linear'); - var details = driver.log.pop(); + var player = driver.animate(elm, startingStyles, styles, 0, 0, 'linear'); + var details = _formatOptions(player); var startKeyframe = details['keyframes'][0]; var firstKeyframe = details['keyframes'][1]; var lastKeyframe = details['keyframes'][2]; @@ -71,8 +72,8 @@ export function main() { var startingStyles = _makeStyles({'borderTopWidth': 40}); var styles = [_makeKeyframe(0, {'font-size': 100}), _makeKeyframe(1, {'height': '555em'})]; - driver.animate(elm, startingStyles, styles, 0, 0, 'linear'); - var details = driver.log.pop(); + var player = driver.animate(elm, startingStyles, styles, 0, 0, 'linear'); + var details = _formatOptions(player); var startKeyframe = details['keyframes'][0]; var firstKeyframe = details['keyframes'][1]; var lastKeyframe = details['keyframes'][2]; @@ -88,8 +89,8 @@ export function main() { var startingStyles = _makeStyles({}); var styles = [_makeKeyframe(0, {'color': 'green'}), _makeKeyframe(1, {'color': 'red'})]; - driver.animate(elm, startingStyles, styles, 1000, 1000, 'linear'); - var details = driver.log.pop(); + var player = driver.animate(elm, startingStyles, styles, 1000, 1000, 'linear'); + var details = _formatOptions(player); var options = details['options']; expect(options['fill']).toEqual('both'); }); @@ -98,8 +99,8 @@ export function main() { var startingStyles = _makeStyles({}); var styles = [_makeKeyframe(0, {'color': 'green'}), _makeKeyframe(1, {'color': 'red'})]; - driver.animate(elm, startingStyles, styles, 1000, 1000, 'ease-out'); - var details = driver.log.pop(); + var player = driver.animate(elm, startingStyles, styles, 1000, 1000, 'ease-out'); + var details = _formatOptions(player); var options = details['options']; expect(options['easing']).toEqual('ease-out'); }); @@ -108,11 +109,15 @@ export function main() { var startingStyles = _makeStyles({}); var styles = [_makeKeyframe(0, {'color': 'green'}), _makeKeyframe(1, {'color': 'red'})]; - driver.animate(elm, startingStyles, styles, 1000, 1000, null); - var details = driver.log.pop(); + var player = driver.animate(elm, startingStyles, styles, 1000, 1000, null); + var details = _formatOptions(player); var options = details['options']; var keys = StringMapWrapper.keys(options); expect(keys.indexOf('easing')).toEqual(-1); }); }); } + +function _formatOptions(player: WebAnimationsPlayer): {[key: string]: any} { + return {'element': player.element, 'keyframes': player.keyframes, 'options': player.options}; +} diff --git a/modules/@angular/platform-browser/test/dom/web_animations_player_spec.ts b/modules/@angular/platform-browser/test/dom/web_animations_player_spec.ts index 8e1b56cc36..4d2a9cd458 100644 --- a/modules/@angular/platform-browser/test/dom/web_animations_player_spec.ts +++ b/modules/@angular/platform-browser/test/dom/web_animations_player_spec.ts @@ -7,16 +7,32 @@ */ 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 {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() { function makePlayer(): {[key: string]: any} { - var mockPlayer = new MockDomAnimatePlayer(); - var c = mockPlayer.captures; - var p = new WebAnimationsPlayer(mockPlayer, 0); - return {'captures': c, 'player': p}; + var someElm = el('
'); + var player = new ExtendedWebAnimationsPlayer(someElm, [], {}); + player.init(); + return {'captures': player.domPlayer.captures, 'player': player}; } describe('WebAnimationsPlayer', () => { diff --git a/modules/@angular/platform-browser/testing/mock_animation_driver.ts b/modules/@angular/platform-browser/testing/mock_animation_driver.ts index c3dd5c6cb3..7924288011 100644 --- a/modules/@angular/platform-browser/testing/mock_animation_driver.ts +++ b/modules/@angular/platform-browser/testing/mock_animation_driver.ts @@ -14,7 +14,7 @@ import {AnimationDriver} from '../src/dom/animation_driver'; import {StringMapWrapper} from '../src/facade/collection'; export class MockAnimationDriver extends AnimationDriver { - log: any[] /** TODO #9100 */ = []; + public log: {[key: string]: any}[] = []; animate( element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], duration: number, delay: number, easing: string): AnimationPlayer { @@ -38,11 +38,9 @@ function _serializeKeyframes(keyframes: AnimationKeyframe[]): any[] { } function _serializeStyles(styles: AnimationStyles): {[key: string]: any} { - var flatStyles = {}; - styles.styles.forEach( - entry => StringMapWrapper.forEach( - entry, (val: any /** TODO #9100 */, prop: any /** TODO #9100 */) => { - (flatStyles as any /** TODO #9100 */)[prop] = val; - })); + var flatStyles: {[key: string]: any} = {}; + styles.styles.forEach(entry => StringMapWrapper.forEach(entry, (val: any, prop: string) => { + flatStyles[prop] = val; + })); return flatStyles; } diff --git a/modules/@angular/core/testing/animation/mock_animation_player.ts b/modules/@angular/platform-browser/testing/mock_animation_player.ts similarity index 79% rename from modules/@angular/core/testing/animation/mock_animation_player.ts rename to modules/@angular/platform-browser/testing/mock_animation_player.ts index 674d0b986c..e82c2572ad 100644 --- a/modules/@angular/core/testing/animation/mock_animation_player.ts +++ b/modules/@angular/platform-browser/testing/mock_animation_player.ts @@ -6,13 +6,15 @@ * found in the LICENSE file at https://angular.io/license */ -import {AnimationPlayer} from '../../src/animation/animation_player'; -import {isPresent} from '../../src/facade/lang'; +import {AnimationPlayer} from '../../core/src/animation/animation_player'; +import {isPresent} from '../../core/src/facade/lang'; export class MockAnimationPlayer implements AnimationPlayer { private _subscriptions: any[] /** TODO #9100 */ = []; private _finished = false; private _destroyed = false; + private _started: boolean = false; + public parentPlayer: AnimationPlayer = null; 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); } - 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'); } diff --git a/modules/playground/src/animate/app/animate-app.ts b/modules/playground/src/animate/app/animate-app.ts index d77581a522..36391cf0de 100644 --- a/modules/playground/src/animate/app/animate-app.ts +++ b/modules/playground/src/animate/app/animate-app.ts @@ -32,6 +32,9 @@ import {
{{ item }} +
+ something inside +
`, diff --git a/tools/public_api_guard/core/index.d.ts b/tools/public_api_guard/core/index.d.ts index d83d740187..156dba1692 100644 --- a/tools/public_api_guard/core/index.d.ts +++ b/tools/public_api_guard/core/index.d.ts @@ -67,6 +67,8 @@ export declare abstract class AnimationPlayer { abstract destroy(): void; abstract finish(): void; abstract getPosition(): number; + abstract hasStarted(): boolean; + abstract init(): void; abstract onDone(fn: Function): void; abstract pause(): void; abstract play(): void;