fix(animations): make animations work in AOT (#14775)

This commit is contained in:
Matias Niemelä 2017-03-01 17:13:06 -08:00 committed by Igor Minar
parent 3168ef75da
commit 9560ad81b9
13 changed files with 119 additions and 60 deletions

View File

@ -66,7 +66,7 @@ export interface AnimationStateMetadata extends AnimationMetadata {
*/ */
export interface AnimationTransitionMetadata extends AnimationMetadata { export interface AnimationTransitionMetadata extends AnimationMetadata {
expr: string|((fromState: string, toState: string) => boolean); expr: string|((fromState: string, toState: string) => boolean);
animation: AnimationMetadata; animation: AnimationMetadata|AnimationMetadata[];
} }
/** /**
@ -86,8 +86,8 @@ export interface AnimationKeyframesSequenceMetadata extends AnimationMetadata {
* @experimental Animation support is experimental. * @experimental Animation support is experimental.
*/ */
export interface AnimationStyleMetadata extends AnimationMetadata { export interface AnimationStyleMetadata extends AnimationMetadata {
styles: {[key: string]: string | number}[]; styles: {[key: string]: string | number}|{[key: string]: string | number}[];
offset: number; offset?: number;
} }
/** /**
@ -341,27 +341,9 @@ export function sequence(steps: AnimationMetadata[]): AnimationSequenceMetadata
export function style( export function style(
tokens: {[key: string]: string | number} | tokens: {[key: string]: string | number} |
Array<{[key: string]: string | number}>): AnimationStyleMetadata { Array<{[key: string]: string | number}>): AnimationStyleMetadata {
let input: ɵStyleData[]; return {type: AnimationMetadataType.Style, styles: tokens};
let offset: number = null;
if (Array.isArray(tokens)) {
input = <ɵStyleData[]>tokens;
} else {
input = [<ɵStyleData>tokens];
}
input.forEach(entry => {
const entryOffset = (entry as ɵStyleData)['offset'];
if (entryOffset != null) {
offset = offset == null ? parseFloat(<string>entryOffset) : offset;
}
});
return _style(offset, input);
} }
function _style(offset: number, styles: ɵStyleData[]): AnimationStyleMetadata {
return {type: AnimationMetadataType.Style, styles: styles, offset: offset};
}
/** /**
* `state` is an animation-specific function that is designed to be used inside of Angular2's * `state` is an animation-specific function that is designed to be used inside of Angular2's
* animation DSL language. If this information is new, please navigate to the {@link * animation DSL language. If this information is new, please navigate to the {@link
@ -573,9 +555,5 @@ export function keyframes(steps: AnimationStyleMetadata[]): AnimationKeyframesSe
export function transition( export function transition(
stateChangeExpr: string | ((fromState: string, toState: string) => boolean), stateChangeExpr: string | ((fromState: string, toState: string) => boolean),
steps: AnimationMetadata | AnimationMetadata[]): AnimationTransitionMetadata { steps: AnimationMetadata | AnimationMetadata[]): AnimationTransitionMetadata {
return { return {type: AnimationMetadataType.Transition, expr: stateChangeExpr, animation: steps};
type: AnimationMetadataType.Transition,
expr: stateChangeExpr,
animation: Array.isArray(steps) ? sequence(steps) : <AnimationMetadata>steps
};
} }

View File

@ -5,12 +5,8 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * found in the LICENSE file at https://angular.io/license
*/ */
import {AUTO_STYLE, animate, state, style, transition, trigger} from '@angular/animations';
import {AUTO_STYLE, Component, animate, state, style, transition, trigger} from '@angular/core'; import {Component} from '@angular/core';
export function anyToAny(stateA: string, stateB: string): boolean {
return Math.random() != Math.random();
}
@Component({ @Component({
selector: 'animate-cmp', selector: 'animate-cmp',
@ -20,7 +16,7 @@ export function anyToAny(stateA: string, stateB: string): boolean {
state('*', style({height: AUTO_STYLE, color: 'black', borderColor: 'black'})), state('*', style({height: AUTO_STYLE, color: 'black', borderColor: 'black'})),
state('closed, void', style({height: '0px', color: 'maroon', borderColor: 'maroon'})), state('closed, void', style({height: '0px', color: 'maroon', borderColor: 'maroon'})),
state('open', style({height: AUTO_STYLE, borderColor: 'green', color: 'green'})), state('open', style({height: AUTO_STYLE, borderColor: 'green', color: 'green'})),
transition(anyToAny, animate('1s')), transition('* => *', animate(500)) transition('* => *', animate('1s'))
])], ])],
template: ` template: `
<button (click)="setAsOpen()">Open</button> <button (click)="setAsOpen()">Open</button>

View File

@ -8,9 +8,9 @@
import {ApplicationRef, NgModule} from '@angular/core'; import {ApplicationRef, NgModule} from '@angular/core';
import {FormsModule} from '@angular/forms'; import {FormsModule} from '@angular/forms';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
import {ServerModule} from '@angular/platform-server'; import {ServerModule} from '@angular/platform-server';
import {MdButtonModule} from '@angular2-material/button'; import {MdButtonModule} from '@angular2-material/button';
// Note: don't refer to third_party_src as we want to test that // Note: don't refer to third_party_src as we want to test that
// we can compile components from node_modules! // we can compile components from node_modules!
import {ThirdpartyModule} from 'third_party/module'; import {ThirdpartyModule} from 'third_party/module';
@ -50,6 +50,7 @@ import {CompForChildQuery, CompWithChildQuery, CompWithDirectiveChild, Directive
ComponentUsingThirdParty, ComponentUsingThirdParty,
], ],
imports: [ imports: [
NoopAnimationsModule,
ServerModule, ServerModule,
FormsModule, FormsModule,
MdButtonModule, MdButtonModule,

View File

@ -39,7 +39,7 @@ export interface AnimationStateMetadata extends AnimationMetadata {
*/ */
export interface AnimationTransitionMetadata extends AnimationMetadata { export interface AnimationTransitionMetadata extends AnimationMetadata {
expr: string|((fromState: string, toState: string) => boolean); expr: string|((fromState: string, toState: string) => boolean);
animation: AnimationMetadata; animation: AnimationMetadata|AnimationMetadata[];
} }
/** /**
@ -53,8 +53,8 @@ export interface AnimationKeyframesSequenceMetadata extends AnimationMetadata {
* @deprecated This symbol has moved. Please Import from @angular/animations instead! * @deprecated This symbol has moved. Please Import from @angular/animations instead!
*/ */
export interface AnimationStyleMetadata extends AnimationMetadata { export interface AnimationStyleMetadata extends AnimationMetadata {
styles: {[key: string]: string | number}[]; styles: {[key: string]: string | number}|{[key: string]: string | number}[];
offset: number; offset?: number;
} }
/** /**

View File

@ -278,7 +278,7 @@ export class AnimationTimelineVisitor implements AnimationDslVisitor {
const firstKeyframe = ast.steps[0]; const firstKeyframe = ast.steps[0];
let offsetGap = 0; let offsetGap = 0;
const containsOffsets = firstKeyframe.styles.find(styles => styles['offset'] >= 0); const containsOffsets = getOffset(firstKeyframe) != null;
if (!containsOffsets) { if (!containsOffsets) {
offsetGap = MAX_KEYFRAME_OFFSET / limit; offsetGap = MAX_KEYFRAME_OFFSET / limit;
} }
@ -291,8 +291,9 @@ export class AnimationTimelineVisitor implements AnimationDslVisitor {
ast.steps.forEach((step: AnimationStyleMetadata, i: number) => { ast.steps.forEach((step: AnimationStyleMetadata, i: number) => {
const normalizedStyles = normalizeStyles(step.styles); const normalizedStyles = normalizeStyles(step.styles);
const offset = containsOffsets ? <number>normalizedStyles['offset'] : const offset = containsOffsets ?
(i == limit ? MAX_KEYFRAME_OFFSET : i * offsetGap); (step.offset != null ? step.offset : parseFloat(normalizedStyles['offset'] as string)) :
(i == limit ? MAX_KEYFRAME_OFFSET : i * offsetGap);
innerTimeline.forwardTime(offset * duration); innerTimeline.forwardTime(offset * duration);
innerTimeline.setStyles(normalizedStyles); innerTimeline.setStyles(normalizedStyles);
}); });
@ -424,3 +425,22 @@ export class TimelineBuilder {
return createTimelineInstruction(finalKeyframes, this.duration, this.startTime, this.easing); return createTimelineInstruction(finalKeyframes, this.duration, this.startTime, this.easing);
} }
} }
function getOffset(ast: AnimationStyleMetadata): number {
let offset = ast.offset;
if (offset == null) {
const styles = ast.styles;
if (Array.isArray(styles)) {
for (let i = 0; i < styles.length; i++) {
const o = styles[i]['offset'] as number;
if (o != null) {
offset = o;
break;
}
}
} else {
offset = styles['offset'] as number;
}
}
return offset;
}

View File

@ -5,7 +5,8 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * found in the LICENSE file at https://angular.io/license
*/ */
import {AnimationMetadata, AnimationTransitionMetadata, ɵStyleData} from '@angular/animations'; import {AnimationMetadata, AnimationTransitionMetadata, sequence, ɵStyleData} from '@angular/animations';
import {buildAnimationKeyframes} from './animation_timeline_visitor'; import {buildAnimationKeyframes} from './animation_timeline_visitor';
import {TransitionMatcherFn} from './animation_transition_expr'; import {TransitionMatcherFn} from './animation_transition_expr';
import {AnimationTransitionInstruction, createTransitionInstruction} from './animation_transition_instruction'; import {AnimationTransitionInstruction, createTransitionInstruction} from './animation_transition_instruction';
@ -17,7 +18,10 @@ export class AnimationTransitionFactory {
private _triggerName: string, ast: AnimationTransitionMetadata, private _triggerName: string, ast: AnimationTransitionMetadata,
private matchFns: TransitionMatcherFn[], private matchFns: TransitionMatcherFn[],
private _stateStyles: {[stateName: string]: ɵStyleData}) { private _stateStyles: {[stateName: string]: ɵStyleData}) {
this._animationAst = ast.animation; const normalizedAst = Array.isArray(ast.animation) ?
sequence(<AnimationMetadata[]>ast.animation) :
<AnimationMetadata>ast.animation;
this._animationAst = normalizedAst;
} }
match(currentState: any, nextState: any): AnimationTransitionInstruction { match(currentState: any, nextState: any): AnimationTransitionInstruction {

View File

@ -93,9 +93,23 @@ class AnimationTriggerVisitor implements AnimationDslVisitor {
context.transitions.push(ast); context.transitions.push(ast);
} }
visitSequence(ast: AnimationSequenceMetadata, context: any) {} visitSequence(ast: AnimationSequenceMetadata, context: any) {
visitGroup(ast: AnimationGroupMetadata, context: any) {} // these values are not visited in this AST
visitAnimate(ast: AnimationAnimateMetadata, context: any) {} }
visitStyle(ast: AnimationStyleMetadata, context: any) {}
visitKeyframeSequence(ast: AnimationKeyframesSequenceMetadata, context: any) {} visitGroup(ast: AnimationGroupMetadata, context: any) {
// these values are not visited in this AST
}
visitAnimate(ast: AnimationAnimateMetadata, context: any) {
// these values are not visited in this AST
}
visitStyle(ast: AnimationStyleMetadata, context: any) {
// these values are not visited in this AST
}
visitKeyframeSequence(ast: AnimationKeyframesSequenceMetadata, context: any) {
// these values are not visited in this AST
}
} }

View File

@ -5,7 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * found in the LICENSE file at https://angular.io/license
*/ */
import {AnimateTimings, AnimationAnimateMetadata, AnimationGroupMetadata, AnimationKeyframesSequenceMetadata, AnimationMetadata, AnimationMetadataType, AnimationSequenceMetadata, AnimationStateMetadata, AnimationStyleMetadata, AnimationTransitionMetadata} from '@angular/animations'; import {AnimateTimings, AnimationAnimateMetadata, AnimationGroupMetadata, AnimationKeyframesSequenceMetadata, AnimationMetadata, AnimationMetadataType, AnimationSequenceMetadata, AnimationStateMetadata, AnimationStyleMetadata, AnimationTransitionMetadata, sequence} from '@angular/animations';
import {normalizeStyles, parseTimeExpression} from '../util'; import {normalizeStyles, parseTimeExpression} from '../util';
@ -52,7 +52,9 @@ export type StyleTimeTuple = {
* Otherwise an error will be thrown. * Otherwise an error will be thrown.
*/ */
export function validateAnimationSequence(ast: AnimationMetadata) { export function validateAnimationSequence(ast: AnimationMetadata) {
return new AnimationValidatorVisitor().validate(ast); const normalizedAst =
Array.isArray(ast) ? sequence(<AnimationMetadata[]>ast) : <AnimationMetadata>ast;
return new AnimationValidatorVisitor().validate(normalizedAst);
} }
export class AnimationValidatorVisitor implements AnimationDslVisitor { export class AnimationValidatorVisitor implements AnimationDslVisitor {
@ -62,9 +64,13 @@ export class AnimationValidatorVisitor implements AnimationDslVisitor {
return context.errors; return context.errors;
} }
visitState(ast: AnimationStateMetadata, context: any): any {} visitState(ast: AnimationStateMetadata, context: any): any {
// these values are not visited in this AST
}
visitTransition(ast: AnimationTransitionMetadata, context: any): any {} visitTransition(ast: AnimationTransitionMetadata, context: any): any {
// these values are not visited in this AST
}
visitSequence(ast: AnimationSequenceMetadata, context: AnimationValidatorContext): any { visitSequence(ast: AnimationSequenceMetadata, context: AnimationValidatorContext): any {
ast.steps.forEach(step => visitAnimationNode(this, step, context)); ast.steps.forEach(step => visitAnimationNode(this, step, context));

View File

@ -49,9 +49,13 @@ export function parseTimeExpression(exp: string | number, errors: string[]): Ani
return {duration, delay, easing}; return {duration, delay, easing};
} }
export function normalizeStyles(styles: ɵStyleData[]): ɵStyleData { export function normalizeStyles(styles: ɵStyleData | ɵStyleData[]): ɵStyleData {
const normalizedStyles: ɵStyleData = {}; const normalizedStyles: ɵStyleData = {};
styles.forEach(data => copyStyles(data, false, normalizedStyles)); if (Array.isArray(styles)) {
styles.forEach(data => copyStyles(data, false, normalizedStyles));
} else {
copyStyles(styles, false, normalizedStyles);
}
return normalizedStyles; return normalizedStyles;
} }

View File

@ -5,7 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * found in the LICENSE file at https://angular.io/license
*/ */
import {AUTO_STYLE, AnimationMetadata, animate, group, keyframes, sequence, style, ɵStyleData} from '@angular/animations'; import {AUTO_STYLE, AnimationMetadata, AnimationMetadataType, animate, group, keyframes, sequence, style, ɵStyleData} from '@angular/animations';
import {Animation} from '../../src/dsl/animation'; import {Animation} from '../../src/dsl/animation';
import {AnimationTimelineInstruction} from '../../src/dsl/animation_timeline_instruction'; import {AnimationTimelineInstruction} from '../../src/dsl/animation_timeline_instruction';
@ -374,6 +374,38 @@ export function main() {
expect(finalAnimatePlayerKeyframes[0]['height']).toEqual('300px'); expect(finalAnimatePlayerKeyframes[0]['height']).toEqual('300px');
expect(finalAnimatePlayerKeyframes[1]['height']).toEqual('500px'); expect(finalAnimatePlayerKeyframes[1]['height']).toEqual('500px');
}); });
it('should respect offsets if provided directly within the style data', () => {
const steps = animate(1000, keyframes([
style({opacity: 0, offset: 0}), style({opacity: .6, offset: .6}),
style({opacity: 1, offset: 1})
]));
const players = invokeAnimationSequence(steps);
expect(players.length).toEqual(1);
const player = players[0];
expect(player.keyframes).toEqual([
{opacity: 0, offset: 0}, {opacity: .6, offset: .6}, {opacity: 1, offset: 1}
]);
});
it('should respect offsets if provided directly within the style metadata type', () => {
const steps =
animate(1000, keyframes([
{type: AnimationMetadataType.Style, offset: 0, styles: {opacity: 0}},
{type: AnimationMetadataType.Style, offset: .4, styles: {opacity: .4}},
{type: AnimationMetadataType.Style, offset: 1, styles: {opacity: 1}},
]));
const players = invokeAnimationSequence(steps);
expect(players.length).toEqual(1);
const player = players[0];
expect(player.keyframes).toEqual([
{opacity: 0, offset: 0}, {opacity: .4, offset: .4}, {opacity: 1, offset: 1}
]);
});
}); });
describe('group()', () => { describe('group()', () => {

View File

@ -3,7 +3,7 @@ set -ex -o pipefail
# These ones can be `npm link`ed for fast development # These ones can be `npm link`ed for fast development
LINKABLE_PKGS=( LINKABLE_PKGS=(
$(pwd)/dist/packages-dist/{common,forms,core,compiler,compiler-cli,platform-{browser,server},platform-browser-dynamic,router,http} $(pwd)/dist/packages-dist/{common,forms,core,compiler,compiler-cli,platform-{browser,server},platform-browser-dynamic,router,http,animations}
$(pwd)/dist/tools/@angular/tsc-wrapped $(pwd)/dist/tools/@angular/tsc-wrapped
) )

View File

@ -81,15 +81,17 @@ export interface AnimationStateMetadata extends AnimationMetadata {
/** @experimental */ /** @experimental */
export interface AnimationStyleMetadata extends AnimationMetadata { export interface AnimationStyleMetadata extends AnimationMetadata {
offset: number; offset?: number;
styles: { styles: {
[key: string]: string | number; [key: string]: string | number;
} | {
[key: string]: string | number;
}[]; }[];
} }
/** @experimental */ /** @experimental */
export interface AnimationTransitionMetadata extends AnimationMetadata { export interface AnimationTransitionMetadata extends AnimationMetadata {
animation: AnimationMetadata; animation: AnimationMetadata | AnimationMetadata[];
expr: string | ((fromState: string, toState: string) => boolean); expr: string | ((fromState: string, toState: string) => boolean);
} }

View File

@ -70,9 +70,11 @@ export declare type AnimationStateTransitionMetadata = any;
/** @deprecated */ /** @deprecated */
export interface AnimationStyleMetadata extends AnimationMetadata { export interface AnimationStyleMetadata extends AnimationMetadata {
offset: number; offset?: number;
styles: { styles: {
[key: string]: string | number; [key: string]: string | number;
} | {
[key: string]: string | number;
}[]; }[];
} }
@ -91,7 +93,7 @@ export interface AnimationTransitionEvent {
/** @deprecated */ /** @deprecated */
export interface AnimationTransitionMetadata extends AnimationMetadata { export interface AnimationTransitionMetadata extends AnimationMetadata {
animation: AnimationMetadata; animation: AnimationMetadata | AnimationMetadata[];
expr: string | ((fromState: string, toState: string) => boolean); expr: string | ((fromState: string, toState: string) => boolean);
} }