feat(animations): support function types in transitions
Closes #13538 Closes #13537
This commit is contained in:
parent
3f67ab074a
commit
9211a22039
|
@ -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>
|
||||
|
|
|
@ -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[],
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue