Skip to content

Commit

Permalink
fix(animations): support function types in transitions
Browse files Browse the repository at this point in the history
Closes #13538
Closes #13537
  • Loading branch information
matsko committed Dec 17, 2016
1 parent 0e3981a commit 5137ebc
Show file tree
Hide file tree
Showing 7 changed files with 131 additions and 35 deletions.
6 changes: 5 additions & 1 deletion modules/@angular/compiler-cli/integrationtest/src/animate.ts
Expand Up @@ -8,6 +8,10 @@

import {AUTO_STYLE, Component, animate, state, style, transition, trigger} from '@angular/core';

function anyToAny(stateA: string, stateB: string): boolean {
return true;
}

@Component({
selector: 'animate-cmp',
animations: [trigger(
Expand All @@ -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(500))
])],
template: `
<button (click)="setAsOpen()">Open</button>
Expand Down
4 changes: 4 additions & 0 deletions modules/@angular/compiler/src/animation/animation_ast.ts
Expand Up @@ -49,6 +49,10 @@ export class AnimationStateTransitionExpression {
constructor(public fromState: string, public toState: string) {}
}

export class AnimationStateTransitionFnExpression extends AnimationStateTransitionExpression {
constructor(public fn: Function) { super(null, null); }
}

export class AnimationStateTransitionAst extends AnimationStateAst {
constructor(
public stateChanges: AnimationStateTransitionExpression[],
Expand Down
29 changes: 18 additions & 11 deletions modules/@angular/compiler/src/animation/animation_compiler.ts
Expand Up @@ -7,12 +7,13 @@
*/


import {state} from '../../../core/src/animation/metadata';
import {isPresent} from '../facade/lang';
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) {}
Expand Down Expand Up @@ -162,16 +163,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.fromState != ANY_STATE) {
context.stateMap.registerState(stateChange.fromState);
}

if (stateChange.toState != ANY_STATE) {
context.stateMap.registerState(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.toState != ANY_STATE) {
context.stateMap.registerState(stateChange.toState);
}
}
});

Expand Down
47 changes: 28 additions & 19 deletions modules/@angular/compiler/src/animation/animation_parser.ts
Expand Up @@ -14,7 +14,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;
Expand Down Expand Up @@ -110,9 +110,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|string> = typeof stateChangeExpr == 'string' ?
(<string>stateChangeExpr).split(/\s*,\s*/) :
[<Function>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);
Expand Down Expand Up @@ -141,25 +144,31 @@ function _parseAnimationAlias(alias: string, errors: AnimationParseError[]): str
}

function _parseAnimationTransitionExpr(
eventStr: string, errors: AnimationParseError[]): AnimationStateTransitionExpression[] {
transitionValue: string | Function,
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>transitionValue));
}
return expressions;
}
Expand Down
6 changes: 5 additions & 1 deletion modules/@angular/compiler/src/compile_metadata.ts
Expand Up @@ -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|((stateA: string, stateB: string) => boolean),
public steps: CompileAnimationMetadata) {
super();
}
}

export abstract class CompileAnimationMetadata {}
Expand Down
11 changes: 8 additions & 3 deletions modules/@angular/core/src/animation/metadata.ts
Expand Up @@ -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|((stateA: string, stateB: string) => boolean),
public steps: AnimationMetadata) {
super();
}
}

/**
Expand Down Expand Up @@ -562,8 +566,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 | ((stateA: string, stateB: string) => boolean),
steps: AnimationMetadata | AnimationMetadata[]): AnimationStateTransitionMetadata {
const animationData = Array.isArray(steps) ? new AnimationSequenceMetadata(steps) : steps;
return new AnimationStateTransitionMetadata(stateChangeExpr, animationData);
}
Expand Down
63 changes: 63 additions & 0 deletions modules/@angular/core/test/animation/animation_integration_spec.ts
Expand Up @@ -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, {
Expand Down

0 comments on commit 5137ebc

Please sign in to comment.