diff --git a/modules/@angular/compiler/src/animation/animation_compiler.ts b/modules/@angular/compiler/src/animation/animation_compiler.ts index d7e6ec6cbc..1429310c81 100644 --- a/modules/@angular/compiler/src/animation/animation_compiler.ts +++ b/modules/@angular/compiler/src/animation/animation_compiler.ts @@ -87,6 +87,7 @@ var _ANIMATION_FACTORY_RENDERER_VAR = _ANIMATION_FACTORY_VIEW_VAR.prop('renderer var _ANIMATION_CURRENT_STATE_VAR = o.variable('currentState'); var _ANIMATION_NEXT_STATE_VAR = o.variable('nextState'); var _ANIMATION_PLAYER_VAR = o.variable('player'); +var _ANIMATION_TIME_VAR = o.variable('totalTime'); var _ANIMATION_START_STATE_STYLES_VAR = o.variable('startStateStyles'); var _ANIMATION_END_STATE_STYLES_VAR = o.variable('endStateStyles'); var _ANIMATION_COLLECTED_STYLES = o.variable('collectedStyles'); @@ -137,7 +138,8 @@ class _AnimationBuilder implements AnimationAstVisitor { var startingStylesExpr = ast.startingStyles.visit(this, context); var keyframeExpressions = ast.keyframes.map(keyframeEntry => keyframeEntry.visit(this, context)); - return this._callAnimateMethod(ast, startingStylesExpr, o.literalArr(keyframeExpressions)); + return this._callAnimateMethod( + ast, startingStylesExpr, o.literalArr(keyframeExpressions), context); } /** @internal */ @@ -149,11 +151,14 @@ class _AnimationBuilder implements AnimationAstVisitor { o.literalArr(keyframeExpressions) ]); - return this._callAnimateMethod(ast, startingStylesExpr, keyframesExpr); + return this._callAnimateMethod(ast, startingStylesExpr, keyframesExpr, context); } /** @internal */ - _callAnimateMethod(ast: AnimationStepAst, startingStylesExpr: any, keyframesExpr: any) { + _callAnimateMethod( + ast: AnimationStepAst, startingStylesExpr: any, keyframesExpr: any, + context: _AnimationBuilderContext) { + context.totalTransitionTime += ast.duration + ast.delay; return _ANIMATION_FACTORY_RENDERER_VAR.callMethod('animate', [ _ANIMATION_FACTORY_ELEMENT_VAR, startingStylesExpr, keyframesExpr, o.literal(ast.duration), o.literal(ast.delay), o.literal(ast.easing) @@ -189,6 +194,7 @@ class _AnimationBuilder implements AnimationAstVisitor { context.endStateAnimateStep = lastStep; } + context.totalTransitionTime = 0; context.isExpectingFirstStyleStep = true; var stateChangePreconditions: o.Expression[] = []; @@ -213,7 +219,10 @@ class _AnimationBuilder implements AnimationAstVisitor { var precondition = _ANIMATION_PLAYER_VAR.equals(o.NULL_EXPR).and(reducedStateChangesPrecondition); - return new o.IfStmt(precondition, [_ANIMATION_PLAYER_VAR.set(animationPlayerExpr).toStmt()]); + var animationStmt = _ANIMATION_PLAYER_VAR.set(animationPlayerExpr).toStmt(); + var totalTimeStmt = _ANIMATION_TIME_VAR.set(o.literal(context.totalTransitionTime)).toStmt(); + + return new o.IfStmt(precondition, [animationStmt, totalTimeStmt]); } visitAnimationEntry(ast: AnimationEntryAst, context: _AnimationBuilderContext): any { @@ -236,6 +245,7 @@ class _AnimationBuilder implements AnimationAstVisitor { statements.push(_ANIMATION_COLLECTED_STYLES.set(EMPTY_MAP).toDeclStmt()); statements.push(_ANIMATION_PLAYER_VAR.set(o.NULL_EXPR).toDeclStmt()); + statements.push(_ANIMATION_TIME_VAR.set(o.literal(0)).toDeclStmt()); statements.push( _ANIMATION_DEFAULT_STATE_VAR.set(this._statesMapVar.key(o.literal(DEFAULT_STATE))) @@ -297,15 +307,15 @@ class _AnimationBuilder implements AnimationAstVisitor { .toStmt()])]) .toStmt()); - statements.push( - _ANIMATION_FACTORY_VIEW_VAR - .callMethod( - 'queueAnimation', - [ - _ANIMATION_FACTORY_ELEMENT_VAR, o.literal(this.animationName), - _ANIMATION_PLAYER_VAR, _ANIMATION_CURRENT_STATE_VAR, _ANIMATION_NEXT_STATE_VAR - ]) - .toStmt()); + statements.push(_ANIMATION_FACTORY_VIEW_VAR + .callMethod( + 'queueAnimation', + [ + _ANIMATION_FACTORY_ELEMENT_VAR, o.literal(this.animationName), + _ANIMATION_PLAYER_VAR, _ANIMATION_TIME_VAR, + _ANIMATION_CURRENT_STATE_VAR, _ANIMATION_NEXT_STATE_VAR + ]) + .toStmt()); return o.fn( [ @@ -348,6 +358,7 @@ class _AnimationBuilderContext { stateMap = new _AnimationBuilderStateMap(); endStateAnimateStep: AnimationStepAst = null; isExpectingFirstStyleStep = false; + totalTransitionTime = 0; } class _AnimationBuilderStateMap { diff --git a/modules/@angular/core/index.ts b/modules/@angular/core/index.ts index 62f3ceebde..d883b137a6 100644 --- a/modules/@angular/core/index.ts +++ b/modules/@angular/core/index.ts @@ -34,6 +34,7 @@ export {ExceptionHandler, WrappedException, BaseException} from './src/facade/ex export * from './private_export'; export * from './src/animation/metadata'; +export {AnimationTransitionEvent} from './src/animation/animation_transition_event'; export {AnimationPlayer} from './src/animation/animation_player'; export {SanitizationService, SecurityContext} from './src/security'; diff --git a/modules/@angular/core/src/animation/animation_transition_event.ts b/modules/@angular/core/src/animation/animation_transition_event.ts new file mode 100644 index 0000000000..cb861394e1 --- /dev/null +++ b/modules/@angular/core/src/animation/animation_transition_event.ts @@ -0,0 +1,51 @@ +/** + * @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 + */ + +/** + * An instance of this class is returned as an event parameter when an animation + * callback is captured for an animation either during the start or done phase. + * + * ```typescript + * @Component({ + * host: { + * '[@myAnimationTrigger]': 'someExpression', + * '(@myAnimationTrigger.start)': 'captureStartEvent($event)', + * '(@myAnimationTrigger.done)': 'captureDoneEvent($event)', + * }, + * animations: [ + * trigger("myAnimationTrigger", [ + * // ... + * ]) + * ] + * }) + * class MyComponent { + * someExpression: any = false; + * captureStartEvent(event: AnimationTransitionEvent) { + * // the toState, fromState and totalTime data is accessible from the event variable + * } + * + * captureDoneEvent(event: AnimationTransitionEvent) { + * // the toState, fromState and totalTime data is accessible from the event variable + * } + * } + * ``` + * + * @experimental Animation support is experimental. + */ +export class AnimationTransitionEvent { + public fromState: string; + public toState: string; + public totalTime: number; + + constructor({fromState, toState, + totalTime}: {fromState: string, toState: string, totalTime: number}) { + this.fromState = fromState; + this.toState = toState; + this.totalTime = totalTime; + } +} diff --git a/modules/@angular/core/src/linker/view.ts b/modules/@angular/core/src/linker/view.ts index fc3672e353..de500ea5e2 100644 --- a/modules/@angular/core/src/linker/view.ts +++ b/modules/@angular/core/src/linker/view.ts @@ -9,6 +9,7 @@ import {AnimationGroupPlayer} from '../animation/animation_group_player'; import {AnimationOutput} from '../animation/animation_output'; import {AnimationPlayer, NoOpAnimationPlayer} from '../animation/animation_player'; +import {AnimationTransitionEvent} from '../animation/animation_transition_event'; import {ViewAnimationMap} from '../animation/view_animation_map'; import {ChangeDetectorRef, ChangeDetectorStatus} from '../change_detection/change_detection'; import {Injector} from '../di/injector'; @@ -81,22 +82,17 @@ export abstract class AppView { } queueAnimation( - element: any, animationName: string, player: AnimationPlayer, fromState: string, - toState: string): void { - var actualAnimationDetected = !(player instanceof NoOpAnimationPlayer); - var animationData = { - 'fromState': fromState, - 'toState': toState, - 'running': actualAnimationDetected - }; + element: any, animationName: string, player: AnimationPlayer, totalTime: number, + fromState: string, toState: string): void { + var event = new AnimationTransitionEvent( + {'fromState': fromState, 'toState': toState, 'totalTime': totalTime}); this.animationPlayers.set(element, animationName, player); player.onDone(() => { // TODO: make this into a datastructure for done|start - this.triggerAnimationOutput(element, animationName, 'done', animationData); + this.triggerAnimationOutput(element, animationName, 'done', event); this.animationPlayers.remove(element, animationName); }); - player.onStart( - () => { this.triggerAnimationOutput(element, animationName, 'start', animationData); }); + player.onStart(() => { this.triggerAnimationOutput(element, animationName, 'start', event); }); } triggerQueuedAnimations() { @@ -108,7 +104,7 @@ export abstract class AppView { } triggerAnimationOutput( - element: any, animationName: string, phase: string, animationData: {[key: string]: any}) { + element: any, animationName: string, phase: string, event: AnimationTransitionEvent) { var listeners = this._animationListeners.get(element); if (isPresent(listeners) && listeners.length) { for (let i = 0; i < listeners.length; i++) { @@ -116,7 +112,7 @@ export abstract class AppView { // we check for both the name in addition to the phase in the event // that there may be more than one @trigger on the same element if (listener.output.name == animationName && listener.output.phase == phase) { - listener.handler(animationData); + listener.handler(event); break; } } diff --git a/modules/@angular/core/test/animation/animation_integration_spec.ts b/modules/@angular/core/test/animation/animation_integration_spec.ts index 2424f7ae7f..5dfdb7eeea 100644 --- a/modules/@angular/core/test/animation/animation_integration_spec.ts +++ b/modules/@angular/core/test/animation/animation_integration_spec.ts @@ -10,11 +10,13 @@ import {CommonModule} from '@angular/common'; 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 {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 {AnimationTransitionEvent} from '../../src/animation/animation_transition_event'; import {AUTO_STYLE, animate, group, keyframes, sequence, state, style, transition, trigger} from '../../src/animation/metadata'; import {isPresent} from '../../src/facade/lang'; import {TestBed, fakeAsync, flushMicrotasks} from '../../testing'; @@ -980,8 +982,8 @@ function declareTests({useJit}: {useJit: boolean}) { var isAnimationRunning = false; var calls = 0; var cmp = fixture.debugElement.componentInstance; - cmp.callback = (e: any) => { - isAnimationRunning = e['running']; + cmp.callback = (e: AnimationTransitionEvent) => { + isAnimationRunning = e.totalTime > 0; calls++; }; @@ -1016,8 +1018,8 @@ function declareTests({useJit}: {useJit: boolean}) { var isAnimationRunning = false; var calls = 0; var cmp = fixture.debugElement.componentInstance; - cmp.callback = (e: any) => { - isAnimationRunning = e['running']; + cmp.callback = (e: AnimationTransitionEvent) => { + isAnimationRunning = e.totalTime > 0; calls++; }; cmp.exp = 'one'; @@ -1056,20 +1058,56 @@ function declareTests({useJit}: {useJit: boolean}) { const driver = TestBed.get(AnimationDriver) as InnerContentTrackingAnimationDriver; let fixture = TestBed.createComponent(DummyIfCmp); - var eventData: any = {}; + var eventData: AnimationTransitionEvent = null; var cmp = fixture.debugElement.componentInstance; - cmp.callback = (e: any) => { eventData = e; }; + cmp.callback = (e: AnimationTransitionEvent) => { eventData = e; }; cmp.exp = 'one'; fixture.detectChanges(); flushMicrotasks(); - expect(eventData['fromState']).toEqual('void'); - expect(eventData['toState']).toEqual('one'); + expect(eventData.fromState).toEqual('void'); + expect(eventData.toState).toEqual('one'); cmp.exp = 'two'; fixture.detectChanges(); flushMicrotasks(); - expect(eventData['fromState']).toEqual('one'); - expect(eventData['toState']).toEqual('two'); + expect(eventData.fromState).toEqual('one'); + expect(eventData.toState).toEqual('two'); + })); + + it('should emit the `totalTime` values for an animation callback', fakeAsync(() => { + TestBed.overrideComponent(DummyIfCmp, { + set: { + template: ` +
+
+ `, + animations: [ + trigger( + 'trigger', + [transition( + '* => *', + [animate('1s 750ms', style({})), animate('2000ms 0ms', style({}))])]), + trigger('noTrigger', []) + ] + } + }); + + const driver = TestBed.get(AnimationDriver) as InnerContentTrackingAnimationDriver; + let fixture = TestBed.createComponent(DummyIfCmp); + var eventData1: AnimationTransitionEvent = null; + var eventData2: AnimationTransitionEvent = null; + var cmp = fixture.debugElement.componentInstance; + cmp.callback1 = (e: AnimationTransitionEvent) => { eventData1 = e; }; + cmp.callback2 = (e: AnimationTransitionEvent) => { eventData2 = e; }; + cmp.exp = 'one'; + fixture.detectChanges(); + flushMicrotasks(); + expect(eventData1.totalTime).toEqual(3750); + + cmp.exp2 = 'two'; + fixture.detectChanges(); + flushMicrotasks(); + expect(eventData2.totalTime).toEqual(0); })); it('should throw an error if an animation output is referenced is not defined within the component', diff --git a/tools/public_api_guard/core/index.d.ts b/tools/public_api_guard/core/index.d.ts index 0351e6618f..63880f3848 100644 --- a/tools/public_api_guard/core/index.d.ts +++ b/tools/public_api_guard/core/index.d.ts @@ -113,6 +113,18 @@ export declare class AnimationStyleMetadata extends AnimationMetadata { }>, offset?: number); } +/** @experimental */ +export declare class AnimationTransitionEvent { + fromState: string; + toState: string; + totalTime: number; + constructor({fromState, toState, totalTime}: { + fromState: string; + toState: string; + totalTime: number; + }); +} + /** @experimental */ export declare abstract class AnimationWithStepsMetadata extends AnimationMetadata { steps: AnimationMetadata[];