feat(animations): support function types in transitions

Closes #13538
Closes #13537
This commit is contained in:
Matias Niemelä 2016-12-16 17:52:17 -08:00
parent 3f67ab074a
commit 9211a22039
8 changed files with 144 additions and 36 deletions

View File

@ -8,6 +8,10 @@
import {AUTO_STYLE, Component, animate, state, style, transition, trigger} from '@angular/core';
export function anyToAny(stateA: string, stateB: string): boolean {
return Math.random() != Math.random();
}
@Component({
selector: 'animate-cmp',
animations: [trigger(
@ -16,7 +20,7 @@ import {AUTO_STYLE, Component, animate, state, style, transition, trigger} from
state('*', style({height: AUTO_STYLE, color: 'black', borderColor: 'black'})),
state('closed, void', style({height: '0px', color: 'maroon', borderColor: 'maroon'})),
state('open', style({height: AUTO_STYLE, borderColor: 'green', color: 'green'})),
transition('* => *', animate(500))
transition(anyToAny, animate('1s')), transition('* => *', animate(500))
])],
template: `
<button (click)="setAsOpen()">Open</button>

View File

@ -5,6 +5,7 @@
* 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
*/
import {StaticSymbol} from '../aot/static_symbol';
export abstract class AnimationAst {
public startTime: number = 0;
@ -49,6 +50,10 @@ export class AnimationStateTransitionExpression {
constructor(public fromState: string, public toState: string) {}
}
export class AnimationStateTransitionFnExpression extends AnimationStateTransitionExpression {
constructor(public fn: Function|StaticSymbol) { super(null, null); }
}
export class AnimationStateTransitionAst extends AnimationStateAst {
constructor(
public stateChanges: AnimationStateTransitionExpression[],

View File

@ -12,7 +12,7 @@ import {Identifiers, createIdentifier} from '../identifiers';
import * as o from '../output/output_ast';
import {ANY_STATE, DEFAULT_STATE, EMPTY_STATE} from '../private_import_core';
import {AnimationAst, AnimationAstVisitor, AnimationEntryAst, AnimationGroupAst, AnimationKeyframeAst, AnimationSequenceAst, AnimationStateDeclarationAst, AnimationStateTransitionAst, AnimationStepAst, AnimationStylesAst} from './animation_ast';
import {AnimationAst, AnimationAstVisitor, AnimationEntryAst, AnimationGroupAst, AnimationKeyframeAst, AnimationSequenceAst, AnimationStateDeclarationAst, AnimationStateTransitionAst, AnimationStateTransitionFnExpression, AnimationStepAst, AnimationStylesAst} from './animation_ast';
export class AnimationEntryCompileResult {
constructor(public name: string, public statements: o.Statement[], public fnExp: o.Expression) {}
@ -162,16 +162,22 @@ class _AnimationBuilder implements AnimationAstVisitor {
const stateChangePreconditions: o.Expression[] = [];
ast.stateChanges.forEach(stateChange => {
stateChangePreconditions.push(
_compareToAnimationStateExpr(_ANIMATION_CURRENT_STATE_VAR, stateChange.fromState)
.and(_compareToAnimationStateExpr(_ANIMATION_NEXT_STATE_VAR, stateChange.toState)));
if (stateChange instanceof AnimationStateTransitionFnExpression) {
stateChangePreconditions.push(o.importExpr({reference: stateChange.fn}).callFn([
_ANIMATION_CURRENT_STATE_VAR, _ANIMATION_NEXT_STATE_VAR
]));
} else {
stateChangePreconditions.push(
_compareToAnimationStateExpr(_ANIMATION_CURRENT_STATE_VAR, stateChange.fromState)
.and(_compareToAnimationStateExpr(_ANIMATION_NEXT_STATE_VAR, stateChange.toState)));
if (stateChange.fromState != ANY_STATE) {
context.stateMap.registerState(stateChange.fromState);
}
if (stateChange.fromState != ANY_STATE) {
context.stateMap.registerState(stateChange.fromState);
}
if (stateChange.toState != ANY_STATE) {
context.stateMap.registerState(stateChange.toState);
if (stateChange.toState != ANY_STATE) {
context.stateMap.registerState(stateChange.toState);
}
}
});

View File

@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {StaticSymbol} from '../aot/static_symbol';
import {CompileAnimationAnimateMetadata, CompileAnimationEntryMetadata, CompileAnimationGroupMetadata, CompileAnimationKeyframesSequenceMetadata, CompileAnimationMetadata, CompileAnimationSequenceMetadata, CompileAnimationStateDeclarationMetadata, CompileAnimationStateTransitionMetadata, CompileAnimationStyleMetadata, CompileAnimationWithStepsMetadata, CompileDirectiveMetadata, identifierName} from '../compile_metadata';
import {StringMapWrapper} from '../facade/collection';
import {isBlank, isPresent} from '../facade/lang';
@ -14,7 +15,7 @@ import {ParseError} from '../parse_util';
import {ANY_STATE, FILL_STYLE_FLAG} from '../private_import_core';
import {ElementSchemaRegistry} from '../schema/element_schema_registry';
import {AnimationAst, AnimationEntryAst, AnimationGroupAst, AnimationKeyframeAst, AnimationSequenceAst, AnimationStateDeclarationAst, AnimationStateTransitionAst, AnimationStateTransitionExpression, AnimationStepAst, AnimationStylesAst, AnimationWithStepsAst} from './animation_ast';
import {AnimationAst, AnimationEntryAst, AnimationGroupAst, AnimationKeyframeAst, AnimationSequenceAst, AnimationStateDeclarationAst, AnimationStateTransitionAst, AnimationStateTransitionExpression, AnimationStateTransitionFnExpression, AnimationStepAst, AnimationStylesAst, AnimationWithStepsAst} from './animation_ast';
import {StylesCollection} from './styles_collection';
const _INITIAL_KEYFRAME = 0;
@ -110,9 +111,12 @@ function _parseAnimationStateTransition(
errors: AnimationParseError[]): AnimationStateTransitionAst {
const styles = new StylesCollection();
const transitionExprs: AnimationStateTransitionExpression[] = [];
const transitionStates = transitionStateMetadata.stateChangeExpr.split(/\s*,\s*/);
const stateChangeExpr = transitionStateMetadata.stateChangeExpr;
const transitionStates: Array<Function|StaticSymbol|string> = typeof stateChangeExpr == 'string' ?
(<string>stateChangeExpr).split(/\s*,\s*/) :
[<Function|StaticSymbol>stateChangeExpr];
transitionStates.forEach(
expr => { transitionExprs.push(..._parseAnimationTransitionExpr(expr, errors)); });
expr => transitionExprs.push(..._parseAnimationTransitionExpr(expr, errors)));
const entry = _normalizeAnimationEntry(transitionStateMetadata.steps);
const animation = _normalizeStyleSteps(entry, stateStyles, schema, errors);
const animationAst = _parseTransitionAnimation(animation, 0, styles, stateStyles, errors);
@ -141,25 +145,32 @@ function _parseAnimationAlias(alias: string, errors: AnimationParseError[]): str
}
function _parseAnimationTransitionExpr(
eventStr: string, errors: AnimationParseError[]): AnimationStateTransitionExpression[] {
transitionValue: string | Function | StaticSymbol,
errors: AnimationParseError[]): AnimationStateTransitionExpression[] {
const expressions: AnimationStateTransitionExpression[] = [];
if (eventStr[0] == ':') {
eventStr = _parseAnimationAlias(eventStr, errors);
}
const match = eventStr.match(/^(\*|[-\w]+)\s*(<?[=-]>)\s*(\*|[-\w]+)$/);
if (!isPresent(match) || match.length < 4) {
errors.push(new AnimationParseError(`the provided ${eventStr} is not of a supported format`));
return expressions;
}
if (typeof transitionValue == 'string') {
let eventStr = <string>transitionValue;
if (eventStr[0] == ':') {
eventStr = _parseAnimationAlias(eventStr, errors);
}
const match = eventStr.match(/^(\*|[-\w]+)\s*(<?[=-]>)\s*(\*|[-\w]+)$/);
if (!isPresent(match) || match.length < 4) {
errors.push(new AnimationParseError(`the provided ${eventStr} is not of a supported format`));
return expressions;
}
const fromState = match[1];
const separator = match[2];
const toState = match[3];
expressions.push(new AnimationStateTransitionExpression(fromState, toState));
const fromState = match[1];
const separator = match[2];
const toState = match[3];
expressions.push(new AnimationStateTransitionExpression(fromState, toState));
const isFullAnyStateExpr = fromState == ANY_STATE && toState == ANY_STATE;
if (separator[0] == '<' && !isFullAnyStateExpr) {
expressions.push(new AnimationStateTransitionExpression(toState, fromState));
const isFullAnyStateExpr = fromState == ANY_STATE && toState == ANY_STATE;
if (separator[0] == '<' && !isFullAnyStateExpr) {
expressions.push(new AnimationStateTransitionExpression(toState, fromState));
}
} else {
expressions.push(
new AnimationStateTransitionFnExpression(<Function|StaticSymbol>transitionValue));
}
return expressions;
}

View File

@ -39,7 +39,11 @@ export class CompileAnimationStateDeclarationMetadata extends CompileAnimationSt
}
export class CompileAnimationStateTransitionMetadata extends CompileAnimationStateMetadata {
constructor(public stateChangeExpr: string, public steps: CompileAnimationMetadata) { super(); }
constructor(
public stateChangeExpr: string|StaticSymbol|((stateA: string, stateB: string) => boolean),
public steps: CompileAnimationMetadata) {
super();
}
}
export abstract class CompileAnimationMetadata {}

View File

@ -48,7 +48,11 @@ export class AnimationStateDeclarationMetadata extends AnimationStateMetadata {
* @experimental Animation support is experimental.
*/
export class AnimationStateTransitionMetadata extends AnimationStateMetadata {
constructor(public stateChangeExpr: string, public steps: AnimationMetadata) { super(); }
constructor(
public stateChangeExpr: string|((fromState: string, toState: string) => boolean),
public steps: AnimationMetadata) {
super();
}
}
/**
@ -471,6 +475,10 @@ export function keyframes(steps: AnimationStyleMetadata[]): AnimationKeyframesSe
* which consists
* of two known states (use an asterix (`*`) to refer to a dynamic starting and/or ending state).
*
* A function can also be provided as the `stateChangeExpr` argument for a transition and this
* function will be executed each time a state change occurs. If the value returned within the
* function is true then the associated animation will be run.
*
* Animation transitions are placed within an {@link trigger animation trigger}. For an transition
* to animate to
* a state value and persist its styles then one or more {@link state animation states} is expected
@ -511,6 +519,12 @@ export function keyframes(steps: AnimationStyleMetadata[]): AnimationKeyframesSe
*
* // this will capture a state change between any states
* transition("* => *", animate("1s 0s")),
*
* // you can also go full out and include a function
* transition((fromState, toState) => {
* // when `true` then it will allow the animation below to be invoked
* return fromState == "off" && toState == "on";
* }, animate("1s 0s"))
* ])
* ```
*
@ -562,8 +576,9 @@ export function keyframes(steps: AnimationStyleMetadata[]): AnimationKeyframesSe
*
* @experimental Animation support is experimental.
*/
export function transition(stateChangeExpr: string, steps: AnimationMetadata | AnimationMetadata[]):
AnimationStateTransitionMetadata {
export function transition(
stateChangeExpr: string | ((fromState: string, toState: string) => boolean),
steps: AnimationMetadata | AnimationMetadata[]): AnimationStateTransitionMetadata {
const animationData = Array.isArray(steps) ? new AnimationSequenceMetadata(steps) : steps;
return new AnimationStateTransitionMetadata(stateChangeExpr, animationData);
}

View File

@ -156,6 +156,69 @@ function declareTests({useJit}: {useJit: boolean}) {
expect(kf[1]).toEqual([1, {'backgroundColor': 'blue'}]);
}));
it('should allow a transition to be a user-defined function', fakeAsync(() => {
TestBed.overrideComponent(DummyIfCmp, {
set: {
template: `
<div *ngIf="exp" [@myAnimation]="exp"></div>
`,
animations: [trigger(
'myAnimation',
[
transition(figureItOut, [animate(1000, style({'backgroundColor': 'blue'}))]),
transition(
'* => *', [animate(1000, style({'backgroundColor': 'black'}))])
])]
}
});
const log: string[] = [];
function figureItOut(stateA: string, stateB: string): boolean {
log.push(`${stateA} => ${stateB}`);
return ['one', 'three', 'five'].indexOf(stateB) >= 0;
}
function assertAnimatedToFirstTransition(animation: any, firstState: boolean) {
const expectedColor = firstState ? 'blue' : 'black';
expect(animation['keyframeLookup'][1]).toEqual([
1, {'backgroundColor': expectedColor}
]);
}
const driver = TestBed.get(AnimationDriver) as MockAnimationDriver;
const fixture = TestBed.createComponent(DummyIfCmp);
const cmp = fixture.componentInstance;
cmp.exp = 'one';
fixture.detectChanges();
flushMicrotasks();
assertAnimatedToFirstTransition(driver.log.pop(), true);
expect(log.pop()).toEqual('void => one');
cmp.exp = 'two';
fixture.detectChanges();
flushMicrotasks();
assertAnimatedToFirstTransition(driver.log.pop(), false);
expect(log.pop()).toEqual('one => two');
cmp.exp = 'three';
fixture.detectChanges();
flushMicrotasks();
assertAnimatedToFirstTransition(driver.log.pop(), true);
expect(log.pop()).toEqual('two => three');
cmp.exp = 'four';
fixture.detectChanges();
flushMicrotasks();
assertAnimatedToFirstTransition(driver.log.pop(), false);
expect(log.pop()).toEqual('three => four');
cmp.exp = 'five';
fixture.detectChanges();
flushMicrotasks();
assertAnimatedToFirstTransition(driver.log.pop(), true);
expect(log.pop()).toEqual('four => five');
}));
it('should throw an error when a provided offset for an animation step if an offset value is greater than 1',
fakeAsync(() => {
TestBed.overrideComponent(DummyIfCmp, {

View File

@ -90,9 +90,9 @@ export declare abstract class AnimationStateMetadata {
/** @experimental */
export declare class AnimationStateTransitionMetadata extends AnimationStateMetadata {
stateChangeExpr: string;
stateChangeExpr: string | ((fromState: string, toState: string) => boolean);
steps: AnimationMetadata;
constructor(stateChangeExpr: string, steps: AnimationMetadata);
constructor(stateChangeExpr: string | ((fromState: string, toState: string) => boolean), steps: AnimationMetadata);
}
/** @experimental */
@ -922,7 +922,7 @@ export interface TrackByFn {
}
/** @experimental */
export declare function transition(stateChangeExpr: string, steps: AnimationMetadata | AnimationMetadata[]): AnimationStateTransitionMetadata;
export declare function transition(stateChangeExpr: string | ((fromState: string, toState: string) => boolean), steps: AnimationMetadata | AnimationMetadata[]): AnimationStateTransitionMetadata;
/** @experimental */
export declare const TRANSLATIONS: OpaqueToken;