feat(animations): expose the element value within transition events

This commit is contained in:
Matias Niemelä
2016-12-20 15:57:02 -08:00
parent 02dd90faed
commit 4bae4b3bb5
6 changed files with 113 additions and 37 deletions

View File

@ -294,8 +294,8 @@ class _AnimationBuilder implements AnimationAstVisitor {
statements.push(new o.ReturnStatement( statements.push(new o.ReturnStatement(
o.importExpr(createIdentifier(Identifiers.AnimationTransition)).instantiate([ o.importExpr(createIdentifier(Identifiers.AnimationTransition)).instantiate([
_ANIMATION_PLAYER_VAR, _ANIMATION_CURRENT_STATE_VAR, _ANIMATION_NEXT_STATE_VAR, _ANIMATION_PLAYER_VAR, _ANIMATION_FACTORY_ELEMENT_VAR, _ANIMATION_CURRENT_STATE_VAR,
_ANIMATION_TIME_VAR _ANIMATION_NEXT_STATE_VAR, _ANIMATION_TIME_VAR
]))); ])));
return o.fn( return o.fn(

View File

@ -5,20 +5,23 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * found in the LICENSE file at https://angular.io/license
*/ */
import {ElementRef} from '../linker/element_ref';
import {AnimationPlayer} from './animation_player'; import {AnimationPlayer} from './animation_player';
import {AnimationTransitionEvent} from './animation_transition_event'; import {AnimationTransitionEvent} from './animation_transition_event';
export class AnimationTransition { export class AnimationTransition {
constructor( constructor(
private _player: AnimationPlayer, private _fromState: string, private _toState: string, private _player: AnimationPlayer, private _element: ElementRef, private _fromState: string,
private _totalTime: number) {} private _toState: string, private _totalTime: number) {}
private _createEvent(phaseName: string): AnimationTransitionEvent { private _createEvent(phaseName: string): AnimationTransitionEvent {
return new AnimationTransitionEvent({ return new AnimationTransitionEvent({
fromState: this._fromState, fromState: this._fromState,
toState: this._toState, toState: this._toState,
totalTime: this._totalTime, totalTime: this._totalTime,
phaseName: phaseName phaseName: phaseName,
element: this._element
}); });
} }

View File

@ -5,6 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * found in the LICENSE file at https://angular.io/license
*/ */
import {ElementRef} from '../linker/element_ref';
/** /**
* An instance of this class is returned as an event parameter when an animation * An instance of this class is returned as an event parameter when an animation
@ -42,12 +43,19 @@ export class AnimationTransitionEvent {
public toState: string; public toState: string;
public totalTime: number; public totalTime: number;
public phaseName: string; public phaseName: string;
public element: ElementRef;
constructor({fromState, toState, totalTime, phaseName}: constructor({fromState, toState, totalTime, phaseName, element}: {
{fromState: string, toState: string, totalTime: number, phaseName: string}) { fromState: string,
toState: string,
totalTime: number,
phaseName: string,
element: any
}) {
this.fromState = fromState; this.fromState = fromState;
this.toState = toState; this.toState = toState;
this.totalTime = totalTime; this.totalTime = totalTime;
this.phaseName = phaseName; this.phaseName = phaseName;
this.element = new ElementRef(element);
} }
} }

View File

@ -27,6 +27,7 @@ import {AnimationTransitionEvent} from '../../src/animation/animation_transition
import {AUTO_STYLE, animate, group, keyframes, sequence, state, style, transition, trigger} from '../../src/animation/metadata'; import {AUTO_STYLE, animate, group, keyframes, sequence, state, style, transition, trigger} from '../../src/animation/metadata';
import {Input} from '../../src/core'; import {Input} from '../../src/core';
import {isPresent} from '../../src/facade/lang'; import {isPresent} from '../../src/facade/lang';
import {ElementRef} from '../../src/linker/element_ref';
import {TestBed, fakeAsync, flushMicrotasks} from '../../testing'; import {TestBed, fakeAsync, flushMicrotasks} from '../../testing';
import {MockAnimationPlayer} from '../../testing/mock_animation_player'; import {MockAnimationPlayer} from '../../testing/mock_animation_player';
@ -1763,6 +1764,36 @@ function declareTests({useJit}: {useJit: boolean}) {
expect(doneCalls[3]).toEqual(1); expect(doneCalls[3]).toEqual(1);
expect(doneCalls[4]).toEqual(1); expect(doneCalls[4]).toEqual(1);
})); }));
it('should expose the element associated with the animation within the callback event',
fakeAsync(() => {
TestBed.overrideComponent(DummyIfCmp, {
set: {
template: `
<div *ngFor="let item of items"
(@trigger.start)="callback($event)"
@trigger class="target">{{ item }}</div>
`,
animations: [trigger('trigger', [transition('* => *', [animate(1000)])])]
}
});
const fixture = TestBed.createComponent(DummyIfCmp);
const cmp = fixture.componentInstance;
const elements: ElementRef[] = [];
cmp.callback = (e: AnimationTransitionEvent) => elements.push(e.element);
cmp.items = [0, 1, 2, 3, 4];
fixture.detectChanges();
flushMicrotasks();
const targetElements =
<any[]>getDOM().querySelectorAll(fixture.nativeElement, '.target');
for (let i = 0; i < elements.length; i++) {
expect(elements[i].nativeElement).toBe(targetElements[i]);
}
}));
}); });
describe('ng directives', () => { describe('ng directives', () => {

View File

@ -8,6 +8,8 @@
import {AUTO_STYLE, AnimationTransitionEvent, Component, Injector, ViewChild, animate, state, style, transition, trigger} from '@angular/core'; import {AUTO_STYLE, AnimationTransitionEvent, Component, Injector, ViewChild, animate, state, style, transition, trigger} from '@angular/core';
import {DebugDomRootRenderer} from '@angular/core/src/debug/debug_renderer'; import {DebugDomRootRenderer} from '@angular/core/src/debug/debug_renderer';
import {ElementRef} from '@angular/core/src/linker/element_ref';
import {ViewChild} from '@angular/core/src/metadata/di';
import {RootRenderer} from '@angular/core/src/render/api'; import {RootRenderer} from '@angular/core/src/render/api';
import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing'; import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing';
import {MockAnimationPlayer} from '@angular/core/testing/testing_internal'; import {MockAnimationPlayer} from '@angular/core/testing/testing_internal';
@ -203,24 +205,34 @@ export function main() {
flushMicrotasks(); flushMicrotasks();
uiDriver.log.shift()['player'].finish(); uiDriver.log.shift()['player'].finish();
const [triggerOneStart, triggerOneDone] = log['one'];
expect(triggerOneStart)
.toEqual(new AnimationTransitionEvent(
{fromState: 'a', toState: 'b', totalTime: 500, phaseName: 'start'}));
expect(triggerOneDone) const [triggerOneStart, triggerOneDone] = log['one'];
.toEqual(new AnimationTransitionEvent( expect(triggerOneStart.fromState).toEqual('a');
{fromState: 'a', toState: 'b', totalTime: 500, phaseName: 'done'})); expect(triggerOneStart.toState).toEqual('b');
expect(triggerOneStart.totalTime).toEqual(500);
expect(triggerOneStart.phaseName).toEqual('start');
expect(triggerOneStart.element instanceof ElementRef).toEqual(true);
expect(triggerOneDone.fromState).toEqual('a');
expect(triggerOneDone.toState).toEqual('b');
expect(triggerOneDone.totalTime).toEqual(500);
expect(triggerOneDone.phaseName).toEqual('done');
expect(triggerOneDone.element instanceof ElementRef).toEqual(true);
uiDriver.log.shift()['player'].finish(); uiDriver.log.shift()['player'].finish();
const [triggerTwoStart, triggerTwoDone] = log['two'];
expect(triggerTwoStart)
.toEqual(new AnimationTransitionEvent(
{fromState: 'c', toState: 'd', totalTime: 1000, phaseName: 'start'}));
expect(triggerTwoDone) const [triggerTwoStart, triggerTwoDone] = log['two'];
.toEqual(new AnimationTransitionEvent( expect(triggerTwoStart.fromState).toEqual('c');
{fromState: 'c', toState: 'd', totalTime: 1000, phaseName: 'done'})); expect(triggerTwoStart.toState).toEqual('d');
expect(triggerTwoStart.totalTime).toEqual(1000);
expect(triggerTwoStart.phaseName).toEqual('start');
expect(triggerTwoStart.element instanceof ElementRef).toEqual(true);
expect(triggerTwoDone.fromState).toEqual('c');
expect(triggerTwoDone.toState).toEqual('d');
expect(triggerTwoDone.totalTime).toEqual(1000);
expect(triggerTwoDone.phaseName).toEqual('done');
expect(triggerTwoDone.element instanceof ElementRef).toEqual(true);
})); }));
it('should handle .start and .done callbacks for mutliple elements that contain animations that are fired at the same time', it('should handle .start and .done callbacks for mutliple elements that contain animations that are fired at the same time',
@ -228,7 +240,7 @@ export function main() {
function logFactory( function logFactory(
log: {[phaseName: string]: AnimationTransitionEvent}, log: {[phaseName: string]: AnimationTransitionEvent},
phaseName: string): (event: AnimationTransitionEvent) => any { phaseName: string): (event: AnimationTransitionEvent) => any {
return (event: AnimationTransitionEvent) => { log[phaseName] = event; }; return (event: AnimationTransitionEvent) => log[phaseName] = event;
} }
const fixture = TestBed.createComponent(ContainerAnimationCmp); const fixture = TestBed.createComponent(ContainerAnimationCmp);
@ -250,25 +262,37 @@ export function main() {
uiDriver.log.shift()['player'].finish(); uiDriver.log.shift()['player'].finish();
expect(cmp1Log['start']) const start1 = cmp1Log['start'];
.toEqual(new AnimationTransitionEvent( expect(start1.fromState).toEqual('void');
{fromState: 'void', toState: 'off', totalTime: 500, phaseName: 'start'})); expect(start1.toState).toEqual('off');
expect(start1.totalTime).toEqual(500);
expect(start1.phaseName).toEqual('start');
expect(start1.element instanceof ElementRef).toBe(true);
expect(cmp1Log['done']) const done1 = cmp1Log['done'];
.toEqual(new AnimationTransitionEvent( expect(done1.fromState).toEqual('void');
{fromState: 'void', toState: 'off', totalTime: 500, phaseName: 'done'})); expect(done1.toState).toEqual('off');
expect(done1.totalTime).toEqual(500);
expect(done1.phaseName).toEqual('done');
expect(done1.element instanceof ElementRef).toBe(true);
// the * => on transition has two steps // the * => on transition has two steps
uiDriver.log.shift()['player'].finish(); uiDriver.log.shift()['player'].finish();
uiDriver.log.shift()['player'].finish(); uiDriver.log.shift()['player'].finish();
expect(cmp2Log['start']) const start2 = cmp2Log['start'];
.toEqual(new AnimationTransitionEvent( expect(start2.fromState).toEqual('void');
{fromState: 'void', toState: 'on', totalTime: 1000, phaseName: 'start'})); expect(start2.toState).toEqual('on');
expect(start2.totalTime).toEqual(1000);
expect(start2.phaseName).toEqual('start');
expect(start2.element instanceof ElementRef).toBe(true);
expect(cmp2Log['done']) const done2 = cmp2Log['done'];
.toEqual(new AnimationTransitionEvent( expect(done2.fromState).toEqual('void');
{fromState: 'void', toState: 'on', totalTime: 1000, phaseName: 'done'})); expect(done2.toState).toEqual('on');
expect(done2.totalTime).toEqual(1000);
expect(done2.phaseName).toEqual('done');
expect(done2.element instanceof ElementRef).toBe(true);
})); }));
it('should destroy the player when the animation is complete', fakeAsync(() => { it('should destroy the player when the animation is complete', fakeAsync(() => {
@ -331,6 +355,7 @@ class ContainerAnimationCmp {
selector: 'my-comp', selector: 'my-comp',
template: ` template: `
<div *ngIf="state" <div *ngIf="state"
#ref
[@myTrigger]="state" [@myTrigger]="state"
(@myTrigger.start)="stateStartFn($event)" (@myTrigger.start)="stateStartFn($event)"
(@myTrigger.done)="stateDoneFn($event)">...</div> (@myTrigger.done)="stateDoneFn($event)">...</div>
@ -348,6 +373,8 @@ class AnimationCmp {
state = 'off'; state = 'off';
stateStartFn = (event: AnimationTransitionEvent): any => {}; stateStartFn = (event: AnimationTransitionEvent): any => {};
stateDoneFn = (event: AnimationTransitionEvent): any => {}; stateDoneFn = (event: AnimationTransitionEvent): any => {};
@ViewChild('ref') public elmRef: ElementRef;
} }
@Component({ @Component({
@ -355,10 +382,10 @@ class AnimationCmp {
template: ` template: `
<div [@one]="oneTriggerState" <div [@one]="oneTriggerState"
(@one.start)="callback('one', $event)" (@one.start)="callback('one', $event)"
(@one.done)="callback('one', $event)">...</div> (@one.done)="callback('one', $event)" #one>...</div>
<div [@two]="twoTriggerState" <div [@two]="twoTriggerState"
(@two.start)="callback('two', $event)" (@two.start)="callback('two', $event)"
(@two.done)="callback('two', $event)">...</div> (@two.done)="callback('two', $event)" #two>...</div>
`, `,
animations: [ animations: [
trigger( trigger(
@ -378,5 +405,10 @@ class AnimationCmp {
class MultiAnimationCmp { class MultiAnimationCmp {
oneTriggerState: string; oneTriggerState: string;
twoTriggerState: string; twoTriggerState: string;
@ViewChild('one') public elmRef1: ElementRef;
@ViewChild('two') public elmRef2: ElementRef;
callback = (triggerName: string, event: AnimationTransitionEvent): any => {}; callback = (triggerName: string, event: AnimationTransitionEvent): any => {};
} }

View File

@ -108,15 +108,17 @@ export declare class AnimationStyleMetadata extends AnimationMetadata {
/** @experimental */ /** @experimental */
export declare class AnimationTransitionEvent { export declare class AnimationTransitionEvent {
element: ElementRef;
fromState: string; fromState: string;
phaseName: string; phaseName: string;
toState: string; toState: string;
totalTime: number; totalTime: number;
constructor({fromState, toState, totalTime, phaseName}: { constructor({fromState, toState, totalTime, phaseName, element}: {
fromState: string; fromState: string;
toState: string; toState: string;
totalTime: number; totalTime: number;
phaseName: string; phaseName: string;
element: any;
}); });
} }