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;