diff --git a/modules/@angular/compiler-cli/integrationtest/src/animate.ts b/modules/@angular/compiler-cli/integrationtest/src/animate.ts
index e7f1955b4d..234a650c5a 100644
--- a/modules/@angular/compiler-cli/integrationtest/src/animate.ts
+++ b/modules/@angular/compiler-cli/integrationtest/src/animate.ts
@@ -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: `
diff --git a/modules/@angular/compiler/src/animation/animation_ast.ts b/modules/@angular/compiler/src/animation/animation_ast.ts
index aa4d10a869..f45a3bc596 100644
--- a/modules/@angular/compiler/src/animation/animation_ast.ts
+++ b/modules/@angular/compiler/src/animation/animation_ast.ts
@@ -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[],
diff --git a/modules/@angular/compiler/src/animation/animation_compiler.ts b/modules/@angular/compiler/src/animation/animation_compiler.ts
index 9bcf6df070..0824e6c80c 100644
--- a/modules/@angular/compiler/src/animation/animation_compiler.ts
+++ b/modules/@angular/compiler/src/animation/animation_compiler.ts
@@ -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);
+ }
}
});
diff --git a/modules/@angular/compiler/src/animation/animation_parser.ts b/modules/@angular/compiler/src/animation/animation_parser.ts
index 3cbf082103..020369ff14 100644
--- a/modules/@angular/compiler/src/animation/animation_parser.ts
+++ b/modules/@angular/compiler/src/animation/animation_parser.ts
@@ -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 = typeof stateChangeExpr == 'string' ?
+ (stateChangeExpr).split(/\s*,\s*/) :
+ [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 = 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(transitionValue));
}
return expressions;
}
diff --git a/modules/@angular/compiler/src/compile_metadata.ts b/modules/@angular/compiler/src/compile_metadata.ts
index 87b1ff572b..7d35b3196a 100644
--- a/modules/@angular/compiler/src/compile_metadata.ts
+++ b/modules/@angular/compiler/src/compile_metadata.ts
@@ -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 {}
diff --git a/modules/@angular/core/src/animation/metadata.ts b/modules/@angular/core/src/animation/metadata.ts
index 4ce32002b5..7e3b7b7850 100644
--- a/modules/@angular/core/src/animation/metadata.ts
+++ b/modules/@angular/core/src/animation/metadata.ts
@@ -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);
}
diff --git a/modules/@angular/core/test/animation/animation_integration_spec.ts b/modules/@angular/core/test/animation/animation_integration_spec.ts
index 7505fe9a86..a550e67654 100644
--- a/modules/@angular/core/test/animation/animation_integration_spec.ts
+++ b/modules/@angular/core/test/animation/animation_integration_spec.ts
@@ -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: `
+
+ `,
+ 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, {
diff --git a/tools/public_api_guard/core/index.d.ts b/tools/public_api_guard/core/index.d.ts
index 979d34eacb..65fad43c6a 100644
--- a/tools/public_api_guard/core/index.d.ts
+++ b/tools/public_api_guard/core/index.d.ts
@@ -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;