feat(animations): expose element and params within transition matchers (#22693)

PR Close #22693
This commit is contained in:
Matias Niemelä 2018-03-12 17:21:02 -07:00 committed by Kara Erickson
parent db56836425
commit 58b94e6f5e
11 changed files with 119 additions and 20 deletions

View File

@ -46,7 +46,8 @@ export interface StateAst extends Ast<AnimationMetadataType.State> {
} }
export interface TransitionAst extends Ast<AnimationMetadataType.Transition> { export interface TransitionAst extends Ast<AnimationMetadataType.Transition> {
matchers: ((fromState: string, toState: string) => boolean)[]; matchers: ((fromState: string, toState: string, element: any, params: {[key: string]:
any}) => boolean)[];
animation: Ast<AnimationMetadataType>; animation: Ast<AnimationMetadataType>;
queryCount: number; queryCount: number;
depCount: number; depCount: number;

View File

@ -6,7 +6,8 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
export const ANY_STATE = '*'; export const ANY_STATE = '*';
export declare type TransitionMatcherFn = (fromState: any, toState: any) => boolean; export declare type TransitionMatcherFn =
(fromState: any, toState: any, element: any, params: {[key: string]: any}) => boolean;
export function parseTransitionExpr( export function parseTransitionExpr(
transitionValue: string | TransitionMatcherFn, errors: string[]): TransitionMatcherFn[] { transitionValue: string | TransitionMatcherFn, errors: string[]): TransitionMatcherFn[] {

View File

@ -24,8 +24,8 @@ export class AnimationTransitionFactory {
private _triggerName: string, public ast: TransitionAst, private _triggerName: string, public ast: TransitionAst,
private _stateStyles: {[stateName: string]: AnimationStateStyles}) {} private _stateStyles: {[stateName: string]: AnimationStateStyles}) {}
match(currentState: any, nextState: any): boolean { match(currentState: any, nextState: any, element: any, params: {[key: string]: any}): boolean {
return oneOrMoreTransitionsMatch(this.ast.matchers, currentState, nextState); return oneOrMoreTransitionsMatch(this.ast.matchers, currentState, nextState, element, params);
} }
buildStyles(stateName: string, params: {[key: string]: any}, errors: any[]) { buildStyles(stateName: string, params: {[key: string]: any}, errors: any[]) {
@ -89,8 +89,9 @@ export class AnimationTransitionFactory {
} }
function oneOrMoreTransitionsMatch( function oneOrMoreTransitionsMatch(
matchFns: TransitionMatcherFn[], currentState: any, nextState: any): boolean { matchFns: TransitionMatcherFn[], currentState: any, nextState: any, element: any,
return matchFns.some(fn => fn(currentState, nextState)); params: {[key: string]: any}): boolean {
return matchFns.some(fn => fn(currentState, nextState, element, params));
} }
export class AnimationStateStyles { export class AnimationStateStyles {

View File

@ -47,8 +47,10 @@ export class AnimationTrigger {
get containsQueries() { return this.ast.queryCount > 0; } get containsQueries() { return this.ast.queryCount > 0; }
matchTransition(currentState: any, nextState: any): AnimationTransitionFactory|null { matchTransition(currentState: any, nextState: any, element: any, params: {[key: string]: any}):
const entry = this.transitionFactories.find(f => f.match(currentState, nextState)); AnimationTransitionFactory|null {
const entry =
this.transitionFactories.find(f => f.match(currentState, nextState, element, params));
return entry || null; return entry || null;
} }

View File

@ -248,7 +248,8 @@ export class AnimationTransitionNamespace {
} }
}); });
let transition = trigger.matchTransition(fromState.value, toState.value); let transition =
trigger.matchTransition(fromState.value, toState.value, element, toState.params);
let isFallbackTransition = false; let isFallbackTransition = false;
if (!transition) { if (!transition) {
if (!defaultToFallback) return; if (!defaultToFallback) return;

View File

@ -103,7 +103,7 @@ import {makeTrigger} from '../shared';
it('should null when no results are found', () => { it('should null when no results are found', () => {
const result = makeTrigger('name', [transition('a => b', animate(1111))]); const result = makeTrigger('name', [transition('a => b', animate(1111))]);
const trigger = result.matchTransition('b', 'a'); const trigger = result.matchTransition('b', 'a', {}, {});
expect(trigger).toBeFalsy(); expect(trigger).toBeFalsy();
}); });
@ -226,7 +226,8 @@ function buildTransition(
trigger: AnimationTrigger, element: any, fromState: any, toState: any, trigger: AnimationTrigger, element: any, fromState: any, toState: any,
fromOptions?: AnimationOptions, toOptions?: AnimationOptions): AnimationTransitionInstruction| fromOptions?: AnimationOptions, toOptions?: AnimationOptions): AnimationTransitionInstruction|
null { null {
const trans = trigger.matchTransition(fromState, toState) !; const params = toOptions && toOptions.params || {};
const trans = trigger.matchTransition(fromState, toState, element, params) !;
if (trans) { if (trans) {
const driver = new MockAnimationDriver(); const driver = new MockAnimationDriver();
return trans.build( return trans.build(

View File

@ -115,7 +115,9 @@ export interface AnimationStateMetadata extends AnimationMetadata {
* @experimental Animation support is experimental. * @experimental Animation support is experimental.
*/ */
export interface AnimationTransitionMetadata extends AnimationMetadata { export interface AnimationTransitionMetadata extends AnimationMetadata {
expr: string|((fromState: string, toState: string) => boolean); expr: string|
((fromState: string, toState: string, element?: any,
params?: {[key: string]: any}) => boolean);
animation: AnimationMetadata|AnimationMetadata[]; animation: AnimationMetadata|AnimationMetadata[];
options: AnimationOptions|null; options: AnimationOptions|null;
} }
@ -294,6 +296,38 @@ export interface AnimationStaggerMetadata extends AnimationMetadata {
* <div [@myAnimationTrigger]="myStatusExp">...</div> * <div [@myAnimationTrigger]="myStatusExp">...</div>
* ``` * ```
* *
* ### Using an inline function
* The `transition` animation method also supports reading an inline function which can decide
* if its associated animation should be run.
*
* ```
* // this method will be run each time the `myAnimationTrigger`
* // trigger value changes...
* function myInlineMatcherFn(fromState: string, toState: string, element: any, params: {[key:
string]: any}): boolean {
* // notice that `element` and `params` are also available here
* return toState == 'yes-please-animate';
* }
*
* @Component({
* selector: 'my-component',
* templateUrl: 'my-component-tpl.html',
* animations: [
* trigger('myAnimationTrigger', [
* transition(myInlineMatcherFn, [
* // the animation sequence code
* ]),
* ])
* ]
* })
* class MyComponent {
* myStatusExp = "yes-please-animate";
* }
* ```
*
* The inline method will be run each time the trigger
* value changes
*
* ## Disable Animations * ## Disable Animations
* A special animation control binding called `@.disabled` can be placed on an element which will * A special animation control binding called `@.disabled` can be placed on an element which will
then disable animations for any inner animation triggers situated within the element as well as then disable animations for any inner animation triggers situated within the element as well as
@ -844,7 +878,8 @@ export function keyframes(steps: AnimationStyleMetadata[]): AnimationKeyframesSe
* @experimental Animation support is experimental. * @experimental Animation support is experimental.
*/ */
export function transition( export function transition(
stateChangeExpr: string | ((fromState: string, toState: string) => boolean), stateChangeExpr: string | ((fromState: string, toState: string, element?: any,
params?: {[key: string]: any}) => boolean),
steps: AnimationMetadata | AnimationMetadata[], steps: AnimationMetadata | AnimationMetadata[],
options: AnimationOptions | null = null): AnimationTransitionMetadata { options: AnimationOptions | null = null): AnimationTransitionMetadata {
return {type: AnimationMetadataType.Transition, expr: stateChangeExpr, animation: steps, options}; return {type: AnimationMetadataType.Transition, expr: stateChangeExpr, animation: steps, options};

View File

@ -38,7 +38,8 @@ export interface AnimationStateMetadata extends AnimationMetadata {
* @deprecated This symbol has moved. Please Import from @angular/animations instead! * @deprecated This symbol has moved. Please Import from @angular/animations instead!
*/ */
export interface AnimationTransitionMetadata extends AnimationMetadata { export interface AnimationTransitionMetadata extends AnimationMetadata {
expr: string|((fromState: string, toState: string) => boolean); expr: string|
((fromState: string, toState: string, element: any, params: {[key: string]: any}) => boolean);
animation: AnimationMetadata|AnimationMetadata[]; animation: AnimationMetadata|AnimationMetadata[];
} }

View File

@ -301,12 +301,15 @@ const DEFAULT_COMPONENT_ID = '1';
it('should allow a transition to use a function to determine what method to run', () => { it('should allow a transition to use a function to determine what method to run', () => {
let valueToMatch = ''; let valueToMatch = '';
const transitionFn = let capturedElement: any;
(fromState: string, toState: string) => { return toState == valueToMatch; }; const transitionFn = (fromState: string, toState: string, element: any) => {
capturedElement = element;
return toState == valueToMatch;
};
@Component({ @Component({
selector: 'if-cmp', selector: 'if-cmp',
template: '<div [@myAnimation]="exp"></div>', template: '<div #element [@myAnimation]="exp"></div>',
animations: [ animations: [
trigger('myAnimation', [transition( trigger('myAnimation', [transition(
transitionFn, transitionFn,
@ -314,6 +317,8 @@ const DEFAULT_COMPONENT_ID = '1';
] ]
}) })
class Cmp { class Cmp {
@ViewChild('element')
element: any;
exp: any = ''; exp: any = '';
} }
@ -323,11 +328,13 @@ const DEFAULT_COMPONENT_ID = '1';
const cmp = fixture.componentInstance; const cmp = fixture.componentInstance;
valueToMatch = cmp.exp = 'something'; valueToMatch = cmp.exp = 'something';
fixture.detectChanges(); fixture.detectChanges();
const element = cmp.element.nativeElement;
let players = getLog(); let players = getLog();
expect(players.length).toEqual(1); expect(players.length).toEqual(1);
let [p1] = players; let [p1] = players;
expect(p1.totalTime).toEqual(1234); expect(p1.totalTime).toEqual(1234);
expect(capturedElement).toEqual(element);
resetLog(); resetLog();
valueToMatch = 'something-else'; valueToMatch = 'something-else';
@ -338,6 +345,49 @@ const DEFAULT_COMPONENT_ID = '1';
expect(players.length).toEqual(0); expect(players.length).toEqual(0);
}); });
it('should allow a transition to use a function to determine what method to run and expose any parameter values',
() => {
const transitionFn =
(fromState: string, toState: string, element: any, params: {[key: string]: any}) => {
return params['doMatch'] == true;
};
@Component({
selector: 'if-cmp',
template: '<div [@myAnimation]="{value:exp, params: {doMatch:doMatch}}"></div>',
animations: [
trigger(
'myAnimation',
[transition(
transitionFn, [style({opacity: 0}), animate(3333, style({opacity: 1}))])]),
]
})
class Cmp {
doMatch = false;
exp: any = '';
}
TestBed.configureTestingModule({declarations: [Cmp]});
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
cmp.doMatch = true;
fixture.detectChanges();
let players = getLog();
expect(players.length).toEqual(1);
let [p1] = players;
expect(p1.totalTime).toEqual(3333);
resetLog();
cmp.doMatch = false;
cmp.exp = 'this-wont-match';
fixture.detectChanges();
players = getLog();
expect(players.length).toEqual(0);
});
it('should allow a state value to be `0`', () => { it('should allow a state value to be `0`', () => {
@Component({ @Component({
selector: 'if-cmp', selector: 'if-cmp',

View File

@ -173,7 +173,9 @@ export interface AnimationStyleMetadata extends AnimationMetadata {
/** @experimental */ /** @experimental */
export interface AnimationTransitionMetadata extends AnimationMetadata { export interface AnimationTransitionMetadata extends AnimationMetadata {
animation: AnimationMetadata | AnimationMetadata[]; animation: AnimationMetadata | AnimationMetadata[];
expr: string | ((fromState: string, toState: string) => boolean); expr: string | ((fromState: string, toState: string, element?: any, params?: {
[key: string]: any;
}) => boolean);
options: AnimationOptions | null; options: AnimationOptions | null;
} }
@ -241,7 +243,9 @@ export declare function style(tokens: '*' | {
}>): AnimationStyleMetadata; }>): AnimationStyleMetadata;
/** @experimental */ /** @experimental */
export declare function transition(stateChangeExpr: string | ((fromState: string, toState: string) => boolean), steps: AnimationMetadata | AnimationMetadata[], options?: AnimationOptions | null): AnimationTransitionMetadata; export declare function transition(stateChangeExpr: string | ((fromState: string, toState: string, element?: any, params?: {
[key: string]: any;
}) => boolean), steps: AnimationMetadata | AnimationMetadata[], options?: AnimationOptions | null): AnimationTransitionMetadata;
/** @experimental */ /** @experimental */
export declare function trigger(name: string, definitions: AnimationMetadata[]): AnimationTriggerMetadata; export declare function trigger(name: string, definitions: AnimationMetadata[]): AnimationTriggerMetadata;

View File

@ -94,7 +94,9 @@ export interface AnimationTransitionEvent {
/** @deprecated */ /** @deprecated */
export interface AnimationTransitionMetadata extends AnimationMetadata { export interface AnimationTransitionMetadata extends AnimationMetadata {
animation: AnimationMetadata | AnimationMetadata[]; animation: AnimationMetadata | AnimationMetadata[];
expr: string | ((fromState: string, toState: string) => boolean); expr: string | ((fromState: string, toState: string, element: any, params: {
[key: string]: any;
}) => boolean);
} }
/** @deprecated */ /** @deprecated */