feat(animations): introduce a wave of new animation features

This commit is contained in:
Matias Niemelä 2017-04-26 10:44:28 -07:00 committed by Jason Aden
parent d761059e4d
commit 16c8167886
55 changed files with 7732 additions and 2217 deletions

View File

@ -8,15 +8,19 @@
import {AnimationPlayer, AnimationTriggerMetadata} from '@angular/animations';
export abstract class AnimationEngine {
abstract registerTrigger(trigger: AnimationTriggerMetadata, name?: string): void;
abstract onInsert(element: any, domFn: () => any): void;
abstract onRemove(element: any, domFn: () => any): void;
abstract setProperty(element: any, property: string, value: any): void;
abstract registerTrigger(
componentId: string, namespaceId: string, hostElement: any, name: string,
metadata: AnimationTriggerMetadata): void;
abstract onInsert(namespaceId: string, element: any, parent: any, insertBefore: boolean): void;
abstract onRemove(namespaceId: string, element: any, context: any): void;
abstract setProperty(namespaceId: string, element: any, property: string, value: any): void;
abstract listen(
element: any, eventName: string, eventPhase: string,
namespaceId: string, element: any, eventName: string, eventPhase: string,
callback: (event: any) => any): () => any;
abstract flush(): void;
abstract destroy(namespaceId: string, context: any): void;
get activePlayers(): AnimationPlayer[] { throw new Error('...'); }
get queuedPlayers(): AnimationPlayer[] { throw new Error('...'); }
onRemovalComplete: (delegate: any, element: any) => void;
public players: AnimationPlayer[];
}

View File

@ -5,23 +5,19 @@
* 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 {AnimationMetadata, AnimationPlayer, AnimationStyleMetadata, sequence, ɵStyleData} from '@angular/animations';
import {AnimationDriver} from '../render/animation_driver';
import {DomAnimationEngine} from '../render/dom_animation_engine';
import {AnimationMetadata, AnimationOptions, ɵStyleData} from '@angular/animations';
import {normalizeStyles} from '../util';
import {Ast} from './animation_ast';
import {buildAnimationAst} from './animation_ast_builder';
import {buildAnimationTimelines} from './animation_timeline_builder';
import {AnimationTimelineInstruction} from './animation_timeline_instruction';
import {buildAnimationKeyframes} from './animation_timeline_visitor';
import {validateAnimationSequence} from './animation_validator_visitor';
import {AnimationStyleNormalizer} from './style_normalization/animation_style_normalizer';
import {ElementInstructionMap} from './element_instruction_map';
export class Animation {
private _animationAst: AnimationMetadata;
private _animationAst: Ast;
constructor(input: AnimationMetadata|AnimationMetadata[]) {
const ast =
Array.isArray(input) ? sequence(<AnimationMetadata[]>input) : <AnimationMetadata>input;
const errors = validateAnimationSequence(ast);
const errors: any[] = [];
const ast = buildAnimationAst(input, errors);
if (errors.length) {
const errorMessage = `animation validation failed:\n${errors.join("\n")}`;
throw new Error(errorMessage);
@ -30,26 +26,21 @@ export class Animation {
}
buildTimelines(
startingStyles: ɵStyleData|ɵStyleData[],
destinationStyles: ɵStyleData|ɵStyleData[]): AnimationTimelineInstruction[] {
element: any, startingStyles: ɵStyleData|ɵStyleData[],
destinationStyles: ɵStyleData|ɵStyleData[], options: AnimationOptions,
subInstructions?: ElementInstructionMap): AnimationTimelineInstruction[] {
const start = Array.isArray(startingStyles) ? normalizeStyles(startingStyles) :
<ɵStyleData>startingStyles;
const dest = Array.isArray(destinationStyles) ? normalizeStyles(destinationStyles) :
<ɵStyleData>destinationStyles;
return buildAnimationKeyframes(this._animationAst, start, dest);
}
// this is only used for development demo purposes for now
private create(
injector: any, element: any, startingStyles: ɵStyleData = {},
destinationStyles: ɵStyleData = {}): AnimationPlayer {
const instructions = this.buildTimelines(startingStyles, destinationStyles);
// note the code below is only here to make the tests happy (once the new renderer is
// within core then the code below will interact with Renderer.transition(...))
const driver: AnimationDriver = injector.get(AnimationDriver);
const normalizer: AnimationStyleNormalizer = injector.get(AnimationStyleNormalizer);
const engine = new DomAnimationEngine(driver, normalizer);
return engine.animateTimeline(element, instructions);
const errors: any = [];
subInstructions = subInstructions || new ElementInstructionMap();
const result = buildAnimationTimelines(
element, this._animationAst, start, dest, options, subInstructions, errors);
if (errors.length) {
const errorMessage = `animation building failed:\n${errors.join("\n")}`;
throw new Error(errorMessage);
}
return result;
}
}

View File

@ -0,0 +1,156 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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 {AnimateTimings, AnimationOptions, ɵStyleData} from '@angular/animations';
const EMPTY_ANIMATION_OPTIONS: AnimationOptions = {};
export interface AstVisitor {
visitTrigger(ast: TriggerAst, context: any): any;
visitState(ast: StateAst, context: any): any;
visitTransition(ast: TransitionAst, context: any): any;
visitSequence(ast: SequenceAst, context: any): any;
visitGroup(ast: GroupAst, context: any): any;
visitAnimate(ast: AnimateAst, context: any): any;
visitStyle(ast: StyleAst, context: any): any;
visitKeyframes(ast: KeyframesAst, context: any): any;
visitReference(ast: ReferenceAst, context: any): any;
visitAnimateChild(ast: AnimateChildAst, context: any): any;
visitAnimateRef(ast: AnimateRefAst, context: any): any;
visitQuery(ast: QueryAst, context: any): any;
visitStagger(ast: StaggerAst, context: any): any;
visitTiming(ast: TimingAst, context: any): any;
}
export abstract class Ast {
abstract visit(ast: AstVisitor, context: any): any;
public options: AnimationOptions = EMPTY_ANIMATION_OPTIONS;
get params(): {[name: string]: any}|null { return this.options['params'] || null; }
}
export class TriggerAst extends Ast {
public queryCount: number = 0;
public depCount: number = 0;
constructor(public name: string, public states: StateAst[], public transitions: TransitionAst[]) {
super();
}
visit(visitor: AstVisitor, context: any): any { return visitor.visitTrigger(this, context); }
}
export class StateAst extends Ast {
constructor(public name: string, public style: StyleAst) { super(); }
visit(visitor: AstVisitor, context: any): any { return visitor.visitState(this, context); }
}
export class TransitionAst extends Ast {
public queryCount: number = 0;
public depCount: number = 0;
constructor(
public matchers: ((fromState: string, toState: string) => boolean)[], public animation: Ast) {
super();
}
visit(visitor: AstVisitor, context: any): any { return visitor.visitTransition(this, context); }
}
export class SequenceAst extends Ast {
constructor(public steps: Ast[]) { super(); }
visit(visitor: AstVisitor, context: any): any { return visitor.visitSequence(this, context); }
}
export class GroupAst extends Ast {
constructor(public steps: Ast[]) { super(); }
visit(visitor: AstVisitor, context: any): any { return visitor.visitGroup(this, context); }
}
export class AnimateAst extends Ast {
constructor(public timings: TimingAst, public style: StyleAst|KeyframesAst) { super(); }
visit(visitor: AstVisitor, context: any): any { return visitor.visitAnimate(this, context); }
}
export class StyleAst extends Ast {
public isEmptyStep = false;
constructor(
public styles: (ɵStyleData|string)[], public easing: string|null,
public offset: number|null) {
super();
}
visit(visitor: AstVisitor, context: any): any { return visitor.visitStyle(this, context); }
}
export class KeyframesAst extends Ast {
constructor(public styles: StyleAst[]) { super(); }
visit(visitor: AstVisitor, context: any): any { return visitor.visitKeyframes(this, context); }
}
export class ReferenceAst extends Ast {
constructor(public animation: Ast) { super(); }
visit(visitor: AstVisitor, context: any): any { return visitor.visitReference(this, context); }
}
export class AnimateChildAst extends Ast {
constructor() { super(); }
visit(visitor: AstVisitor, context: any): any { return visitor.visitAnimateChild(this, context); }
}
export class AnimateRefAst extends Ast {
constructor(public animation: ReferenceAst) { super(); }
visit(visitor: AstVisitor, context: any): any { return visitor.visitAnimateRef(this, context); }
}
export class QueryAst extends Ast {
public originalSelector: string;
constructor(
public selector: string, public limit: number, public optional: boolean,
public includeSelf: boolean, public animation: Ast) {
super();
}
visit(visitor: AstVisitor, context: any): any { return visitor.visitQuery(this, context); }
}
export class StaggerAst extends Ast {
constructor(public timings: AnimateTimings, public animation: Ast) { super(); }
visit(visitor: AstVisitor, context: any): any { return visitor.visitStagger(this, context); }
}
export class TimingAst extends Ast {
constructor(
public duration: number, public delay: number = 0, public easing: string|null = null) {
super();
}
visit(visitor: AstVisitor, context: any): any { return visitor.visitTiming(this, context); }
}
export class DynamicTimingAst extends TimingAst {
constructor(public value: string) { super(0, 0, ''); }
visit(visitor: AstVisitor, context: any): any { return visitor.visitTiming(this, context); }
}

View File

@ -0,0 +1,475 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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 {AUTO_STYLE, AnimateTimings, AnimationAnimateChildMetadata, AnimationAnimateMetadata, AnimationAnimateRefMetadata, AnimationGroupMetadata, AnimationKeyframesSequenceMetadata, AnimationMetadata, AnimationMetadataType, AnimationOptions, AnimationQueryMetadata, AnimationQueryOptions, AnimationReferenceMetadata, AnimationSequenceMetadata, AnimationStaggerMetadata, AnimationStateMetadata, AnimationStyleMetadata, AnimationTransitionMetadata, AnimationTriggerMetadata, style, ɵStyleData} from '@angular/animations';
import {getOrSetAsInMap} from '../render/shared';
import {ENTER_SELECTOR, LEAVE_SELECTOR, NG_ANIMATING_SELECTOR, NG_TRIGGER_SELECTOR, copyObj, normalizeAnimationEntry, resolveTiming, validateStyleParams} from '../util';
import {AnimateAst, AnimateChildAst, AnimateRefAst, Ast, DynamicTimingAst, GroupAst, KeyframesAst, QueryAst, ReferenceAst, SequenceAst, StaggerAst, StateAst, StyleAst, TimingAst, TransitionAst, TriggerAst} from './animation_ast';
import {AnimationDslVisitor, visitAnimationNode} from './animation_dsl_visitor';
import {parseTransitionExpr} from './animation_transition_expr';
const SELF_TOKEN = ':self';
const SELF_TOKEN_REGEX = new RegExp(`\s*${SELF_TOKEN}\s*,?`, 'g');
/*
* [Validation]
* The visitor code below will traverse the animation AST generated by the animation verb functions
* (the output is a tree of objects) and attempt to perform a series of validations on the data. The
* following corner-cases will be validated:
*
* 1. Overlap of animations
* Given that a CSS property cannot be animated in more than one place at the same time, it's
* important that this behaviour is detected and validated. The way in which this occurs is that
* each time a style property is examined, a string-map containing the property will be updated with
* the start and end times for when the property is used within an animation step.
*
* If there are two or more parallel animations that are currently running (these are invoked by the
* group()) on the same element then the validator will throw an error. Since the start/end timing
* values are collected for each property then if the current animation step is animating the same
* property and its timing values fall anywhere into the window of time that the property is
* currently being animated within then this is what causes an error.
*
* 2. Timing values
* The validator will validate to see if a timing value of `duration delay easing` or
* `durationNumber` is valid or not.
*
* (note that upon validation the code below will replace the timing data with an object containing
* {duration,delay,easing}.
*
* 3. Offset Validation
* Each of the style() calls are allowed to have an offset value when placed inside of keyframes().
* Offsets within keyframes() are considered valid when:
*
* - No offsets are used at all
* - Each style() entry contains an offset value
* - Each offset is between 0 and 1
* - Each offset is greater to or equal than the previous one
*
* Otherwise an error will be thrown.
*/
export function buildAnimationAst(
metadata: AnimationMetadata | AnimationMetadata[], errors: any[]): Ast {
return new AnimationAstBuilderVisitor().build(metadata, errors);
}
const LEAVE_TOKEN = ':leave';
const LEAVE_TOKEN_REGEX = new RegExp(LEAVE_TOKEN, 'g');
const ENTER_TOKEN = ':enter';
const ENTER_TOKEN_REGEX = new RegExp(ENTER_TOKEN, 'g');
const ROOT_SELECTOR = '';
export class AnimationAstBuilderVisitor implements AnimationDslVisitor {
build(metadata: AnimationMetadata|AnimationMetadata[], errors: any[]): Ast {
const context = new AnimationAstBuilderContext(errors);
this._resetContextStyleTimingState(context);
return visitAnimationNode(this, normalizeAnimationEntry(metadata), context) as Ast;
}
private _resetContextStyleTimingState(context: AnimationAstBuilderContext) {
context.currentQuerySelector = ROOT_SELECTOR;
context.collectedStyles[ROOT_SELECTOR] = {};
context.currentTime = 0;
}
visitTrigger(metadata: AnimationTriggerMetadata, context: AnimationAstBuilderContext):
TriggerAst {
let queryCount = context.queryCount = 0;
let depCount = context.depCount = 0;
const states: StateAst[] = [];
const transitions: TransitionAst[] = [];
metadata.definitions.forEach(def => {
this._resetContextStyleTimingState(context);
if (def.type == AnimationMetadataType.State) {
const stateDef = def as AnimationStateMetadata;
const name = stateDef.name;
name.split(/\s*,\s*/).forEach(n => {
stateDef.name = n;
states.push(this.visitState(stateDef, context));
});
stateDef.name = name;
} else if (def.type == AnimationMetadataType.Transition) {
const transition = this.visitTransition(def as AnimationTransitionMetadata, context);
queryCount += transition.queryCount;
depCount += transition.depCount;
transitions.push(transition);
} else {
context.errors.push(
'only state() and transition() definitions can sit inside of a trigger()');
}
});
const ast = new TriggerAst(metadata.name, states, transitions);
ast.options = normalizeAnimationOptions(metadata.options);
ast.queryCount = queryCount;
ast.depCount = depCount;
return ast;
}
visitState(metadata: AnimationStateMetadata, context: AnimationAstBuilderContext): StateAst {
return new StateAst(metadata.name, this.visitStyle(metadata.styles, context));
}
visitTransition(metadata: AnimationTransitionMetadata, context: AnimationAstBuilderContext):
TransitionAst {
context.queryCount = 0;
context.depCount = 0;
const entry = visitAnimationNode(this, normalizeAnimationEntry(metadata.animation), context);
const matchers = parseTransitionExpr(metadata.expr, context.errors);
const ast = new TransitionAst(matchers, entry);
ast.options = normalizeAnimationOptions(metadata.options);
ast.queryCount = context.queryCount;
ast.depCount = context.depCount;
return ast;
}
visitSequence(metadata: AnimationSequenceMetadata, context: AnimationAstBuilderContext):
SequenceAst {
const ast = new SequenceAst(metadata.steps.map(s => visitAnimationNode(this, s, context)));
ast.options = normalizeAnimationOptions(metadata.options);
return ast;
}
visitGroup(metadata: AnimationGroupMetadata, context: AnimationAstBuilderContext): GroupAst {
const currentTime = context.currentTime;
let furthestTime = 0;
const steps = metadata.steps.map(step => {
context.currentTime = currentTime;
const innerAst = visitAnimationNode(this, step, context);
furthestTime = Math.max(furthestTime, context.currentTime);
return innerAst;
});
context.currentTime = furthestTime;
const ast = new GroupAst(steps);
ast.options = normalizeAnimationOptions(metadata.options);
return ast;
}
visitAnimate(metadata: AnimationAnimateMetadata, context: AnimationAstBuilderContext):
AnimateAst {
const timingAst = constructTimingAst(metadata.timings, context.errors);
context.currentAnimateTimings = timingAst;
let styles: StyleAst|KeyframesAst;
let styleMetadata: AnimationMetadata = metadata.styles ? metadata.styles : style({});
if (styleMetadata.type == AnimationMetadataType.Keyframes) {
styles = this.visitKeyframes(styleMetadata as AnimationKeyframesSequenceMetadata, context);
} else {
let styleMetadata = metadata.styles as AnimationStyleMetadata;
let isEmpty = false;
if (!styleMetadata) {
isEmpty = true;
const newStyleData: {[prop: string]: string | number} = {};
if (timingAst.easing) {
newStyleData['easing'] = timingAst.easing;
}
styleMetadata = style(newStyleData);
}
context.currentTime += timingAst.duration + timingAst.delay;
const styleAst = this.visitStyle(styleMetadata, context);
styleAst.isEmptyStep = isEmpty;
styles = styleAst;
}
context.currentAnimateTimings = null;
return new AnimateAst(timingAst, styles);
}
visitStyle(metadata: AnimationStyleMetadata, context: AnimationAstBuilderContext): StyleAst {
const ast = this._makeStyleAst(metadata, context);
this._validateStyleAst(ast, context);
return ast;
}
private _makeStyleAst(metadata: AnimationStyleMetadata, context: AnimationAstBuilderContext):
StyleAst {
const styles: (ɵStyleData | string)[] = [];
if (Array.isArray(metadata.styles)) {
(metadata.styles as(ɵStyleData | string)[]).forEach(styleTuple => {
if (typeof styleTuple == 'string') {
if (styleTuple == AUTO_STYLE) {
styles.push(styleTuple as string);
} else {
context.errors.push(`The provided style string value ${styleTuple} is not allowed.`);
}
} else {
styles.push(styleTuple as ɵStyleData);
}
})
} else {
styles.push(metadata.styles);
}
let collectedEasing: string|null = null;
styles.forEach(styleData => {
if (isObject(styleData)) {
const styleMap = styleData as ɵStyleData;
const easing = styleMap['easing'];
if (easing) {
collectedEasing = easing as string;
delete styleMap['easing'];
}
}
});
return new StyleAst(styles, collectedEasing, metadata.offset);
}
private _validateStyleAst(ast: StyleAst, context: AnimationAstBuilderContext): void {
const timings = context.currentAnimateTimings;
let endTime = context.currentTime;
let startTime = context.currentTime;
if (timings && startTime > 0) {
startTime -= timings.duration + timings.delay;
}
ast.styles.forEach(tuple => {
if (typeof tuple == 'string') return;
Object.keys(tuple).forEach(prop => {
const collectedStyles = context.collectedStyles[context.currentQuerySelector !];
const collectedEntry = collectedStyles[prop];
let updateCollectedStyle = true;
if (collectedEntry) {
if (startTime != endTime && startTime >= collectedEntry.startTime &&
endTime <= collectedEntry.endTime) {
context.errors.push(
`The CSS property "${prop}" that exists between the times of "${collectedEntry.startTime}ms" and "${collectedEntry.endTime}ms" is also being animated in a parallel animation between the times of "${startTime}ms" and "${endTime}ms"`);
updateCollectedStyle = false;
}
// we always choose the smaller start time value since we
// want to have a record of the entire animation window where
// the style property is being animated in between
startTime = collectedEntry.startTime;
}
if (updateCollectedStyle) {
collectedStyles[prop] = {startTime, endTime};
}
if (context.options) {
validateStyleParams(tuple[prop], context.options, context.errors);
}
});
});
}
visitKeyframes(metadata: AnimationKeyframesSequenceMetadata, context: AnimationAstBuilderContext):
KeyframesAst {
if (!context.currentAnimateTimings) {
context.errors.push(`keyframes() must be placed inside of a call to animate()`);
return new KeyframesAst([]);
}
const MAX_KEYFRAME_OFFSET = 1;
let totalKeyframesWithOffsets = 0;
const offsets: number[] = [];
let offsetsOutOfOrder = false;
let keyframesOutOfRange = false;
let previousOffset: number = 0;
const keyframes: StyleAst[] = metadata.steps.map(styles => {
const style = this._makeStyleAst(styles, context);
let offsetVal: number|null =
style.offset != null ? style.offset : consumeOffset(style.styles);
let offset: number = 0;
if (offsetVal != null) {
totalKeyframesWithOffsets++;
offset = style.offset = offsetVal;
}
keyframesOutOfRange = keyframesOutOfRange || offset < 0 || offset > 1;
offsetsOutOfOrder = offsetsOutOfOrder || offset < previousOffset;
previousOffset = offset;
offsets.push(offset);
return style;
});
if (keyframesOutOfRange) {
context.errors.push(`Please ensure that all keyframe offsets are between 0 and 1`);
}
if (offsetsOutOfOrder) {
context.errors.push(`Please ensure that all keyframe offsets are in order`);
}
const length = metadata.steps.length;
let generatedOffset = 0;
if (totalKeyframesWithOffsets > 0 && totalKeyframesWithOffsets < length) {
context.errors.push(`Not all style() steps within the declared keyframes() contain offsets`);
} else if (totalKeyframesWithOffsets == 0) {
generatedOffset = MAX_KEYFRAME_OFFSET / (length - 1);
}
const limit = length - 1;
const currentTime = context.currentTime;
const currentAnimateTimings = context.currentAnimateTimings !;
const animateDuration = currentAnimateTimings.duration;
keyframes.forEach((kf, i) => {
const offset = generatedOffset > 0 ? (i == limit ? 1 : (generatedOffset * i)) : offsets[i];
const durationUpToThisFrame = offset * animateDuration;
context.currentTime = currentTime + currentAnimateTimings.delay + durationUpToThisFrame;
currentAnimateTimings.duration = durationUpToThisFrame;
this._validateStyleAst(kf, context);
kf.offset = offset;
});
return new KeyframesAst(keyframes);
}
visitReference(metadata: AnimationReferenceMetadata, context: AnimationAstBuilderContext):
ReferenceAst {
const entry = visitAnimationNode(this, normalizeAnimationEntry(metadata.animation), context);
const ast = new ReferenceAst(entry);
ast.options = normalizeAnimationOptions(metadata.options);
return ast;
}
visitAnimateChild(metadata: AnimationAnimateChildMetadata, context: AnimationAstBuilderContext):
AnimateChildAst {
context.depCount++;
const ast = new AnimateChildAst();
ast.options = normalizeAnimationOptions(metadata.options);
return ast;
}
visitAnimateRef(metadata: AnimationAnimateRefMetadata, context: AnimationAstBuilderContext):
AnimateRefAst {
const animation = this.visitReference(metadata.animation, context);
const ast = new AnimateRefAst(animation);
ast.options = normalizeAnimationOptions(metadata.options);
return ast;
}
visitQuery(metadata: AnimationQueryMetadata, context: AnimationAstBuilderContext): QueryAst {
const parentSelector = context.currentQuerySelector !;
const options = (metadata.options || {}) as AnimationQueryOptions;
context.queryCount++;
context.currentQuery = metadata;
const [selector, includeSelf] = normalizeSelector(metadata.selector);
context.currentQuerySelector =
parentSelector.length ? (parentSelector + ' ' + selector) : selector;
getOrSetAsInMap(context.collectedStyles, context.currentQuerySelector, {});
const entry = visitAnimationNode(this, normalizeAnimationEntry(metadata.animation), context);
context.currentQuery = null;
context.currentQuerySelector = parentSelector;
const ast = new QueryAst(selector, options.limit || 0, !!options.optional, includeSelf, entry);
ast.originalSelector = metadata.selector;
ast.options = normalizeAnimationOptions(metadata.options);
return ast;
}
visitStagger(metadata: AnimationStaggerMetadata, context: AnimationAstBuilderContext):
StaggerAst {
if (!context.currentQuery) {
context.errors.push(`stagger() can only be used inside of query()`);
}
const timings = metadata.timings === 'full' ?
{duration: 0, delay: 0, easing: 'full'} :
resolveTiming(metadata.timings, context.errors, true);
const animation =
visitAnimationNode(this, normalizeAnimationEntry(metadata.animation), context);
return new StaggerAst(timings, animation);
}
}
function normalizeSelector(selector: string): [string, boolean] {
const hasAmpersand = selector.split(/\s*,\s*/).find(token => token == SELF_TOKEN) ? true : false;
if (hasAmpersand) {
selector = selector.replace(SELF_TOKEN_REGEX, '');
}
selector = selector.replace(ENTER_TOKEN_REGEX, ENTER_SELECTOR)
.replace(LEAVE_TOKEN_REGEX, LEAVE_SELECTOR)
.replace(/@\*/g, NG_TRIGGER_SELECTOR)
.replace(/@\w+/g, match => NG_TRIGGER_SELECTOR + '-' + match.substr(1))
.replace(/:animating/g, NG_ANIMATING_SELECTOR);
return [selector, hasAmpersand];
}
function normalizeParams(obj: {[key: string]: any} | any): {[key: string]: any}|null {
return obj ? copyObj(obj) : null;
}
export type StyleTimeTuple = {
startTime: number; endTime: number;
};
export class AnimationAstBuilderContext {
public queryCount: number = 0;
public depCount: number = 0;
public currentTransition: AnimationTransitionMetadata|null = null;
public currentQuery: AnimationQueryMetadata|null = null;
public currentQuerySelector: string|null = null;
public currentAnimateTimings: TimingAst|null = null;
public currentTime: number = 0;
public collectedStyles: {[selectorName: string]: {[propName: string]: StyleTimeTuple}} = {};
public options: AnimationOptions|null = null;
constructor(public errors: any[]) {}
}
function consumeOffset(styles: ɵStyleData | string | (ɵStyleData | string)[]): number|null {
if (typeof styles == 'string') return null;
let offset: number|null = null;
if (Array.isArray(styles)) {
styles.forEach(styleTuple => {
if (isObject(styleTuple) && styleTuple.hasOwnProperty('offset')) {
const obj = styleTuple as ɵStyleData;
offset = parseFloat(obj['offset'] as string);
delete obj['offset'];
}
});
} else if (isObject(styles) && styles.hasOwnProperty('offset')) {
const obj = styles as ɵStyleData;
offset = parseFloat(obj['offset'] as string);
delete obj['offset'];
}
return offset;
}
function isObject(value: any): boolean {
return !Array.isArray(value) && typeof value == 'object';
}
function constructTimingAst(value: string | number | AnimateTimings, errors: any[]) {
let timings: AnimateTimings|null = null;
if (value.hasOwnProperty('duration')) {
timings = value as AnimateTimings;
} else if (typeof value == 'number') {
const duration = resolveTiming(value as number, errors).duration;
return new TimingAst(value as number, 0, '');
}
const strValue = value as string;
const isDynamic = strValue.split(/\s+/).some(v => v.charAt(0) == '{' && v.charAt(1) == '{');
if (isDynamic) {
return new DynamicTimingAst(strValue);
}
timings = timings || resolveTiming(strValue, errors);
return new TimingAst(timings.duration, timings.delay, timings.easing);
}
function normalizeAnimationOptions(options: AnimationOptions | null): AnimationOptions {
if (options) {
options = copyObj(options);
if (options['params']) {
options['params'] = normalizeParams(options['params']) !;
}
} else {
options = {};
}
return options;
}

View File

@ -5,35 +5,53 @@
* 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 {AnimationAnimateMetadata, AnimationGroupMetadata, AnimationKeyframesSequenceMetadata, AnimationMetadata, AnimationMetadataType, AnimationSequenceMetadata, AnimationStateMetadata, AnimationStyleMetadata, AnimationTransitionMetadata} from '@angular/animations';
import {AnimationAnimateChildMetadata, AnimationAnimateMetadata, AnimationAnimateRefMetadata, AnimationGroupMetadata, AnimationKeyframesSequenceMetadata, AnimationMetadata, AnimationMetadataType, AnimationQueryMetadata, AnimationReferenceMetadata, AnimationSequenceMetadata, AnimationStaggerMetadata, AnimationStateMetadata, AnimationStyleMetadata, AnimationTransitionMetadata, AnimationTriggerMetadata} from '@angular/animations';
export interface AnimationDslVisitor {
visitTrigger(ast: AnimationTriggerMetadata, context: any): any;
visitState(ast: AnimationStateMetadata, context: any): any;
visitTransition(ast: AnimationTransitionMetadata, context: any): any;
visitSequence(ast: AnimationSequenceMetadata, context: any): any;
visitGroup(ast: AnimationGroupMetadata, context: any): any;
visitAnimate(ast: AnimationAnimateMetadata, context: any): any;
visitStyle(ast: AnimationStyleMetadata, context: any): any;
visitKeyframeSequence(ast: AnimationKeyframesSequenceMetadata, context: any): any;
visitKeyframes(ast: AnimationKeyframesSequenceMetadata, context: any): any;
visitReference(ast: AnimationReferenceMetadata, context: any): any;
visitAnimateChild(ast: AnimationAnimateChildMetadata, context: any): any;
visitAnimateRef(ast: AnimationAnimateRefMetadata, context: any): any;
visitQuery(ast: AnimationQueryMetadata, context: any): any;
visitStagger(ast: AnimationStaggerMetadata, context: any): any;
}
export function visitAnimationNode(
visitor: AnimationDslVisitor, node: AnimationMetadata, context: any) {
switch (node.type) {
case AnimationMetadataType.Trigger:
return visitor.visitTrigger(node as AnimationTriggerMetadata, context);
case AnimationMetadataType.State:
return visitor.visitState(<AnimationStateMetadata>node, context);
return visitor.visitState(node as AnimationStateMetadata, context);
case AnimationMetadataType.Transition:
return visitor.visitTransition(<AnimationTransitionMetadata>node, context);
return visitor.visitTransition(node as AnimationTransitionMetadata, context);
case AnimationMetadataType.Sequence:
return visitor.visitSequence(<AnimationSequenceMetadata>node, context);
return visitor.visitSequence(node as AnimationSequenceMetadata, context);
case AnimationMetadataType.Group:
return visitor.visitGroup(<AnimationGroupMetadata>node, context);
return visitor.visitGroup(node as AnimationGroupMetadata, context);
case AnimationMetadataType.Animate:
return visitor.visitAnimate(<AnimationAnimateMetadata>node, context);
case AnimationMetadataType.KeyframeSequence:
return visitor.visitKeyframeSequence(<AnimationKeyframesSequenceMetadata>node, context);
return visitor.visitAnimate(node as AnimationAnimateMetadata, context);
case AnimationMetadataType.Keyframes:
return visitor.visitKeyframes(node as AnimationKeyframesSequenceMetadata, context);
case AnimationMetadataType.Style:
return visitor.visitStyle(<AnimationStyleMetadata>node, context);
return visitor.visitStyle(node as AnimationStyleMetadata, context);
case AnimationMetadataType.Reference:
return visitor.visitReference(node as AnimationReferenceMetadata, context);
case AnimationMetadataType.AnimateChild:
return visitor.visitAnimateChild(node as AnimationAnimateChildMetadata, context);
case AnimationMetadataType.AnimateRef:
return visitor.visitAnimateRef(node as AnimationAnimateRefMetadata, context);
case AnimationMetadataType.Query:
return visitor.visitQuery(node as AnimationQueryMetadata, context);
case AnimationMetadataType.Stagger:
return visitor.visitStagger(node as AnimationStaggerMetadata, context);
default:
throw new Error(`Unable to resolve animation metadata node #${node.type}`);
}

View File

@ -0,0 +1,873 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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 {AUTO_STYLE, AnimateTimings, AnimationOptions, AnimationQueryOptions, ɵPRE_STYLE as PRE_STYLE, ɵStyleData} from '@angular/animations';
import {copyObj, copyStyles, interpolateParams, iteratorToArray, resolveTiming, resolveTimingValue} from '../util';
import {AnimateAst, AnimateChildAst, AnimateRefAst, Ast, AstVisitor, DynamicTimingAst, GroupAst, KeyframesAst, QueryAst, ReferenceAst, SequenceAst, StaggerAst, StateAst, StyleAst, TimingAst, TransitionAst, TriggerAst} from './animation_ast';
import {AnimationTimelineInstruction, createTimelineInstruction} from './animation_timeline_instruction';
import {ElementInstructionMap} from './element_instruction_map';
const ONE_FRAME_IN_MILLISECONDS = 1;
/*
* The code within this file aims to generate web-animations-compatible keyframes from Angular's
* animation DSL code.
*
* The code below will be converted from:
*
* ```
* sequence([
* style({ opacity: 0 }),
* animate(1000, style({ opacity: 0 }))
* ])
* ```
*
* To:
* ```
* keyframes = [{ opacity: 0, offset: 0 }, { opacity: 1, offset: 1 }]
* duration = 1000
* delay = 0
* easing = ''
* ```
*
* For this operation to cover the combination of animation verbs (style, animate, group, etc...) a
* combination of prototypical inheritance, AST traversal and merge-sort-like algorithms are used.
*
* [AST Traversal]
* Each of the animation verbs, when executed, will return an string-map object representing what
* type of action it is (style, animate, group, etc...) and the data associated with it. This means
* that when functional composition mix of these functions is evaluated (like in the example above)
* then it will end up producing a tree of objects representing the animation itself.
*
* When this animation object tree is processed by the visitor code below it will visit each of the
* verb statements within the visitor. And during each visit it will build the context of the
* animation keyframes by interacting with the `TimelineBuilder`.
*
* [TimelineBuilder]
* This class is responsible for tracking the styles and building a series of keyframe objects for a
* timeline between a start and end time. The builder starts off with an initial timeline and each
* time the AST comes across a `group()`, `keyframes()` or a combination of the two wihtin a
* `sequence()` then it will generate a sub timeline for each step as well as a new one after
* they are complete.
*
* As the AST is traversed, the timing state on each of the timelines will be incremented. If a sub
* timeline was created (based on one of the cases above) then the parent timeline will attempt to
* merge the styles used within the sub timelines into itself (only with group() this will happen).
* This happens with a merge operation (much like how the merge works in mergesort) and it will only
* copy the most recently used styles from the sub timelines into the parent timeline. This ensures
* that if the styles are used later on in another phase of the animation then they will be the most
* up-to-date values.
*
* [How Missing Styles Are Updated]
* Each timeline has a `backFill` property which is responsible for filling in new styles into
* already processed keyframes if a new style shows up later within the animation sequence.
*
* ```
* sequence([
* style({ width: 0 }),
* animate(1000, style({ width: 100 })),
* animate(1000, style({ width: 200 })),
* animate(1000, style({ width: 300 }))
* animate(1000, style({ width: 400, height: 400 })) // notice how `height` doesn't exist anywhere
* else
* ])
* ```
*
* What is happening here is that the `height` value is added later in the sequence, but is missing
* from all previous animation steps. Therefore when a keyframe is created it would also be missing
* from all previous keyframes up until where it is first used. For the timeline keyframe generation
* to properly fill in the style it will place the previous value (the value from the parent
* timeline) or a default value of `*` into the backFill object. Given that each of the keyframe
* styles are objects that prototypically inhert from the backFill object, this means that if a
* value is added into the backFill then it will automatically propagate any missing values to all
* keyframes. Therefore the missing `height` value will be properly filled into the already
* processed keyframes.
*
* When a sub-timeline is created it will have its own backFill property. This is done so that
* styles present within the sub-timeline do not accidentally seep into the previous/future timeline
* keyframes
*
* (For prototypically-inherited contents to be detected a `for(i in obj)` loop must be used.)
*
* [Validation]
* The code in this file is not responsible for validation. That functionality happens with within
* the `AnimationValidatorVisitor` code.
*/
export function buildAnimationTimelines(
rootElement: any, ast: Ast, startingStyles: ɵStyleData = {}, finalStyles: ɵStyleData = {},
options: AnimationOptions, subInstructions?: ElementInstructionMap,
errors: any[] = []): AnimationTimelineInstruction[] {
return new AnimationTimelineBuilderVisitor().buildKeyframes(
rootElement, ast, startingStyles, finalStyles, options, subInstructions, errors);
}
export declare type StyleAtTime = {
time: number; value: string | number;
};
const DEFAULT_NOOP_PREVIOUS_NODE = <Ast>{};
export class AnimationTimelineContext {
public parentContext: AnimationTimelineContext|null = null;
public currentTimeline: TimelineBuilder;
public currentAnimateTimings: AnimateTimings|null = null;
public previousNode: Ast = DEFAULT_NOOP_PREVIOUS_NODE;
public subContextCount = 0;
public options: AnimationOptions = {};
public currentQueryIndex: number = 0;
public currentQueryTotal: number = 0;
public currentStaggerTime: number = 0;
constructor(
public element: any, public subInstructions: ElementInstructionMap, public errors: any[],
public timelines: TimelineBuilder[], initialTimeline?: TimelineBuilder) {
this.currentTimeline = initialTimeline || new TimelineBuilder(element, 0);
timelines.push(this.currentTimeline);
}
get params() { return this.options.params; }
updateOptions(newOptions: AnimationOptions|null, skipIfExists?: boolean) {
if (!newOptions) return;
if (newOptions.duration != null) {
this.options.duration = resolveTimingValue(newOptions.duration);
}
if (newOptions.delay != null) {
this.options.delay = resolveTimingValue(newOptions.delay);
}
const newParams = newOptions.params;
if (newParams) {
let params: {[name: string]: any} = this.options && this.options.params !;
if (!params) {
params = this.options.params = {};
}
Object.keys(params).forEach(name => {
const value = params[name];
if (!skipIfExists || !newOptions.hasOwnProperty(name)) {
params[name] = value;
}
});
}
}
private _copyOptions() {
const options: AnimationOptions = {};
if (this.options) {
const oldParams = this.options.params;
if (oldParams) {
const params: {[name: string]: any} = options['params'] = {};
Object.keys(this.options.params).forEach(name => { params[name] = oldParams[name]; });
}
}
return options;
}
createSubContext(options: AnimationOptions|null = null, element?: any, newTime?: number):
AnimationTimelineContext {
const target = element || this.element;
const context = new AnimationTimelineContext(
target, this.subInstructions, this.errors, this.timelines,
this.currentTimeline.fork(target, newTime || 0));
context.previousNode = this.previousNode;
context.currentAnimateTimings = this.currentAnimateTimings;
context.options = this._copyOptions();
context.updateOptions(options);
context.currentQueryIndex = this.currentQueryIndex;
context.currentQueryTotal = this.currentQueryTotal;
context.parentContext = this;
this.subContextCount++;
return context;
}
transformIntoNewTimeline(newTime?: number) {
this.previousNode = DEFAULT_NOOP_PREVIOUS_NODE;
this.currentTimeline = this.currentTimeline.fork(this.element, newTime);
this.timelines.push(this.currentTimeline);
return this.currentTimeline;
}
appendInstructionToTimeline(
instruction: AnimationTimelineInstruction, duration: number|null,
delay: number|null): AnimateTimings {
const updatedTimings: AnimateTimings = {
duration: duration != null ? duration : instruction.duration,
delay: this.currentTimeline.currentTime + (delay != null ? delay : 0) + instruction.delay,
easing: ''
};
const builder = new SubTimelineBuilder(
instruction.element, instruction.keyframes, instruction.preStyleProps,
instruction.postStyleProps, updatedTimings, instruction.stretchStartingKeyframe);
this.timelines.push(builder);
return updatedTimings;
}
incrementTime(time: number) {
this.currentTimeline.forwardTime(this.currentTimeline.duration + time);
}
delayNextStep(delay: number) {
// negative delays are not yet supported
if (delay > 0) {
this.currentTimeline.delayNextStep(delay);
}
}
}
export class AnimationTimelineBuilderVisitor implements AstVisitor {
buildKeyframes(
rootElement: any, ast: Ast, startingStyles: ɵStyleData, finalStyles: ɵStyleData,
options: AnimationOptions, subInstructions?: ElementInstructionMap,
errors: any[] = []): AnimationTimelineInstruction[] {
subInstructions = subInstructions || new ElementInstructionMap();
const context = new AnimationTimelineContext(rootElement, subInstructions, errors, []);
context.options = options;
context.currentTimeline.setStyles([startingStyles], null, context.errors, options);
ast.visit(this, context);
// this checks to see if an actual animation happened
const timelines = context.timelines.filter(timeline => timeline.containsAnimation());
if (timelines.length && Object.keys(finalStyles).length) {
const tl = timelines[timelines.length - 1];
if (!tl.allowOnlyTimelineStyles()) {
tl.setStyles([finalStyles], null, context.errors, options);
}
}
return timelines.length ? timelines.map(timeline => timeline.buildKeyframes()) :
[createTimelineInstruction(rootElement, [], [], [], 0, 0, '', false)];
}
visitTrigger(ast: TriggerAst, context: AnimationTimelineContext): any {
// these values are not visited in this AST
}
visitState(ast: StateAst, context: AnimationTimelineContext): any {
// these values are not visited in this AST
}
visitTransition(ast: TransitionAst, context: AnimationTimelineContext): any {
// these values are not visited in this AST
}
visitAnimateChild(ast: AnimateChildAst, context: AnimationTimelineContext): any {
const elementInstructions = context.subInstructions.consume(context.element);
if (elementInstructions) {
const innerContext = context.createSubContext(ast.options);
const startTime = context.currentTimeline.currentTime;
const endTime = this._visitSubInstructions(elementInstructions, innerContext);
if (startTime != endTime) {
// we do this on the upper context because we created a sub context for
// the sub child animations
context.transformIntoNewTimeline(endTime);
}
}
context.previousNode = ast;
}
visitAnimateRef(ast: AnimateRefAst, context: AnimationTimelineContext): any {
const innerContext = context.createSubContext(ast.options);
innerContext.transformIntoNewTimeline();
this.visitReference(ast.animation, innerContext);
context.transformIntoNewTimeline(innerContext.currentTimeline.currentTime);
context.previousNode = ast;
}
private _visitSubInstructions(
instructions: AnimationTimelineInstruction[], context: AnimationTimelineContext): number {
const options = context.options;
const startTime = context.currentTimeline.currentTime;
let furthestTime = startTime;
// this is a special-case for when a user wants to skip a sub
// animation from being fired entirely.
const duration = options.duration != null ? resolveTimingValue(options.duration) : null;
const delay = options.delay != null ? resolveTimingValue(options.delay) : null;
if (duration !== 0) {
instructions.forEach(instruction => {
const instructionTimings =
context.appendInstructionToTimeline(instruction, duration, delay);
furthestTime =
Math.max(furthestTime, instructionTimings.duration + instructionTimings.delay);
});
}
return furthestTime;
}
visitReference(ast: ReferenceAst, context: AnimationTimelineContext) {
context.updateOptions(ast.options, true);
ast.animation.visit(this, context);
context.previousNode = ast;
}
visitSequence(ast: SequenceAst, context: AnimationTimelineContext) {
const subContextCount = context.subContextCount;
const options = ast.options;
if (options && (options.params || options.delay)) {
context.createSubContext(options);
context.transformIntoNewTimeline();
if (options.delay != null) {
if (context.previousNode instanceof StyleAst) {
context.currentTimeline.snapshotCurrentStyles();
context.previousNode = DEFAULT_NOOP_PREVIOUS_NODE;
}
const delay = resolveTimingValue(options.delay);
context.delayNextStep(delay);
}
}
if (ast.steps.length) {
ast.steps.forEach(s => s.visit(this, context));
// this is here just incase the inner steps only contain or end with a style() call
context.currentTimeline.applyStylesToKeyframe();
// this means that some animation function within the sequence
// ended up creating a sub timeline (which means the current
// timeline cannot overlap with the contents of the sequence)
if (context.subContextCount > subContextCount) {
context.transformIntoNewTimeline();
}
}
context.previousNode = ast;
}
visitGroup(ast: GroupAst, context: AnimationTimelineContext) {
const innerTimelines: TimelineBuilder[] = [];
let furthestTime = context.currentTimeline.currentTime;
const delay = ast.options && ast.options.delay ? resolveTimingValue(ast.options.delay) : 0;
ast.steps.forEach(s => {
const innerContext = context.createSubContext(ast.options);
if (delay) {
innerContext.delayNextStep(delay);
}
s.visit(this, innerContext);
furthestTime = Math.max(furthestTime, innerContext.currentTimeline.currentTime);
innerTimelines.push(innerContext.currentTimeline);
});
// this operation is run after the AST loop because otherwise
// if the parent timeline's collected styles were updated then
// it would pass in invalid data into the new-to-be forked items
innerTimelines.forEach(
timeline => context.currentTimeline.mergeTimelineCollectedStyles(timeline));
context.transformIntoNewTimeline(furthestTime);
context.previousNode = ast;
}
visitTiming(ast: TimingAst, context: AnimationTimelineContext): AnimateTimings {
if (ast instanceof DynamicTimingAst) {
const strValue = context.params ?
interpolateParams(ast.value, context.params, context.errors) :
ast.value.toString();
return resolveTiming(strValue, context.errors);
} else {
return {duration: ast.duration, delay: ast.delay, easing: ast.easing};
}
}
visitAnimate(ast: AnimateAst, context: AnimationTimelineContext) {
const timings = context.currentAnimateTimings = this.visitTiming(ast.timings, context);
const timeline = context.currentTimeline;
if (timings.delay) {
context.incrementTime(timings.delay);
timeline.snapshotCurrentStyles();
}
const style = ast.style;
if (style instanceof KeyframesAst) {
this.visitKeyframes(style, context);
} else {
context.incrementTime(timings.duration);
this.visitStyle(style as StyleAst, context);
timeline.applyStylesToKeyframe();
}
context.currentAnimateTimings = null;
context.previousNode = ast;
}
visitStyle(ast: StyleAst, context: AnimationTimelineContext) {
const timeline = context.currentTimeline;
const timings = context.currentAnimateTimings !;
// this is a special case for when a style() call
// directly follows an animate() call (but not inside of an animate() call)
if (!timings && timeline.getCurrentStyleProperties().length) {
timeline.forwardFrame();
}
const easing = (timings && timings.easing) || ast.easing;
if (ast.isEmptyStep) {
timeline.applyEmptyStep(easing);
} else {
timeline.setStyles(ast.styles, easing, context.errors, context.options);
}
context.previousNode = ast;
}
visitKeyframes(ast: KeyframesAst, context: AnimationTimelineContext) {
const currentAnimateTimings = context.currentAnimateTimings !;
const startTime = (context.currentTimeline !).duration;
const duration = currentAnimateTimings.duration;
const innerContext = context.createSubContext();
const innerTimeline = innerContext.currentTimeline;
innerTimeline.easing = currentAnimateTimings.easing;
ast.styles.forEach(step => {
const offset: number = step.offset || 0;
innerTimeline.forwardTime(offset * duration);
innerTimeline.setStyles(step.styles, step.easing, context.errors, context.options);
innerTimeline.applyStylesToKeyframe();
});
// this will ensure that the parent timeline gets all the styles from
// the child even if the new timeline below is not used
context.currentTimeline.mergeTimelineCollectedStyles(innerTimeline);
// we do this because the window between this timeline and the sub timeline
// should ensure that the styles within are exactly the same as they were before
context.transformIntoNewTimeline(startTime + duration);
context.previousNode = ast;
}
visitQuery(ast: QueryAst, context: AnimationTimelineContext) {
// in the event that the first step before this is a style step we need
// to ensure the styles are applied before the children are animated
const startTime = context.currentTimeline.currentTime;
const options = (ast.options || {}) as AnimationQueryOptions;
const delay = options.delay ? resolveTimingValue(options.delay) : 0;
if (delay && (context.previousNode instanceof StyleAst ||
(startTime == 0 && context.currentTimeline.getCurrentStyleProperties().length))) {
context.currentTimeline.snapshotCurrentStyles();
context.previousNode = DEFAULT_NOOP_PREVIOUS_NODE;
}
let furthestTime = startTime;
const elms = invokeQuery(
context.element, ast.selector, ast.originalSelector, ast.limit, ast.includeSelf,
options.optional ? true : false, context.errors);
context.currentQueryTotal = elms.length;
let sameElementTimeline: TimelineBuilder|null = null;
elms.forEach((element, i) => {
context.currentQueryIndex = i;
const innerContext = context.createSubContext(ast.options, element);
if (delay) {
innerContext.delayNextStep(delay);
}
if (element === context.element) {
sameElementTimeline = innerContext.currentTimeline;
}
ast.animation.visit(this, innerContext);
// this is here just incase the inner steps only contain or end
// with a style() call (which is here to signal that this is a preparatory
// call to style an element before it is animated again)
innerContext.currentTimeline.applyStylesToKeyframe();
const endTime = innerContext.currentTimeline.currentTime;
furthestTime = Math.max(furthestTime, endTime);
});
context.currentQueryIndex = 0;
context.currentQueryTotal = 0;
context.transformIntoNewTimeline(furthestTime);
if (sameElementTimeline) {
context.currentTimeline.mergeTimelineCollectedStyles(sameElementTimeline);
context.currentTimeline.snapshotCurrentStyles();
}
context.previousNode = ast;
}
visitStagger(ast: StaggerAst, context: AnimationTimelineContext) {
const parentContext = context.parentContext !;
const tl = context.currentTimeline;
const timings = ast.timings;
const duration = Math.abs(timings.duration);
const maxTime = duration * (context.currentQueryTotal - 1);
let delay = duration * context.currentQueryIndex;
let staggerTransformer = timings.duration < 0 ? 'reverse' : timings.easing;
switch (staggerTransformer) {
case 'reverse':
delay = maxTime - delay;
break;
case 'full':
delay = parentContext.currentStaggerTime;
break;
}
if (delay) {
context.currentTimeline.delayNextStep(delay);
}
const startingTime = context.currentTimeline.currentTime;
ast.animation.visit(this, context);
context.previousNode = ast;
// time = duration + delay
// the reason why this computation is so complex is because
// the inner timeline may either have a delay value or a stretched
// keyframe depending on if a subtimeline is not used or is used.
parentContext.currentStaggerTime =
(tl.currentTime - startingTime) + (tl.startTime - parentContext.currentTimeline.startTime);
}
}
export class TimelineBuilder {
public duration: number = 0;
public easing: string|null;
private _previousKeyframe: ɵStyleData = {};
private _currentKeyframe: ɵStyleData = {};
private _keyframes = new Map<number, ɵStyleData>();
private _styleSummary: {[prop: string]: StyleAtTime} = {};
private _localTimelineStyles: ɵStyleData;
private _globalTimelineStyles: ɵStyleData;
private _pendingStyles: ɵStyleData = {};
private _backFill: ɵStyleData = {};
private _currentEmptyStepKeyframe: ɵStyleData|null = null;
constructor(
public element: any, public startTime: number,
private _elementTimelineStylesLookup?: Map<any, ɵStyleData>) {
if (!this._elementTimelineStylesLookup) {
this._elementTimelineStylesLookup = new Map<any, ɵStyleData>();
}
this._localTimelineStyles = Object.create(this._backFill, {});
this._globalTimelineStyles = this._elementTimelineStylesLookup.get(element) !;
if (!this._globalTimelineStyles) {
this._globalTimelineStyles = this._localTimelineStyles;
this._elementTimelineStylesLookup.set(element, this._localTimelineStyles);
}
this._loadKeyframe();
}
containsAnimation(): boolean {
switch (this._keyframes.size) {
case 0:
return false;
case 1:
return this.getCurrentStyleProperties().length > 0;
default:
return true;
}
}
getCurrentStyleProperties(): string[] { return Object.keys(this._currentKeyframe); }
get currentTime() { return this.startTime + this.duration; }
delayNextStep(delay: number) {
if (this.duration == 0) {
this.startTime += delay;
} else {
this.forwardTime(this.currentTime + delay);
}
}
fork(element: any, currentTime?: number): TimelineBuilder {
this.applyStylesToKeyframe();
return new TimelineBuilder(
element, currentTime || this.currentTime, this._elementTimelineStylesLookup);
}
private _loadKeyframe() {
if (this._currentKeyframe) {
this._previousKeyframe = this._currentKeyframe;
}
this._currentKeyframe = this._keyframes.get(this.duration) !;
if (!this._currentKeyframe) {
this._currentKeyframe = Object.create(this._backFill, {});
this._keyframes.set(this.duration, this._currentKeyframe);
}
}
forwardFrame() {
this.duration += ONE_FRAME_IN_MILLISECONDS;
this._loadKeyframe();
}
forwardTime(time: number) {
this.applyStylesToKeyframe();
this.duration = time;
this._loadKeyframe();
}
private _updateStyle(prop: string, value: string|number) {
this._localTimelineStyles[prop] = value;
this._globalTimelineStyles[prop] = value;
this._styleSummary[prop] = {time: this.currentTime, value};
}
allowOnlyTimelineStyles() { return this._currentEmptyStepKeyframe !== this._currentKeyframe; }
applyEmptyStep(easing: string|null) {
if (easing) {
this._previousKeyframe['easing'] = easing;
}
// special case for animate(duration):
// all missing styles are filled with a `*` value then
// if any destination styles are filled in later on the same
// keyframe then they will override the overridden styles
// We use `_globalTimelineStyles` here because there may be
// styles in previous keyframes that are not present in this timeline
Object.keys(this._globalTimelineStyles).forEach(prop => {
this._backFill[prop] = this._globalTimelineStyles[prop] || AUTO_STYLE;
this._currentKeyframe[prop] = AUTO_STYLE;
});
this._currentEmptyStepKeyframe = this._currentKeyframe;
}
setStyles(
input: (ɵStyleData|string)[], easing: string|null, errors: any[],
options?: AnimationOptions) {
if (easing) {
this._previousKeyframe['easing'] = easing;
}
const params = (options && options.params) || {};
const styles = flattenStyles(input, this._globalTimelineStyles);
Object.keys(styles).forEach(prop => {
const val = interpolateParams(styles[prop], params, errors);
this._pendingStyles[prop] = val;
if (!this._localTimelineStyles.hasOwnProperty(prop)) {
this._backFill[prop] = this._globalTimelineStyles.hasOwnProperty(prop) ?
this._globalTimelineStyles[prop] :
AUTO_STYLE;
}
this._updateStyle(prop, val);
});
}
applyStylesToKeyframe() {
const styles = this._pendingStyles;
const props = Object.keys(styles);
if (props.length == 0) return;
this._pendingStyles = {};
props.forEach(prop => {
const val = styles[prop];
this._currentKeyframe[prop] = val;
});
Object.keys(this._localTimelineStyles).forEach(prop => {
if (!this._currentKeyframe.hasOwnProperty(prop)) {
this._currentKeyframe[prop] = this._localTimelineStyles[prop];
}
});
}
snapshotCurrentStyles() {
Object.keys(this._localTimelineStyles).forEach(prop => {
const val = this._localTimelineStyles[prop];
this._pendingStyles[prop] = val;
this._updateStyle(prop, val);
});
}
getFinalKeyframe() { return this._keyframes.get(this.duration); }
get properties() {
const properties: string[] = [];
for (let prop in this._currentKeyframe) {
properties.push(prop);
}
return properties;
}
mergeTimelineCollectedStyles(timeline: TimelineBuilder) {
Object.keys(timeline._styleSummary).forEach(prop => {
const details0 = this._styleSummary[prop];
const details1 = timeline._styleSummary[prop];
if (!details0 || details1.time > details0.time) {
this._updateStyle(prop, details1.value);
}
});
}
buildKeyframes(): AnimationTimelineInstruction {
this.applyStylesToKeyframe();
const preStyleProps = new Set<string>();
const postStyleProps = new Set<string>();
const isEmpty = this._keyframes.size === 1 && this.duration === 0;
let finalKeyframes: ɵStyleData[] = [];
this._keyframes.forEach((keyframe, time) => {
const finalKeyframe = copyStyles(keyframe, true);
Object.keys(finalKeyframe).forEach(prop => {
const value = finalKeyframe[prop];
if (value == PRE_STYLE) {
preStyleProps.add(prop);
} else if (value == AUTO_STYLE) {
postStyleProps.add(prop);
}
});
if (!isEmpty) {
finalKeyframe['offset'] = time / this.duration;
}
finalKeyframes.push(finalKeyframe);
});
const preProps: string[] = preStyleProps.size ? iteratorToArray(preStyleProps.values()) : [];
const postProps: string[] = postStyleProps.size ? iteratorToArray(postStyleProps.values()) : [];
// special case for a 0-second animation (which is designed just to place styles onscreen)
if (isEmpty) {
const kf0 = finalKeyframes[0];
const kf1 = copyObj(kf0);
kf0['offset'] = 0;
kf1['offset'] = 1;
finalKeyframes = [kf0, kf1];
}
return createTimelineInstruction(
this.element, finalKeyframes, preProps, postProps, this.duration, this.startTime,
this.easing, false);
}
}
class SubTimelineBuilder extends TimelineBuilder {
public timings: AnimateTimings;
constructor(
public element: any, public keyframes: ɵStyleData[], public preStyleProps: string[],
public postStyleProps: string[], timings: AnimateTimings,
private _stretchStartingKeyframe: boolean = false) {
super(element, timings.delay);
this.timings = {duration: timings.duration, delay: timings.delay, easing: timings.easing};
}
containsAnimation(): boolean { return this.keyframes.length > 1; }
buildKeyframes(): AnimationTimelineInstruction {
let keyframes = this.keyframes;
let {delay, duration, easing} = this.timings;
if (this._stretchStartingKeyframe && delay) {
const newKeyframes: ɵStyleData[] = [];
const totalTime = duration + delay;
const startingGap = delay / totalTime;
// the original starting keyframe now starts once the delay is done
const newFirstKeyframe = copyStyles(keyframes[0], false);
newFirstKeyframe['offset'] = 0;
newKeyframes.push(newFirstKeyframe);
const oldFirstKeyframe = copyStyles(keyframes[0], false);
oldFirstKeyframe['offset'] = roundOffset(startingGap);
newKeyframes.push(oldFirstKeyframe);
/*
When the keyframe is stretched then it means that the delay before the animation
starts is gone. Instead the first keyframe is placed at the start of the animation
and it is then copied to where it starts when the original delay is over. This basically
means nothing animates during that delay, but the styles are still renderered. For this
to work the original offset values that exist in the original keyframes must be "warped"
so that they can take the new keyframe + delay into account.
delay=1000, duration=1000, keyframes = 0 .5 1
turns into
delay=0, duration=2000, keyframes = 0 .33 .66 1
*/
// offsets between 1 ... n -1 are all warped by the keyframe stretch
const limit = keyframes.length - 1;
for (let i = 1; i <= limit; i++) {
let kf = copyStyles(keyframes[i], false);
const oldOffset = kf['offset'] as number;
const timeAtKeyframe = delay + oldOffset * duration;
kf['offset'] = roundOffset(timeAtKeyframe / totalTime);
newKeyframes.push(kf);
}
// the new starting keyframe should be added at the start
duration = totalTime;
delay = 0;
easing = '';
keyframes = newKeyframes;
}
return createTimelineInstruction(
this.element, keyframes, this.preStyleProps, this.postStyleProps, duration, delay, easing,
true);
}
}
function invokeQuery(
rootElement: any, selector: string, originalSelector: string, limit: number,
includeSelf: boolean, optional: boolean, errors: any[]): any[] {
const multi = limit != 1;
let results: any[] = [];
if (includeSelf) {
results.push(rootElement);
}
if (selector.length > 0) { // if :self is only used then the selector is empty
if (multi) {
results.push(...rootElement.querySelectorAll(selector));
if (limit > 1) {
results = results.slice(0, limit);
}
} else {
const elm = rootElement.querySelector(selector);
if (elm) {
results.push(elm);
}
}
}
if (!optional && results.length == 0) {
errors.push(
`\`query("${originalSelector}")\` returned zero elements. (Use \`query("${originalSelector}", { optional: true })\` if you wish to allow this.)`);
}
return results;
}
function roundOffset(offset: number, decimalPoints = 3): number {
const mult = Math.pow(10, decimalPoints - 1);
return Math.round(offset * mult) / mult;
}
function flattenStyles(input: (ɵStyleData | string)[], allStyles: ɵStyleData) {
const styles: ɵStyleData = {};
let allProperties: string[];
input.forEach(token => {
if (token === '*') {
allProperties = allProperties || Object.keys(allStyles);
allProperties.forEach(prop => { styles[prop] = AUTO_STYLE; });
} else {
copyStyles(token as ɵStyleData, false, styles);
}
});
return styles;
}

View File

@ -9,21 +9,30 @@ import {ɵStyleData} from '@angular/animations';
import {AnimationEngineInstruction, AnimationTransitionInstructionType} from '../render/animation_engine_instruction';
export interface AnimationTimelineInstruction extends AnimationEngineInstruction {
element: any;
keyframes: ɵStyleData[];
preStyleProps: string[];
postStyleProps: string[];
duration: number;
delay: number;
totalTime: number;
easing: string|null|undefined;
easing: string|null;
stretchStartingKeyframe?: boolean;
subTimeline: boolean;
}
export function createTimelineInstruction(
keyframes: ɵStyleData[], duration: number, delay: number,
easing: string | null | undefined): AnimationTimelineInstruction {
element: any, keyframes: ɵStyleData[], preStyleProps: string[], postStyleProps: string[],
duration: number, delay: number, easing: string | null = null,
subTimeline: boolean = false): AnimationTimelineInstruction {
return {
type: AnimationTransitionInstructionType.TimelineAnimation,
element,
keyframes,
preStyleProps,
postStyleProps,
duration,
delay,
totalTime: duration + delay, easing
totalTime: duration + delay, easing, subTimeline
};
}

View File

@ -1,470 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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 {AUTO_STYLE, AnimateTimings, AnimationAnimateMetadata, AnimationGroupMetadata, AnimationKeyframesSequenceMetadata, AnimationMetadata, AnimationMetadataType, AnimationSequenceMetadata, AnimationStateMetadata, AnimationStyleMetadata, AnimationTransitionMetadata, sequence, style, ɵStyleData} from '@angular/animations';
import {copyStyles, normalizeStyles, parseTimeExpression} from '../util';
import {AnimationDslVisitor, visitAnimationNode} from './animation_dsl_visitor';
import {AnimationTimelineInstruction, createTimelineInstruction} from './animation_timeline_instruction';
/*
* The code within this file aims to generate web-animations-compatible keyframes from Angular's
* animation DSL code.
*
* The code below will be converted from:
*
* ```
* sequence([
* style({ opacity: 0 }),
* animate(1000, style({ opacity: 0 }))
* ])
* ```
*
* To:
* ```
* keyframes = [{ opacity: 0, offset: 0 }, { opacity: 1, offset: 1 }]
* duration = 1000
* delay = 0
* easing = ''
* ```
*
* For this operation to cover the combination of animation verbs (style, animate, group, etc...) a
* combination of prototypical inheritance, AST traversal and merge-sort-like algorithms are used.
*
* [AST Traversal]
* Each of the animation verbs, when executed, will return an string-map object representing what
* type of action it is (style, animate, group, etc...) and the data associated with it. This means
* that when functional composition mix of these functions is evaluated (like in the example above)
* then it will end up producing a tree of objects representing the animation itself.
*
* When this animation object tree is processed by the visitor code below it will visit each of the
* verb statements within the visitor. And during each visit it will build the context of the
* animation keyframes by interacting with the `TimelineBuilder`.
*
* [TimelineBuilder]
* This class is responsible for tracking the styles and building a series of keyframe objects for a
* timeline between a start and end time. The builder starts off with an initial timeline and each
* time the AST comes across a `group()`, `keyframes()` or a combination of the two within a
* `sequence()` then it will generate a sub timeline for each step as well as a new one after
* they are complete.
*
* As the AST is traversed, the timing state on each of the timelines will be incremented. If a sub
* timeline was created (based on one of the cases above) then the parent timeline will attempt to
* merge the styles used within the sub timelines into itself (only with group() this will happen).
* This happens with a merge operation (much like how the merge works in mergesort) and it will only
* copy the most recently used styles from the sub timelines into the parent timeline. This ensures
* that if the styles are used later on in another phase of the animation then they will be the most
* up-to-date values.
*
* [How Missing Styles Are Updated]
* Each timeline has a `backFill` property which is responsible for filling in new styles into
* already processed keyframes if a new style shows up later within the animation sequence.
*
* ```
* sequence([
* style({ width: 0 }),
* animate(1000, style({ width: 100 })),
* animate(1000, style({ width: 200 })),
* animate(1000, style({ width: 300 }))
* animate(1000, style({ width: 400, height: 400 })) // notice how `height` doesn't exist anywhere
* else
* ])
* ```
*
* What is happening here is that the `height` value is added later in the sequence, but is missing
* from all previous animation steps. Therefore when a keyframe is created it would also be missing
* from all previous keyframes up until where it is first used. For the timeline keyframe generation
* to properly fill in the style it will place the previous value (the value from the parent
* timeline) or a default value of `*` into the backFill object. Given that each of the keyframe
* styles are objects that prototypically inherited from the backFill object, this means that if a
* value is added into the backFill then it will automatically propagate any missing values to all
* keyframes. Therefore the missing `height` value will be properly filled into the already
* processed keyframes.
*
* When a sub-timeline is created it will have its own backFill property. This is done so that
* styles present within the sub-timeline do not accidentally seep into the previous/future timeline
* keyframes
*
* (For prototypically inherited contents to be detected a `for(i in obj)` loop must be used.)
*
* [Validation]
* The code in this file is not responsible for validation. That functionality happens with within
* the `AnimationValidatorVisitor` code.
*/
export function buildAnimationKeyframes(
ast: AnimationMetadata | AnimationMetadata[], startingStyles: ɵStyleData = {},
finalStyles: ɵStyleData = {}): AnimationTimelineInstruction[] {
const normalizedAst =
Array.isArray(ast) ? sequence(<AnimationMetadata[]>ast) : <AnimationMetadata>ast;
return new AnimationTimelineVisitor().buildKeyframes(normalizedAst, startingStyles, finalStyles);
}
export declare type StyleAtTime = {
time: number; value: string | number;
};
export class AnimationTimelineContext {
currentTimeline: TimelineBuilder;
currentAnimateTimings: AnimateTimings|null;
previousNode: AnimationMetadata = <AnimationMetadata>{};
subContextCount = 0;
constructor(
public errors: any[], public timelines: TimelineBuilder[],
initialTimeline?: TimelineBuilder) {
this.currentTimeline = initialTimeline || new TimelineBuilder(0);
timelines.push(this.currentTimeline);
}
createSubContext(): AnimationTimelineContext {
const context =
new AnimationTimelineContext(this.errors, this.timelines, this.currentTimeline.fork());
context.previousNode = this.previousNode;
context.currentAnimateTimings = this.currentAnimateTimings;
this.subContextCount++;
return context;
}
transformIntoNewTimeline(newTime = 0) {
this.currentTimeline = this.currentTimeline.fork(newTime);
this.timelines.push(this.currentTimeline);
return this.currentTimeline;
}
incrementTime(time: number) {
this.currentTimeline.forwardTime(this.currentTimeline.duration + time);
}
}
export class AnimationTimelineVisitor implements AnimationDslVisitor {
buildKeyframes(ast: AnimationMetadata, startingStyles: ɵStyleData, finalStyles: ɵStyleData):
AnimationTimelineInstruction[] {
const context = new AnimationTimelineContext([], []);
context.currentTimeline.setStyles(startingStyles);
visitAnimationNode(this, ast, context);
// this checks to see if an actual animation happened
const timelines = context.timelines.filter(timeline => timeline.hasStyling());
if (timelines.length && Object.keys(finalStyles).length) {
const tl = timelines[timelines.length - 1];
if (!tl.allowOnlyTimelineStyles()) {
tl.setStyles(finalStyles);
}
}
return timelines.length ? timelines.map(timeline => timeline.buildKeyframes()) :
[createTimelineInstruction([], 0, 0, '')];
}
visitState(ast: AnimationStateMetadata, context: any): any {
// these values are not visited in this AST
}
visitTransition(ast: AnimationTransitionMetadata, context: any): any {
// these values are not visited in this AST
}
visitSequence(ast: AnimationSequenceMetadata, context: AnimationTimelineContext) {
const subContextCount = context.subContextCount;
if (context.previousNode.type == AnimationMetadataType.Style) {
context.currentTimeline.forwardFrame();
context.currentTimeline.snapshotCurrentStyles();
}
ast.steps.forEach(s => visitAnimationNode(this, s, context));
// this means that some animation function within the sequence
// ended up creating a sub timeline (which means the current
// timeline cannot overlap with the contents of the sequence)
if (context.subContextCount > subContextCount) {
context.transformIntoNewTimeline();
}
context.previousNode = ast;
}
visitGroup(ast: AnimationGroupMetadata, context: AnimationTimelineContext) {
const innerTimelines: TimelineBuilder[] = [];
let furthestTime = context.currentTimeline.currentTime;
ast.steps.forEach(s => {
const innerContext = context.createSubContext();
visitAnimationNode(this, s, innerContext);
furthestTime = Math.max(furthestTime, innerContext.currentTimeline.currentTime);
innerTimelines.push(innerContext.currentTimeline);
});
// this operation is run after the AST loop because otherwise
// if the parent timeline's collected styles were updated then
// it would pass in invalid data into the new-to-be forked items
innerTimelines.forEach(
timeline => context.currentTimeline.mergeTimelineCollectedStyles(timeline));
context.transformIntoNewTimeline(furthestTime);
context.previousNode = ast;
}
visitAnimate(ast: AnimationAnimateMetadata, context: AnimationTimelineContext) {
const timings = ast.timings.hasOwnProperty('duration') ?
<AnimateTimings>ast.timings :
parseTimeExpression(<string|number>ast.timings, context.errors);
context.currentAnimateTimings = timings;
if (timings.delay) {
context.incrementTime(timings.delay);
context.currentTimeline.snapshotCurrentStyles();
}
const astType = ast.styles ? ast.styles.type : -1;
if (astType == AnimationMetadataType.KeyframeSequence) {
this.visitKeyframeSequence(<AnimationKeyframesSequenceMetadata>ast.styles, context);
} else {
let styleAst = ast.styles as AnimationStyleMetadata;
if (!styleAst) {
const newStyleData: {[prop: string]: string | number} = {};
if (timings.easing) {
newStyleData['easing'] = timings.easing;
}
styleAst = style(newStyleData);
(styleAst as any)['treatAsEmptyStep'] = true;
}
context.incrementTime(timings.duration);
if (styleAst) {
this.visitStyle(styleAst, context);
}
}
context.currentAnimateTimings = null;
context.previousNode = ast;
}
visitStyle(ast: AnimationStyleMetadata, context: AnimationTimelineContext) {
// this is a special case when a style() call is issued directly after
// a call to animate(). If the clock is not forwarded by one frame then
// the style() calls will be merged into the previous animate() call
// which is incorrect.
if (!context.currentAnimateTimings &&
context.previousNode.type == AnimationMetadataType.Animate) {
context.currentTimeline.forwardFrame();
}
const normalizedStyles = normalizeStyles(ast.styles);
const easing = context.currentAnimateTimings && context.currentAnimateTimings.easing;
this._applyStyles(
normalizedStyles, easing, (ast as any)['treatAsEmptyStep'] ? true : false, context);
context.previousNode = ast;
}
private _applyStyles(
styles: ɵStyleData, easing: string|null, treatAsEmptyStep: boolean,
context: AnimationTimelineContext) {
if (styles.hasOwnProperty('easing')) {
easing = easing || styles['easing'] as string;
delete styles['easing'];
}
context.currentTimeline.setStyles(styles, easing, treatAsEmptyStep);
}
visitKeyframeSequence(
ast: AnimationKeyframesSequenceMetadata, context: AnimationTimelineContext) {
const MAX_KEYFRAME_OFFSET = 1;
const limit = ast.steps.length - 1;
const firstKeyframe = ast.steps[0];
let offsetGap = 0;
const containsOffsets = getOffset(firstKeyframe) != null;
if (!containsOffsets) {
offsetGap = MAX_KEYFRAME_OFFSET / limit;
}
const startTime = context.currentTimeline.duration;
const duration = context.currentAnimateTimings !.duration;
const innerContext = context.createSubContext();
const innerTimeline = innerContext.currentTimeline;
innerTimeline.easing = context.currentAnimateTimings !.easing;
ast.steps.forEach((step: AnimationStyleMetadata, i: number) => {
const normalizedStyles = normalizeStyles(step.styles);
const offset = containsOffsets ?
(step.offset != null ? step.offset : parseFloat(normalizedStyles['offset'] as string)) :
(i == limit ? MAX_KEYFRAME_OFFSET : i * offsetGap);
innerTimeline.forwardTime(offset * duration);
this._applyStyles(normalizedStyles, null, false, innerContext);
});
// this will ensure that the parent timeline gets all the styles from
// the child even if the new timeline below is not used
context.currentTimeline.mergeTimelineCollectedStyles(innerTimeline);
// we do this because the window between this timeline and the sub timeline
// should ensure that the styles within are exactly the same as they were before
context.transformIntoNewTimeline(startTime + duration);
context.previousNode = ast;
}
}
export class TimelineBuilder {
public duration: number = 0;
public easing: string|null = '';
private _previousKeyframe: ɵStyleData = {};
private _currentKeyframe: ɵStyleData;
private _keyframes = new Map<number, ɵStyleData>();
private _styleSummary: {[prop: string]: StyleAtTime} = {};
private _localTimelineStyles: ɵStyleData;
private _backFill: ɵStyleData = {};
private _currentEmptyStepKeyframe: ɵStyleData|null = null;
private _globalTimelineStyles: ɵStyleData;
constructor(public startTime: number, globalTimelineStyles?: ɵStyleData) {
this._localTimelineStyles = Object.create(this._backFill, {});
this._globalTimelineStyles =
globalTimelineStyles ? globalTimelineStyles : this._localTimelineStyles;
this._loadKeyframe();
}
hasStyling(): boolean { return this._keyframes.size > 1; }
get currentTime() { return this.startTime + this.duration; }
fork(currentTime = 0): TimelineBuilder {
return new TimelineBuilder(currentTime || this.currentTime, this._globalTimelineStyles);
}
private _loadKeyframe() {
if (this._currentKeyframe) {
this._previousKeyframe = this._currentKeyframe;
}
this._currentKeyframe = this._keyframes.get(this.duration) !;
if (!this._currentKeyframe) {
this._currentKeyframe = Object.create(this._backFill, {});
this._keyframes.set(this.duration, this._currentKeyframe);
}
}
forwardFrame() {
this.duration++;
this._loadKeyframe();
}
forwardTime(time: number) {
this.duration = time;
this._loadKeyframe();
}
private _updateStyle(prop: string, value: string|number) {
this._localTimelineStyles[prop] = value;
this._globalTimelineStyles ![prop] = value;
this._styleSummary[prop] = {time: this.currentTime, value};
}
allowOnlyTimelineStyles() { return this._currentEmptyStepKeyframe !== this._currentKeyframe; }
setStyles(styles: ɵStyleData, easing: string|null = null, treatAsEmptyStep: boolean = false) {
if (easing) {
this._previousKeyframe !['easing'] = easing;
}
if (treatAsEmptyStep) {
// special case for animate(duration):
// all missing styles are filled with a `*` value then
// if any destination styles are filled in later on the same
// keyframe then they will override the overridden styles
// We use `_globalTimelineStyles` here because there may be
// styles in previous keyframes that are not present in this timeline
Object.keys(this._globalTimelineStyles).forEach(prop => {
this._backFill[prop] = this._globalTimelineStyles[prop] || AUTO_STYLE;
this._currentKeyframe[prop] = AUTO_STYLE;
});
this._currentEmptyStepKeyframe = this._currentKeyframe;
} else {
Object.keys(styles).forEach(prop => {
if (prop !== 'offset') {
const val = styles[prop];
this._currentKeyframe[prop] = val;
if (!this._localTimelineStyles[prop]) {
this._backFill[prop] = this._globalTimelineStyles[prop] || AUTO_STYLE;
}
this._updateStyle(prop, val);
}
});
Object.keys(this._localTimelineStyles).forEach(prop => {
if (!this._currentKeyframe.hasOwnProperty(prop)) {
this._currentKeyframe[prop] = this._localTimelineStyles[prop];
}
});
}
}
snapshotCurrentStyles() { copyStyles(this._localTimelineStyles, false, this._currentKeyframe); }
getFinalKeyframe(): ɵStyleData { return this._keyframes.get(this.duration) !; }
get properties() {
const properties: string[] = [];
for (let prop in this._currentKeyframe) {
properties.push(prop);
}
return properties;
}
mergeTimelineCollectedStyles(timeline: TimelineBuilder) {
Object.keys(timeline._styleSummary).forEach(prop => {
const details0 = this._styleSummary[prop];
const details1 = timeline._styleSummary[prop];
if (!details0 || details1.time > details0.time) {
this._updateStyle(prop, details1.value);
}
});
}
buildKeyframes(): AnimationTimelineInstruction {
const finalKeyframes: ɵStyleData[] = [];
// special case for when there are only start/destination
// styles but no actual animation animate steps...
if (this.duration == 0) {
const targetKeyframe = this.getFinalKeyframe();
const firstKeyframe = copyStyles(targetKeyframe, true);
firstKeyframe['offset'] = 0;
finalKeyframes.push(firstKeyframe);
const lastKeyframe = copyStyles(targetKeyframe, true);
lastKeyframe['offset'] = 1;
finalKeyframes.push(lastKeyframe);
} else {
this._keyframes.forEach((keyframe, time) => {
const finalKeyframe = copyStyles(keyframe, true);
finalKeyframe['offset'] = time / this.duration;
finalKeyframes.push(finalKeyframe);
});
}
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

@ -57,8 +57,16 @@ function parseAnimationAlias(alias: string, errors: string[]): string {
function makeLambdaFromStates(lhs: string, rhs: string): TransitionMatcherFn {
return (fromState: any, toState: any): boolean => {
const lhsMatch = lhs == ANY_STATE || lhs == fromState;
const rhsMatch = rhs == ANY_STATE || rhs == toState;
let lhsMatch = lhs == ANY_STATE || lhs == fromState;
let rhsMatch = rhs == ANY_STATE || rhs == toState;
if (!lhsMatch && typeof fromState === 'boolean') {
lhsMatch = fromState ? lhs === 'true' : lhs === 'false';
}
if (!rhsMatch && typeof toState === 'boolean') {
rhsMatch = toState ? rhs === 'true' : rhs === 'false';
}
return lhsMatch && rhsMatch;
};
}

View File

@ -5,38 +5,66 @@
* 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 {AnimationMetadata, AnimationTransitionMetadata, sequence, ɵStyleData} from '@angular/animations';
import {AnimationOptions, ɵStyleData} from '@angular/animations';
import {buildAnimationKeyframes} from './animation_timeline_visitor';
import {getOrSetAsInMap} from '../render/shared';
import {iteratorToArray, mergeAnimationOptions} from '../util';
import {TransitionAst} from './animation_ast';
import {buildAnimationTimelines} from './animation_timeline_builder';
import {TransitionMatcherFn} from './animation_transition_expr';
import {AnimationTransitionInstruction, createTransitionInstruction} from './animation_transition_instruction';
import {ElementInstructionMap} from './element_instruction_map';
export class AnimationTransitionFactory {
private _animationAst: AnimationMetadata;
constructor(
private _triggerName: string, ast: AnimationTransitionMetadata,
private matchFns: TransitionMatcherFn[],
private _stateStyles: {[stateName: string]: ɵStyleData}) {
const normalizedAst = Array.isArray(ast.animation) ?
sequence(<AnimationMetadata[]>ast.animation) :
<AnimationMetadata>ast.animation;
this._animationAst = normalizedAst;
private _triggerName: string, public ast: TransitionAst,
private _stateStyles: {[stateName: string]: ɵStyleData}) {}
match(currentState: any, nextState: any): boolean {
return oneOrMoreTransitionsMatch(this.ast.matchers, currentState, nextState);
}
match(currentState: any, nextState: any): AnimationTransitionInstruction|undefined {
if (!oneOrMoreTransitionsMatch(this.matchFns, currentState, nextState)) return;
build(
element: any, currentState: any, nextState: any, options?: AnimationOptions,
subInstructions?: ElementInstructionMap): AnimationTransitionInstruction|undefined {
const animationOptions = mergeAnimationOptions(this.ast.options || {}, options || {});
const backupStateStyles = this._stateStyles['*'] || {};
const currentStateStyles = this._stateStyles[currentState] || backupStateStyles;
const nextStateStyles = this._stateStyles[nextState] || backupStateStyles;
const timelines =
buildAnimationKeyframes(this._animationAst, currentStateStyles, nextStateStyles);
const errors: any[] = [];
const timelines = buildAnimationTimelines(
element, this.ast.animation, currentStateStyles, nextStateStyles, animationOptions,
subInstructions, errors);
if (errors.length) {
const errorMessage = `animation building failed:\n${errors.join("\n")}`;
throw new Error(errorMessage);
}
const preStyleMap = new Map<any, {[prop: string]: boolean}>();
const postStyleMap = new Map<any, {[prop: string]: boolean}>();
const queriedElements = new Set<any>();
timelines.forEach(tl => {
const elm = tl.element;
const preProps = getOrSetAsInMap(preStyleMap, elm, {});
tl.preStyleProps.forEach(prop => preProps[prop] = true);
const postProps = getOrSetAsInMap(postStyleMap, elm, {});
tl.postStyleProps.forEach(prop => postProps[prop] = true);
if (elm !== element) {
queriedElements.add(elm);
}
});
const queriedElementsList = iteratorToArray(queriedElements.values());
return createTransitionInstruction(
this._triggerName, currentState, nextState, nextState === 'void', currentStateStyles,
nextStateStyles, timelines);
element, this._triggerName, currentState, nextState, nextState === 'void',
currentStateStyles, nextStateStyles, timelines, queriedElementsList, preStyleMap,
postStyleMap);
}
}

View File

@ -10,6 +10,7 @@ import {AnimationEngineInstruction, AnimationTransitionInstructionType} from '..
import {AnimationTimelineInstruction} from './animation_timeline_instruction';
export interface AnimationTransitionInstruction extends AnimationEngineInstruction {
element: any;
triggerName: string;
isRemovalTransition: boolean;
fromState: string;
@ -17,20 +18,29 @@ export interface AnimationTransitionInstruction extends AnimationEngineInstructi
toState: string;
toStyles: ɵStyleData;
timelines: AnimationTimelineInstruction[];
queriedElements: any[];
preStyleProps: Map<any, {[prop: string]: boolean}>;
postStyleProps: Map<any, {[prop: string]: boolean}>;
}
export function createTransitionInstruction(
triggerName: string, fromState: string, toState: string, isRemovalTransition: boolean,
fromStyles: ɵStyleData, toStyles: ɵStyleData,
timelines: AnimationTimelineInstruction[]): AnimationTransitionInstruction {
element: any, triggerName: string, fromState: string, toState: string,
isRemovalTransition: boolean, fromStyles: ɵStyleData, toStyles: ɵStyleData,
timelines: AnimationTimelineInstruction[], queriedElements: any[],
preStyleProps: Map<any, {[prop: string]: boolean}>,
postStyleProps: Map<any, {[prop: string]: boolean}>): AnimationTransitionInstruction {
return {
type: AnimationTransitionInstructionType.TransitionAnimation,
element,
triggerName,
isRemovalTransition,
fromState,
fromStyles,
toState,
toStyles,
timelines
timelines,
queriedElements,
preStyleProps,
postStyleProps
};
}

View File

@ -5,22 +5,18 @@
* 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 {AnimationAnimateMetadata, AnimationGroupMetadata, AnimationKeyframesSequenceMetadata, AnimationMetadata, AnimationSequenceMetadata, AnimationStateMetadata, AnimationStyleMetadata, AnimationTransitionMetadata, ɵStyleData} from '@angular/animations';
import {ɵStyleData} from '@angular/animations';
import {copyStyles, normalizeStyles} from '../util';
import {copyStyles} from '../util';
import {AnimationDslVisitor, visitAnimationNode} from './animation_dsl_visitor';
import {parseTransitionExpr} from './animation_transition_expr';
import {SequenceAst, TransitionAst, TriggerAst} from './animation_ast';
import {AnimationTransitionFactory} from './animation_transition_factory';
import {AnimationTransitionInstruction, createTransitionInstruction} from './animation_transition_instruction';
import {validateAnimationSequence} from './animation_validator_visitor';
/**
* @experimental Animation support is experimental.
*/
export function buildTrigger(name: string, definitions: AnimationMetadata[]): AnimationTrigger {
return new AnimationTriggerVisitor().buildTrigger(name, definitions);
export function buildTrigger(name: string, ast: TriggerAst): AnimationTrigger {
return new AnimationTrigger(name, ast);
}
/**
@ -28,90 +24,51 @@ export function buildTrigger(name: string, definitions: AnimationMetadata[]): An
*/
export class AnimationTrigger {
public transitionFactories: AnimationTransitionFactory[] = [];
public fallbackTransition: AnimationTransitionFactory;
public states: {[stateName: string]: ɵStyleData} = {};
constructor(
public name: string, states: {[stateName: string]: ɵStyleData},
private _transitionAsts: AnimationTransitionMetadata[]) {
Object.keys(states).forEach(
stateName => { this.states[stateName] = copyStyles(states[stateName], false); });
const errors: string[] = [];
_transitionAsts.forEach(ast => {
const exprs = parseTransitionExpr(ast.expr, errors);
const sequenceErrors = validateAnimationSequence(ast);
if (sequenceErrors.length) {
errors.push(...sequenceErrors);
} else {
this.transitionFactories.push(
new AnimationTransitionFactory(this.name, ast, exprs, states));
}
constructor(public name: string, public ast: TriggerAst) {
ast.states.forEach(ast => {
const obj = this.states[ast.name] = {};
ast.style.styles.forEach(styleTuple => {
if (typeof styleTuple == 'object') {
copyStyles(styleTuple as ɵStyleData, false, obj);
}
});
});
if (errors.length) {
const LINE_START = '\n - ';
throw new Error(
`Animation parsing for the ${name} trigger have failed:${LINE_START}${errors.join(LINE_START)}`);
balanceProperties(this.states, 'true', '1');
balanceProperties(this.states, 'false', '0');
ast.transitions.forEach(ast => {
this.transitionFactories.push(new AnimationTransitionFactory(name, ast, this.states));
});
this.fallbackTransition = createFallbackTransition(name, this.states);
}
get containsQueries() { return this.ast.queryCount > 0; }
matchTransition(currentState: any, nextState: any): AnimationTransitionFactory|null {
const entry = this.transitionFactories.find(f => f.match(currentState, nextState));
return entry || null;
}
}
function createFallbackTransition(
triggerName: string, states: {[stateName: string]: ɵStyleData}): AnimationTransitionFactory {
const matchers = [(fromState: any, toState: any) => true];
const animation = new SequenceAst([]);
const transition = new TransitionAst(matchers, animation);
return new AnimationTransitionFactory(triggerName, transition, states);
}
function balanceProperties(obj: {[key: string]: any}, key1: string, key2: string) {
if (obj.hasOwnProperty(key1)) {
if (!obj.hasOwnProperty(key2)) {
obj[key2] = obj[key1];
}
}
createFallbackInstruction(currentState: any, nextState: any): AnimationTransitionInstruction {
const backupStateStyles = this.states['*'] || {};
const currentStateStyles = this.states[currentState] || backupStateStyles;
const nextStateStyles = this.states[nextState] || backupStateStyles;
return createTransitionInstruction(
this.name, currentState, nextState, nextState == 'void', currentStateStyles,
nextStateStyles, []);
}
matchTransition(currentState: any, nextState: any): AnimationTransitionInstruction|null {
for (let i = 0; i < this.transitionFactories.length; i++) {
let result = this.transitionFactories[i].match(currentState, nextState);
if (result) return result;
}
return null;
}
}
class AnimationTriggerContext {
public errors: string[] = [];
public states: {[stateName: string]: ɵStyleData} = {};
public transitions: AnimationTransitionMetadata[] = [];
}
class AnimationTriggerVisitor implements AnimationDslVisitor {
buildTrigger(name: string, definitions: AnimationMetadata[]): AnimationTrigger {
const context = new AnimationTriggerContext();
definitions.forEach(def => visitAnimationNode(this, def, context));
return new AnimationTrigger(name, context.states, context.transitions);
}
visitState(ast: AnimationStateMetadata, context: any): any {
const styles = normalizeStyles(ast.styles.styles);
ast.name.split(/\s*,\s*/).forEach(name => { context.states[name] = styles; });
}
visitTransition(ast: AnimationTransitionMetadata, context: any): any {
context.transitions.push(ast);
}
visitSequence(ast: AnimationSequenceMetadata, context: any) {
// these values are not visited in this AST
}
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
} else if (obj.hasOwnProperty(key2)) {
obj[key1] = obj[key2];
}
}

View File

@ -1,195 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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 {AnimateTimings, AnimationAnimateMetadata, AnimationGroupMetadata, AnimationKeyframesSequenceMetadata, AnimationMetadata, AnimationMetadataType, AnimationSequenceMetadata, AnimationStateMetadata, AnimationStyleMetadata, AnimationTransitionMetadata, sequence} from '@angular/animations';
import {normalizeStyles, parseTimeExpression} from '../util';
import {AnimationDslVisitor, visitAnimationNode} from './animation_dsl_visitor';
export type StyleTimeTuple = {
startTime: number; endTime: number;
};
/*
* [Validation]
* The visitor code below will traverse the animation AST generated by the animation verb functions
* (the output is a tree of objects) and attempt to perform a series of validations on the data. The
* following corner-cases will be validated:
*
* 1. Overlap of animations
* Given that a CSS property cannot be animated in more than one place at the same time, it's
* important that this behaviour is detected and validated. The way in which this occurs is that
* each time a style property is examined, a string-map containing the property will be updated with
* the start and end times for when the property is used within an animation step.
*
* If there are two or more parallel animations that are currently running (these are invoked by the
* group()) on the same element then the validator will throw an error. Since the start/end timing
* values are collected for each property then if the current animation step is animating the same
* property and its timing values fall anywhere into the window of time that the property is
* currently being animated within then this is what causes an error.
*
* 2. Timing values
* The validator will validate to see if a timing value of `duration delay easing` or
* `durationNumber` is valid or not.
*
* (note that upon validation the code below will replace the timing data with an object containing
* {duration,delay,easing}.
*
* 3. Offset Validation
* Each of the style() calls are allowed to have an offset value when placed inside of keyframes().
* Offsets within keyframes() are considered valid when:
*
* - No offsets are used at all
* - Each style() entry contains an offset value
* - Each offset is between 0 and 1
* - Each offset is greater to or equal than the previous one
*
* Otherwise an error will be thrown.
*/
export function validateAnimationSequence(ast: AnimationMetadata) {
const normalizedAst =
Array.isArray(ast) ? sequence(<AnimationMetadata[]>ast) : <AnimationMetadata>ast;
return new AnimationValidatorVisitor().validate(normalizedAst);
}
export class AnimationValidatorVisitor implements AnimationDslVisitor {
validate(ast: AnimationMetadata): string[] {
const context = new AnimationValidatorContext();
visitAnimationNode(this, ast, context);
return context.errors;
}
visitState(ast: AnimationStateMetadata, context: any): any {
// these values are not visited in this AST
}
visitTransition(ast: AnimationTransitionMetadata, context: any): any {
// these values are not visited in this AST
}
visitSequence(ast: AnimationSequenceMetadata, context: AnimationValidatorContext): any {
ast.steps.forEach(step => visitAnimationNode(this, step, context));
}
visitGroup(ast: AnimationGroupMetadata, context: AnimationValidatorContext): any {
const currentTime = context.currentTime;
let furthestTime = 0;
ast.steps.forEach(step => {
context.currentTime = currentTime;
visitAnimationNode(this, step, context);
furthestTime = Math.max(furthestTime, context.currentTime);
});
context.currentTime = furthestTime;
}
visitAnimate(ast: AnimationAnimateMetadata, context: AnimationValidatorContext): any {
// we reassign the timings here so that they are not reparsed each
// time an animation occurs
context.currentAnimateTimings = ast.timings =
parseTimeExpression(<string|number>ast.timings, context.errors);
const astType = ast.styles && ast.styles.type;
if (astType == AnimationMetadataType.KeyframeSequence) {
this.visitKeyframeSequence(<AnimationKeyframesSequenceMetadata>ast.styles, context);
} else {
context.currentTime +=
context.currentAnimateTimings.duration + context.currentAnimateTimings.delay;
if (astType == AnimationMetadataType.Style) {
this.visitStyle(<AnimationStyleMetadata>ast.styles, context);
}
}
context.currentAnimateTimings = null;
}
visitStyle(ast: AnimationStyleMetadata, context: AnimationValidatorContext): any {
const styleData = normalizeStyles(ast.styles);
const timings = context.currentAnimateTimings;
let endTime = context.currentTime;
let startTime = context.currentTime;
if (timings && startTime > 0) {
startTime -= timings.duration + timings.delay;
}
Object.keys(styleData).forEach(prop => {
const collectedEntry = context.collectedStyles[prop];
let updateCollectedStyle = true;
if (collectedEntry) {
if (startTime != endTime && startTime >= collectedEntry.startTime &&
endTime <= collectedEntry.endTime) {
context.errors.push(
`The CSS property "${prop}" that exists between the times of "${collectedEntry.startTime}ms" and "${collectedEntry.endTime}ms" is also being animated in a parallel animation between the times of "${startTime}ms" and "${endTime}ms"`);
updateCollectedStyle = false;
}
// we always choose the smaller start time value since we
// want to have a record of the entire animation window where
// the style property is being animated in between
startTime = collectedEntry.startTime;
}
if (updateCollectedStyle) {
context.collectedStyles[prop] = {startTime, endTime};
}
});
}
visitKeyframeSequence(
ast: AnimationKeyframesSequenceMetadata, context: AnimationValidatorContext): any {
let totalKeyframesWithOffsets = 0;
const offsets: number[] = [];
let offsetsOutOfOrder = false;
let keyframesOutOfRange = false;
let previousOffset: number = 0;
ast.steps.forEach(step => {
const styleData = normalizeStyles(step.styles);
let offset = 0;
if (styleData.hasOwnProperty('offset')) {
totalKeyframesWithOffsets++;
offset = <number>styleData['offset'];
}
keyframesOutOfRange = keyframesOutOfRange || offset < 0 || offset > 1;
offsetsOutOfOrder = offsetsOutOfOrder || offset < previousOffset;
previousOffset = offset;
offsets.push(offset);
});
if (keyframesOutOfRange) {
context.errors.push(`Please ensure that all keyframe offsets are between 0 and 1`);
}
if (offsetsOutOfOrder) {
context.errors.push(`Please ensure that all keyframe offsets are in order`);
}
const length = ast.steps.length;
let generatedOffset = 0;
if (totalKeyframesWithOffsets > 0 && totalKeyframesWithOffsets < length) {
context.errors.push(`Not all style() steps within the declared keyframes() contain offsets`);
} else if (totalKeyframesWithOffsets == 0) {
generatedOffset = 1 / length;
}
const limit = length - 1;
const currentTime = context.currentTime;
const animateDuration = context.currentAnimateTimings !.duration;
ast.steps.forEach((step, i) => {
const offset = generatedOffset > 0 ? (i == limit ? 1 : (generatedOffset * i)) : offsets[i];
const durationUpToThisFrame = offset * animateDuration;
context.currentTime =
currentTime + context.currentAnimateTimings !.delay + durationUpToThisFrame;
context.currentAnimateTimings !.duration = durationUpToThisFrame;
this.visitStyle(step, context);
});
}
}
export class AnimationValidatorContext {
public errors: string[] = [];
public currentTime: number = 0;
public currentAnimateTimings: AnimateTimings|null;
public collectedStyles: {[propName: string]: StyleTimeTuple} = {};
}

View File

@ -0,0 +1,34 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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 {AnimationTimelineInstruction} from './animation_timeline_instruction';
export class ElementInstructionMap {
private _map = new Map<any, AnimationTimelineInstruction[]>();
consume(element: any): AnimationTimelineInstruction[] {
let instructions = this._map.get(element);
if (instructions) {
this._map.delete(element);
} else {
instructions = [];
}
return instructions;
}
append(element: any, instructions: AnimationTimelineInstruction[]) {
let existingInstructions = this._map.get(element);
if (!existingInstructions) {
this._map.set(element, existingInstructions = []);
}
existingInstructions.push(...instructions);
}
has(element: any): boolean { return this._map.has(element); }
clear() { this._map.clear(); }
}

View File

@ -10,7 +10,7 @@ export {Animation as ɵAnimation} from './dsl/animation';
export {AnimationStyleNormalizer as ɵAnimationStyleNormalizer, NoopAnimationStyleNormalizer as ɵNoopAnimationStyleNormalizer} from './dsl/style_normalization/animation_style_normalizer';
export {WebAnimationsStyleNormalizer as ɵWebAnimationsStyleNormalizer} from './dsl/style_normalization/web_animations_style_normalizer';
export {NoopAnimationDriver as ɵNoopAnimationDriver} from './render/animation_driver';
export {DomAnimationEngine as ɵDomAnimationEngine} from './render/dom_animation_engine';
export {DomAnimationEngine as ɵDomAnimationEngine} from './render/dom_animation_engine_next';
export {NoopAnimationEngine as ɵNoopAnimationEngine} from './render/noop_animation_engine';
export {WebAnimationsDriver as ɵWebAnimationsDriver, supportsWebAnimations as ɵsupportsWebAnimations} from './render/web_animations/web_animations_driver';
export {WebAnimationsPlayer as ɵWebAnimationsPlayer} from './render/web_animations/web_animations_player';

View File

@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ɵStyleData} from '@angular/animations';
import {AnimationPlayer, NoopAnimationPlayer} from '@angular/animations';
@ -14,6 +15,10 @@ import {AnimationPlayer, NoopAnimationPlayer} from '@angular/animations';
* @experimental
*/
export class NoopAnimationDriver implements AnimationDriver {
computeStyle(element: any, prop: string, defaultValue?: string): string {
return defaultValue || '';
}
animate(
element: any, keyframes: {[key: string]: string | number}[], duration: number, delay: number,
easing: string, previousPlayers: any[] = []): AnimationPlayer {
@ -26,6 +31,9 @@ export class NoopAnimationDriver implements AnimationDriver {
*/
export abstract class AnimationDriver {
static NOOP: AnimationDriver = new NoopAnimationDriver();
abstract computeStyle(element: any, prop: string, defaultValue?: string): string;
abstract animate(
element: any, keyframes: {[key: string]: string | number}[], duration: number, delay: number,
easing?: string|null, previousPlayers?: any[]): any;

View File

@ -1,527 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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 {AnimationEvent, AnimationPlayer, AnimationTriggerMetadata, NoopAnimationPlayer, ɵAnimationGroupPlayer, ɵStyleData} from '@angular/animations';
import {AnimationTimelineInstruction} from '../dsl/animation_timeline_instruction';
import {AnimationTransitionInstruction} from '../dsl/animation_transition_instruction';
import {AnimationTrigger, buildTrigger} from '../dsl/animation_trigger';
import {AnimationStyleNormalizer} from '../dsl/style_normalization/animation_style_normalizer';
import {eraseStyles, setStyles} from '../util';
import {AnimationDriver} from './animation_driver';
export interface QueuedAnimationTransitionTuple {
element: any;
player: AnimationPlayer;
triggerName: string;
event: AnimationEvent;
}
export interface TriggerListenerTuple {
triggerName: string;
phase: string;
callback: (event: any) => any;
}
const MARKED_FOR_ANIMATION_CLASSNAME = 'ng-animating';
const MARKED_FOR_ANIMATION_SELECTOR = '.ng-animating';
const MARKED_FOR_REMOVAL = '$$ngRemove';
const VOID_STATE = 'void';
export class DomAnimationEngine {
private _flaggedInserts = new Set<any>();
private _queuedRemovals = new Map<any, () => any>();
private _queuedTransitionAnimations: QueuedAnimationTransitionTuple[] = [];
private _activeTransitionAnimations = new Map<any, {[triggerName: string]: AnimationPlayer}>();
private _activeElementAnimations = new Map<any, AnimationPlayer[]>();
private _elementTriggerStates = new Map<any, {[triggerName: string]: string}>();
private _triggers: {[triggerName: string]: AnimationTrigger} = Object.create(null);
private _triggerListeners = new Map<any, TriggerListenerTuple[]>();
private _pendingListenerRemovals = new Map<any, TriggerListenerTuple[]>();
constructor(private _driver: AnimationDriver, private _normalizer: AnimationStyleNormalizer) {}
get queuedPlayers(): AnimationPlayer[] {
return this._queuedTransitionAnimations.map(q => q.player);
}
get activePlayers(): AnimationPlayer[] {
const players: AnimationPlayer[] = [];
this._activeElementAnimations.forEach(activePlayers => players.push(...activePlayers));
return players;
}
registerTrigger(trigger: AnimationTriggerMetadata, name?: string): void {
name = name || trigger.name;
if (this._triggers[name]) {
return;
}
this._triggers[name] = buildTrigger(name, trigger.definitions);
}
onInsert(element: any, domFn: () => any): void {
if (element['nodeType'] == 1) {
this._flaggedInserts.add(element);
}
domFn();
}
onRemove(element: any, domFn: () => any): void {
if (element['nodeType'] != 1) {
domFn();
return;
}
let lookupRef = this._elementTriggerStates.get(element);
if (lookupRef) {
const possibleTriggers = Object.keys(lookupRef);
const hasRemoval = possibleTriggers.some(triggerName => {
const oldValue = lookupRef ![triggerName];
const instruction = this._triggers[triggerName].matchTransition(oldValue, VOID_STATE);
return !!instruction;
});
if (hasRemoval) {
element[MARKED_FOR_REMOVAL] = true;
this._queuedRemovals.set(element, domFn);
return;
}
}
// this means that there are no animations to take on this
// leave operation therefore we should fire the done|start callbacks
if (this._triggerListeners.has(element)) {
element[MARKED_FOR_REMOVAL] = true;
this._queuedRemovals.set(element, () => {});
}
this._onRemovalTransition(element).forEach(player => player.destroy());
domFn();
}
setProperty(element: any, property: string, value: any): void {
const trigger = this._triggers[property];
if (!trigger) {
throw new Error(`The provided animation trigger "${property}" has not been registered!`);
}
let lookupRef = this._elementTriggerStates.get(element);
if (!lookupRef) {
this._elementTriggerStates.set(element, lookupRef = {});
}
let oldValue = lookupRef.hasOwnProperty(property) ? lookupRef[property] : VOID_STATE;
if (oldValue !== value) {
value = normalizeTriggerValue(value);
let instruction = trigger.matchTransition(oldValue, value);
if (!instruction) {
// we do this to make sure we always have an animation player so
// that callback operations are properly called
instruction = trigger.createFallbackInstruction(oldValue, value);
}
this.animateTransition(element, instruction);
lookupRef[property] = value;
}
}
listen(element: any, eventName: string, eventPhase: string, callback: (event: any) => any):
() => void {
if (!eventPhase) {
throw new Error(
`Unable to listen on the animation trigger "${eventName}" because the provided event is undefined!`);
}
if (!this._triggers[eventName]) {
throw new Error(
`Unable to listen on the animation trigger event "${eventPhase}" because the animation trigger "${eventName}" doesn't exist!`);
}
let elementListeners = this._triggerListeners.get(element);
if (!elementListeners) {
this._triggerListeners.set(element, elementListeners = []);
}
validatePlayerEvent(eventName, eventPhase);
const tuple = <TriggerListenerTuple>{triggerName: eventName, phase: eventPhase, callback};
elementListeners.push(tuple);
return () => {
// this is queued up in the event that a removal animation is set
// to fire on the element (the listeners need to be set during flush)
getOrSetAsInMap(this._pendingListenerRemovals, element, []).push(tuple);
};
}
private _clearPendingListenerRemovals() {
this._pendingListenerRemovals.forEach((tuples: TriggerListenerTuple[], element: any) => {
const elementListeners = this._triggerListeners.get(element);
if (elementListeners) {
tuples.forEach(tuple => {
const index = elementListeners.indexOf(tuple);
if (index >= 0) {
elementListeners.splice(index, 1);
}
});
}
});
this._pendingListenerRemovals.clear();
}
private _onRemovalTransition(element: any): AnimationPlayer[] {
// when a parent animation is set to trigger a removal we want to
// find all of the children that are currently animating and clear
// them out by destroying each of them.
const elms = element.querySelectorAll(MARKED_FOR_ANIMATION_SELECTOR);
for (let i = 0; i < elms.length; i++) {
const elm = elms[i];
const activePlayers = this._activeElementAnimations.get(elm);
if (activePlayers) {
activePlayers.forEach(player => player.destroy());
}
const activeTransitions = this._activeTransitionAnimations.get(elm);
if (activeTransitions) {
Object.keys(activeTransitions).forEach(triggerName => {
const player = activeTransitions[triggerName];
if (player) {
player.destroy();
}
});
}
}
// we make a copy of the array because the actual source array is modified
// each time a player is finished/destroyed (the forEach loop would fail otherwise)
return copyArray(this._activeElementAnimations.get(element) !);
}
animateTransition(element: any, instruction: AnimationTransitionInstruction): AnimationPlayer {
const triggerName = instruction.triggerName;
let previousPlayers: AnimationPlayer[];
if (instruction.isRemovalTransition) {
previousPlayers = this._onRemovalTransition(element);
} else {
previousPlayers = [];
const existingTransitions = this._activeTransitionAnimations.get(element);
const existingPlayer = existingTransitions ? existingTransitions[triggerName] : null;
if (existingPlayer) {
previousPlayers.push(existingPlayer);
}
}
// it's important to do this step before destroying the players
// so that the onDone callback below won't fire before this
eraseStyles(element, instruction.fromStyles);
// we first run this so that the previous animation player
// data can be passed into the successive animation players
let totalTime = 0;
const players = instruction.timelines.map((timelineInstruction, i) => {
totalTime = Math.max(totalTime, timelineInstruction.totalTime);
return this._buildPlayer(element, timelineInstruction, previousPlayers, i);
});
previousPlayers.forEach(previousPlayer => previousPlayer.destroy());
const player = optimizeGroupPlayer(players);
player.onDone(() => {
player.destroy();
const elmTransitionMap = this._activeTransitionAnimations.get(element);
if (elmTransitionMap) {
delete elmTransitionMap[triggerName];
if (Object.keys(elmTransitionMap).length == 0) {
this._activeTransitionAnimations.delete(element);
}
}
deleteFromArrayMap(this._activeElementAnimations, element, player);
setStyles(element, instruction.toStyles);
});
const elmTransitionMap = getOrSetAsInMap(this._activeTransitionAnimations, element, {});
elmTransitionMap[triggerName] = player;
this._queuePlayer(
element, triggerName, player,
makeAnimationEvent(
element, triggerName, instruction.fromState, instruction.toState,
null, // this will be filled in during event creation
totalTime));
return player;
}
public animateTimeline(
element: any, instructions: AnimationTimelineInstruction[],
previousPlayers: AnimationPlayer[] = []): AnimationPlayer {
const players = instructions.map((instruction, i) => {
const player = this._buildPlayer(element, instruction, previousPlayers, i);
player.onDestroy(
() => { deleteFromArrayMap(this._activeElementAnimations, element, player); });
this._markPlayerAsActive(element, player);
return player;
});
return optimizeGroupPlayer(players);
}
private _buildPlayer(
element: any, instruction: AnimationTimelineInstruction, previousPlayers: AnimationPlayer[],
index: number = 0): AnimationPlayer {
// only the very first animation can absorb the previous styles. This
// is here to prevent the an overlap situation where a group animation
// absorbs previous styles multiple times for the same element.
if (index && previousPlayers.length) {
previousPlayers = [];
}
return this._driver.animate(
element, this._normalizeKeyframes(instruction.keyframes), instruction.duration,
instruction.delay, instruction.easing, previousPlayers);
}
private _normalizeKeyframes(keyframes: ɵStyleData[]): ɵStyleData[] {
const errors: string[] = [];
const normalizedKeyframes: ɵStyleData[] = [];
keyframes.forEach(kf => {
const normalizedKeyframe: ɵStyleData = {};
Object.keys(kf).forEach(prop => {
let normalizedProp = prop;
let normalizedValue = kf[prop];
if (prop != 'offset') {
normalizedProp = this._normalizer.normalizePropertyName(prop, errors);
normalizedValue =
this._normalizer.normalizeStyleValue(prop, normalizedProp, kf[prop], errors);
}
normalizedKeyframe[normalizedProp] = normalizedValue;
});
normalizedKeyframes.push(normalizedKeyframe);
});
if (errors.length) {
const LINE_START = '\n - ';
throw new Error(
`Unable to animate due to the following errors:${LINE_START}${errors.join(LINE_START)}`);
}
return normalizedKeyframes;
}
private _markPlayerAsActive(element: any, player: AnimationPlayer) {
const elementAnimations = getOrSetAsInMap(this._activeElementAnimations, element, []);
elementAnimations.push(player);
}
private _queuePlayer(
element: any, triggerName: string, player: AnimationPlayer, event: AnimationEvent) {
const tuple = <QueuedAnimationTransitionTuple>{element, player, triggerName, event};
this._queuedTransitionAnimations.push(tuple);
player.init();
element.classList.add(MARKED_FOR_ANIMATION_CLASSNAME);
player.onDone(() => { element.classList.remove(MARKED_FOR_ANIMATION_CLASSNAME); });
}
private _flushQueuedAnimations() {
parentLoop: while (this._queuedTransitionAnimations.length) {
const {player, element, triggerName, event} = this._queuedTransitionAnimations.shift() !;
let parent = element;
while (parent = parent.parentNode) {
// this means that a parent element will or will not
// have its own animation operation which in this case
// there's no point in even trying to do an animation
if (parent[MARKED_FOR_REMOVAL]) continue parentLoop;
}
const listeners = this._triggerListeners.get(element);
if (listeners) {
listeners.forEach(tuple => {
if (tuple.triggerName == triggerName) {
listenOnPlayer(player, tuple.phase, event, tuple.callback);
}
});
}
// if a removal exists for the given element then we need cancel
// all the queued players so that a proper removal animation can go
if (this._queuedRemovals.has(element)) {
player.destroy();
continue;
}
this._markPlayerAsActive(element, player);
// in the event that an animation throws an error then we do
// not want to re-run animations on any previous animations
// if they have already been kicked off beforehand
player.init();
if (!player.hasStarted()) {
player.play();
}
}
}
flush() {
const leaveListeners = new Map<any, TriggerListenerTuple[]>();
this._queuedRemovals.forEach((callback, element) => {
const tuple = this._pendingListenerRemovals.get(element);
if (tuple) {
leaveListeners.set(element, tuple);
this._pendingListenerRemovals.delete(element);
}
});
this._clearPendingListenerRemovals();
this._pendingListenerRemovals = leaveListeners;
this._flushQueuedAnimations();
let flushAgain = false;
this._queuedRemovals.forEach((callback, element) => {
// an item that was inserted/removed in the same flush means
// that an animation should not happen anyway
if (this._flaggedInserts.has(element)) return;
let parent = element;
let players: AnimationPlayer[] = [];
while (parent = parent.parentNode) {
// there is no reason to even try to
if (parent[MARKED_FOR_REMOVAL]) {
callback();
return;
}
const match = this._activeElementAnimations.get(parent);
if (match) {
players.push(...match);
break;
}
}
// the loop was unable to find an parent that is animating even
// though this element has set to be removed, so the algorithm
// should check to see if there are any triggers on the element
// that are present to handle a leave animation and then setup
// those players to facilitate the callback after done
if (players.length == 0) {
// this means that the element has valid state triggers
const stateDetails = this._elementTriggerStates.get(element);
if (stateDetails) {
Object.keys(stateDetails).forEach(triggerName => {
flushAgain = true;
const oldValue = stateDetails[triggerName];
const instruction = this._triggers[triggerName].matchTransition(oldValue, VOID_STATE);
if (instruction) {
players.push(this.animateTransition(element, instruction));
} else {
const event = makeAnimationEvent(element, triggerName, oldValue, VOID_STATE, '', 0);
const player = new NoopAnimationPlayer();
this._queuePlayer(element, triggerName, player, event);
}
});
}
}
if (players.length) {
optimizeGroupPlayer(players).onDone(callback);
} else {
callback();
}
});
this._queuedRemovals.clear();
this._flaggedInserts.clear();
// this means that one or more leave animations were detected
if (flushAgain) {
this._flushQueuedAnimations();
this._clearPendingListenerRemovals();
}
}
}
function getOrSetAsInMap(map: Map<any, any>, key: any, defaultValue: any) {
let value = map.get(key);
if (!value) {
map.set(key, value = defaultValue);
}
return value;
}
function deleteFromArrayMap(map: Map<any, any[]>, key: any, value: any) {
let arr = map.get(key);
if (arr) {
const index = arr.indexOf(value);
if (index >= 0) {
arr.splice(index, 1);
if (arr.length == 0) {
map.delete(key);
}
}
}
}
function optimizeGroupPlayer(players: AnimationPlayer[]): AnimationPlayer {
switch (players.length) {
case 0:
return new NoopAnimationPlayer();
case 1:
return players[0];
default:
return new ɵAnimationGroupPlayer(players);
}
}
function copyArray(source: any[]): any[] {
return source ? source.splice(0) : [];
}
function validatePlayerEvent(triggerName: string, eventName: string) {
switch (eventName) {
case 'start':
case 'done':
return;
default:
throw new Error(
`The provided animation trigger event "${eventName}" for the animation trigger "${triggerName}" is not supported!`);
}
}
function listenOnPlayer(
player: AnimationPlayer, eventName: string, baseEvent: AnimationEvent,
callback: (event: any) => any) {
switch (eventName) {
case 'start':
player.onStart(() => {
const event = copyAnimationEvent(baseEvent);
event.phaseName = 'start';
callback(event);
});
break;
case 'done':
player.onDone(() => {
const event = copyAnimationEvent(baseEvent);
event.phaseName = 'done';
callback(event);
});
break;
}
}
function copyAnimationEvent(e: AnimationEvent): AnimationEvent {
return makeAnimationEvent(
e.element, e.triggerName, e.fromState, e.toState, e.phaseName, e.totalTime);
}
function makeAnimationEvent(
element: any, triggerName: string, fromState: string, toState: string, phaseName: string | null,
totalTime: number): AnimationEvent {
return <AnimationEvent>{element, triggerName, fromState, toState, phaseName, totalTime};
}
function normalizeTriggerValue(value: any): string {
switch (typeof value) {
case 'boolean':
return value ? '1' : '0';
default:
return value ? value.toString() : null;
}
}

View File

@ -0,0 +1,97 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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 {AnimationMetadata, AnimationPlayer, AnimationTriggerMetadata} from '@angular/animations';
import {AnimationEngine} from '../animation_engine';
import {TriggerAst} from '../dsl/animation_ast';
import {buildAnimationAst} from '../dsl/animation_ast_builder';
import {AnimationTrigger, buildTrigger} from '../dsl/animation_trigger';
import {AnimationStyleNormalizer} from '../dsl/style_normalization/animation_style_normalizer';
import {AnimationDriver} from './animation_driver';
import {parseTimelineCommand} from './shared';
import {TimelineAnimationEngine} from './timeline_animation_engine';
import {TransitionAnimationEngine} from './transition_animation_engine';
export class DomAnimationEngine implements AnimationEngine {
private _transitionEngine: TransitionAnimationEngine;
private _timelineEngine: TimelineAnimationEngine;
private _triggerCache: {[key: string]: AnimationTrigger} = {};
// this method is designed to be overridden by the code that uses this engine
public onRemovalComplete = (element: any, context: any) => {};
constructor(driver: AnimationDriver, normalizer: AnimationStyleNormalizer) {
this._transitionEngine = new TransitionAnimationEngine(driver, normalizer);
this._timelineEngine = new TimelineAnimationEngine(driver, normalizer);
this._transitionEngine.onRemovalComplete =
(element: any, context: any) => { this.onRemovalComplete(element, context); }
}
registerTrigger(
componentId: string, namespaceId: string, hostElement: any, name: string,
metadata: AnimationTriggerMetadata): void {
const cacheKey = componentId + '-' + name;
let trigger = this._triggerCache[cacheKey];
if (!trigger) {
const errors: any[] = [];
const ast = buildAnimationAst(metadata as AnimationMetadata, errors) as TriggerAst;
if (errors.length) {
throw new Error(
`The animation trigger "${name}" has failed to build due to the following errors:\n - ${errors.join("\n - ")}`);
}
trigger = buildTrigger(name, ast);
this._triggerCache[cacheKey] = trigger;
}
this._transitionEngine.register(namespaceId, hostElement, name, trigger);
}
destroy(namespaceId: string, context: any) {
this._transitionEngine.destroy(namespaceId, context);
}
onInsert(namespaceId: string, element: any, parent: any, insertBefore: boolean): void {
this._transitionEngine.insertNode(namespaceId, element, parent, insertBefore);
}
onRemove(namespaceId: string, element: any, context: any): void {
this._transitionEngine.removeNode(namespaceId, element, context);
}
setProperty(namespaceId: string, element: any, property: string, value: any): boolean {
// @@property
if (property.charAt(0) == '@') {
const [id, action] = parseTimelineCommand(property);
const args = value as any[];
this._timelineEngine.command(id, element, action, args);
return false;
}
return this._transitionEngine.trigger(namespaceId, element, property, value);
}
listen(
namespaceId: string, element: any, eventName: string, eventPhase: string,
callback: (event: any) => any): () => any {
// @@listen
if (eventName.charAt(0) == '@') {
const [id, action] = parseTimelineCommand(eventName);
return this._timelineEngine.listen(id, element, action, callback);
}
return this._transitionEngine.listen(namespaceId, element, eventName, eventPhase, callback);
}
flush(): void { this._transitionEngine.flush(); }
get players(): AnimationPlayer[] {
return (this._transitionEngine.players as AnimationPlayer[])
.concat(this._timelineEngine.players as AnimationPlayer[]);
}
}

View File

@ -5,20 +5,30 @@
* 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 {AnimationEvent, AnimationMetadataType, AnimationPlayer, AnimationStateMetadata, AnimationTriggerMetadata, ɵStyleData} from '@angular/animations';
import {AnimationEvent, AnimationPlayer, AnimationTriggerMetadata, ɵStyleData} from '@angular/animations';
import {AnimationEngine} from '../animation_engine';
import {TriggerAst} from '../dsl/animation_ast';
import {buildAnimationAst} from '../dsl/animation_ast_builder';
import {buildTrigger} from '../dsl/animation_trigger';
import {AnimationStyleNormalizer} from '../dsl/style_normalization/animation_style_normalizer';
import {copyStyles, eraseStyles, normalizeStyles, setStyles} from '../util';
import {AnimationDriver} from './animation_driver';
import {parseTimelineCommand} from './shared';
import {TimelineAnimationEngine} from './timeline_animation_engine';
interface ListenerTuple {
eventPhase: string;
triggerName: string;
namespacedName: string;
callback: (event: any) => any;
doRemove?: boolean;
}
interface ChangeTuple {
element: any;
namespacedName: string;
triggerName: string;
oldValue: string;
newValue: string;
@ -35,36 +45,55 @@ export class NoopAnimationEngine extends AnimationEngine {
private _triggerStyles: {[triggerName: string]: {[stateName: string]: ɵStyleData}} =
Object.create(null);
registerTrigger(trigger: AnimationTriggerMetadata, name?: string): void {
name = name || trigger.name;
private _timelineEngine: TimelineAnimationEngine;
// this method is designed to be overridden by the code that uses this engine
public onRemovalComplete = (element: any, context: any) => {};
constructor(driver: AnimationDriver, normalizer: AnimationStyleNormalizer) {
super();
this._timelineEngine = new TimelineAnimationEngine(driver, normalizer);
}
registerTrigger(
componentId: string, namespaceId: string, hostElement: any, name: string,
metadata: AnimationTriggerMetadata): void {
name = name || metadata.name;
name = namespaceId + '#' + name;
if (this._triggerStyles[name]) {
return;
}
const stateMap: {[stateName: string]: ɵStyleData} = {};
trigger.definitions.forEach(def => {
if (def.type === AnimationMetadataType.State) {
const stateDef = def as AnimationStateMetadata;
stateMap[stateDef.name] = normalizeStyles(stateDef.styles.styles);
}
});
this._triggerStyles[name] = stateMap;
const errors: any[] = [];
const ast = buildAnimationAst(metadata, errors) as TriggerAst;
const trigger = buildTrigger(name, ast);
this._triggerStyles[name] = trigger.states;
}
onInsert(element: any, domFn: () => any): void { domFn(); }
onInsert(namespaceId: string, element: any, parent: any, insertBefore: boolean): void {}
onRemove(element: any, domFn: () => any): void {
domFn();
onRemove(namespaceId: string, element: any, context: any): void {
this.onRemovalComplete(element, context);
if (element['nodeType'] == 1) {
this._flaggedRemovals.add(element);
}
}
setProperty(element: any, property: string, value: any): void {
const storageProp = makeStorageProp(property);
const oldValue = element[storageProp] || DEFAULT_STATE_VALUE;
this._changes.push(<ChangeTuple>{element, oldValue, newValue: value, triggerName: property});
setProperty(namespaceId: string, element: any, property: string, value: any): boolean {
if (property.charAt(0) == '@') {
const [id, action] = parseTimelineCommand(property);
const args = value as any[];
this._timelineEngine.command(id, element, action, args);
return false;
}
const triggerStateStyles = this._triggerStyles[property] || {};
const namespacedName = namespaceId + '#' + property;
const storageProp = makeStorageProp(namespacedName);
const oldValue = element[storageProp] || DEFAULT_STATE_VALUE;
this._changes.push(
<ChangeTuple>{element, oldValue, newValue: value, triggerName: property, namespacedName});
const triggerStateStyles = this._triggerStyles[namespacedName] || {};
const fromStateStyles =
triggerStateStyles[oldValue] || triggerStateStyles[DEFAULT_STATE_STYLES];
if (fromStateStyles) {
@ -78,16 +107,27 @@ export class NoopAnimationEngine extends AnimationEngine {
setStyles(element, toStateStyles);
}
});
return true;
}
listen(element: any, eventName: string, eventPhase: string, callback: (event: any) => any):
() => any {
listen(
namespaceId: string, element: any, eventName: string, eventPhase: string,
callback: (event: any) => any): () => any {
if (eventName.charAt(0) == '@') {
const [id, action] = parseTimelineCommand(eventName);
return this._timelineEngine.listen(id, element, action, callback);
}
let listeners = this._listeners.get(element);
if (!listeners) {
this._listeners.set(element, listeners = []);
}
const tuple = <ListenerTuple>{triggerName: eventName, eventPhase, callback};
const tuple = <ListenerTuple>{
namespacedName: namespaceId + '#' + eventName,
triggerName: eventName, eventPhase, callback
};
listeners.push(tuple);
return () => tuple.doRemove = true;
@ -113,7 +153,7 @@ export class NoopAnimationEngine extends AnimationEngine {
const listeners = this._listeners.get(element);
if (listeners) {
listeners.forEach(listener => {
if (listener.triggerName == change.triggerName) {
if (listener.namespacedName == change.namespacedName) {
handleListener(listener, change);
}
});
@ -126,10 +166,12 @@ export class NoopAnimationEngine extends AnimationEngine {
if (listeners) {
listeners.forEach(listener => {
const triggerName = listener.triggerName;
const storageProp = makeStorageProp(triggerName);
const namespacedName = listener.namespacedName;
const storageProp = makeStorageProp(namespacedName);
handleListener(listener, <ChangeTuple>{
element: element,
triggerName: triggerName,
element,
triggerName,
namespacedName: listener.namespacedName,
oldValue: element[storageProp] || DEFAULT_STATE_VALUE,
newValue: DEFAULT_STATE_VALUE
});
@ -156,8 +198,9 @@ export class NoopAnimationEngine extends AnimationEngine {
this._onDoneFns = [];
}
get activePlayers(): AnimationPlayer[] { return []; }
get queuedPlayers(): AnimationPlayer[] { return []; }
get players(): AnimationPlayer[] { return []; }
destroy(namespaceId: string) {}
}
function makeAnimationEvent(

View File

@ -0,0 +1,116 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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 {AUTO_STYLE, AnimationEvent, AnimationPlayer, NoopAnimationPlayer, ɵAnimationGroupPlayer, ɵPRE_STYLE as PRE_STYLE, ɵStyleData} from '@angular/animations';
import {AnimationStyleNormalizer} from '../../src/dsl/style_normalization/animation_style_normalizer';
import {AnimationDriver} from '../../src/render/animation_driver';
export function optimizeGroupPlayer(players: AnimationPlayer[]): AnimationPlayer {
switch (players.length) {
case 0:
return new NoopAnimationPlayer();
case 1:
return players[0];
default:
return new ɵAnimationGroupPlayer(players);
}
}
export function normalizeKeyframes(
driver: AnimationDriver, normalizer: AnimationStyleNormalizer, element: any,
keyframes: ɵStyleData[], preStyles: ɵStyleData = {},
postStyles: ɵStyleData = {}): ɵStyleData[] {
const errors: string[] = [];
const normalizedKeyframes: ɵStyleData[] = [];
let previousOffset = -1;
let previousKeyframe: ɵStyleData|null = null;
keyframes.forEach(kf => {
const offset = kf['offset'] as number;
const isSameOffset = offset == previousOffset;
const normalizedKeyframe: ɵStyleData = (isSameOffset && previousKeyframe) || {};
Object.keys(kf).forEach(prop => {
let normalizedProp = prop;
let normalizedValue = kf[prop];
if (normalizedValue == PRE_STYLE) {
normalizedValue = preStyles[prop];
} else if (normalizedValue == AUTO_STYLE) {
normalizedValue = postStyles[prop];
} else if (prop != 'offset') {
normalizedProp = normalizer.normalizePropertyName(prop, errors);
normalizedValue = normalizer.normalizeStyleValue(prop, normalizedProp, kf[prop], errors);
}
normalizedKeyframe[normalizedProp] = normalizedValue;
});
if (!isSameOffset) {
normalizedKeyframes.push(normalizedKeyframe);
}
previousKeyframe = normalizedKeyframe;
previousOffset = offset;
});
if (errors.length) {
const LINE_START = '\n - ';
throw new Error(
`Unable to animate due to the following errors:${LINE_START}${errors.join(LINE_START)}`);
}
return normalizedKeyframes;
}
export function listenOnPlayer(
player: AnimationPlayer, eventName: string, event: AnimationEvent | undefined,
callback: (event: any) => any) {
switch (eventName) {
case 'start':
player.onStart(() => callback(event && copyAnimationEvent(event, 'start', player.totalTime)));
break;
case 'done':
player.onDone(() => callback(event && copyAnimationEvent(event, 'done', player.totalTime)));
break;
case 'destroy':
player.onDestroy(
() => callback(event && copyAnimationEvent(event, 'destroy', player.totalTime)));
break;
}
}
export function copyAnimationEvent(
e: AnimationEvent, phaseName?: string, totalTime?: number): AnimationEvent {
return makeAnimationEvent(
e.element, e.triggerName, e.fromState, e.toState, phaseName || e.phaseName,
totalTime == undefined ? e.totalTime : totalTime);
}
export function makeAnimationEvent(
element: any, triggerName: string, fromState: string, toState: string, phaseName: string = '',
totalTime: number = 0): AnimationEvent {
return {element, triggerName, fromState, toState, phaseName, totalTime};
}
export function getOrSetAsInMap(
map: Map<any, any>| {[key: string]: any}, key: any, defaultValue: any) {
let value: any;
if (map instanceof Map) {
value = map.get(key);
if (!value) {
map.set(key, value = defaultValue);
}
} else {
value = map[key];
if (!value) {
value = map[key] = defaultValue;
}
}
return value;
}
export function parseTimelineCommand(command: string): [string, string] {
const separatorPos = command.indexOf(':');
const id = command.substring(1, separatorPos);
const action = command.substr(separatorPos + 1);
return [id, action];
}

View File

@ -0,0 +1,156 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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 {AUTO_STYLE, AnimationMetadata, AnimationOptions, AnimationPlayer, ɵStyleData} from '@angular/animations';
import {Ast} from '../dsl/animation_ast';
import {buildAnimationAst} from '../dsl/animation_ast_builder';
import {buildAnimationTimelines} from '../dsl/animation_timeline_builder';
import {AnimationTimelineInstruction} from '../dsl/animation_timeline_instruction';
import {ElementInstructionMap} from '../dsl/element_instruction_map';
import {AnimationStyleNormalizer} from '../dsl/style_normalization/animation_style_normalizer';
import {AnimationDriver} from './animation_driver';
import {getOrSetAsInMap, listenOnPlayer, makeAnimationEvent, normalizeKeyframes, optimizeGroupPlayer} from './shared';
const EMPTY_INSTRUCTION_MAP = new ElementInstructionMap();
export class TimelineAnimationEngine {
private _animations: {[id: string]: Ast} = {};
private _playersById: {[id: string]: AnimationPlayer} = {};
public players: AnimationPlayer[] = [];
constructor(private _driver: AnimationDriver, private _normalizer: AnimationStyleNormalizer) {}
register(id: string, metadata: AnimationMetadata|AnimationMetadata[]) {
const errors: any[] = [];
const ast = buildAnimationAst(metadata, errors);
if (errors.length) {
throw new Error(
`Unable to build the animation due to the following errors: ${errors.join("\n")}`);
} else {
this._animations[id] = ast;
}
}
private _buildPlayer(
i: AnimationTimelineInstruction, preStyles: ɵStyleData,
postStyles?: ɵStyleData): AnimationPlayer {
const element = i.element;
const keyframes = normalizeKeyframes(
this._driver, this._normalizer, element, i.keyframes, preStyles, postStyles);
return this._driver.animate(element, keyframes, i.duration, i.delay, i.easing, []);
}
create(id: string, element: any, options: AnimationOptions = {}): AnimationPlayer {
const errors: any[] = [];
const ast = this._animations[id];
let instructions: AnimationTimelineInstruction[];
const autoStylesMap = new Map<any, ɵStyleData>();
if (ast) {
instructions =
buildAnimationTimelines(element, ast, {}, {}, options, EMPTY_INSTRUCTION_MAP, errors);
instructions.forEach(inst => {
const styles = getOrSetAsInMap(autoStylesMap, inst.element, {});
inst.postStyleProps.forEach(prop => styles[prop] = null);
});
} else {
errors.push('The requested animation doesn\'t exist or has already been destroyed');
instructions = [];
}
if (errors.length) {
throw new Error(
`Unable to create the animation due to the following errors: ${errors.join("\n")}`);
}
autoStylesMap.forEach((styles, element) => {
Object.keys(styles).forEach(
prop => { styles[prop] = this._driver.computeStyle(element, prop, AUTO_STYLE); });
});
const players = instructions.map(i => {
const styles = autoStylesMap.get(i.element);
return this._buildPlayer(i, {}, styles);
});
const player = optimizeGroupPlayer(players);
this._playersById[id] = player;
player.onDestroy(() => this.destroy(id));
this.players.push(player);
return player;
}
destroy(id: string) {
const player = this._getPlayer(id);
player.destroy();
delete this._playersById[id];
const index = this.players.indexOf(player);
if (index >= 0) {
this.players.splice(index, 1);
}
}
private _getPlayer(id: string): AnimationPlayer {
const player = this._playersById[id];
if (!player) {
throw new Error(`Unable to find the timeline player referenced by ${id}`);
}
return player;
}
listen(id: string, element: string, eventName: string, callback: (event: any) => any):
() => void {
// triggerName, fromState, toState are all ignored for timeline animations
const baseEvent = makeAnimationEvent(element, '', '', '');
listenOnPlayer(this._getPlayer(id), eventName, baseEvent, callback);
return () => {};
}
command(id: string, element: any, command: string, args: any[]): void {
if (command == 'register') {
this.register(id, args[0] as AnimationMetadata | AnimationMetadata[]);
return;
}
if (command == 'create') {
const options = (args[0] || {}) as AnimationOptions;
this.create(id, element, options);
return;
}
const player = this._getPlayer(id);
switch (command) {
case 'play':
player.play();
break;
case 'pause':
player.pause();
break;
case 'reset':
player.reset();
break;
case 'restart':
player.restart();
break;
case 'finish':
player.finish();
break;
case 'init':
player.init();
break;
case 'setPosition':
player.setPosition(parseFloat(args[0] as string));
break;
case 'destroy':
this.destroy(id);
break;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -12,11 +12,15 @@ import {AnimationDriver} from '../animation_driver';
import {WebAnimationsPlayer} from './web_animations_player';
export class WebAnimationsDriver implements AnimationDriver {
computeStyle(element: any, prop: string, defaultValue?: string): string {
return (window.getComputedStyle(element) as any)[prop] as string;
}
animate(
element: any, keyframes: ɵStyleData[], duration: number, delay: number, easing: string,
previousPlayers: AnimationPlayer[] = []): WebAnimationsPlayer {
const playerOptions: {[key: string]: string |
number} = {'duration': duration, 'delay': delay, 'fill': 'forwards'};
const fill = delay == 0 ? 'both' : 'forwards';
const playerOptions: {[key: string]: string | number} = {duration, delay, fill};
// we check for this to avoid having a null|undefined value be present
// for the easing (which results in an error for certain browsers #9752)

View File

@ -5,7 +5,10 @@
* 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 {AUTO_STYLE, AnimationPlayer} from '@angular/animations';
import {AnimationPlayer} from '@angular/animations';
import {copyStyles, eraseStyles, setStyles} from '../../util';
import {DOMAnimation} from './dom_animation';
export class WebAnimationsPlayer implements AnimationPlayer {
@ -24,18 +27,19 @@ export class WebAnimationsPlayer implements AnimationPlayer {
public parentPlayer: AnimationPlayer|null = null;
public previousStyles: {[styleName: string]: string | number};
public currentSnapshot: {[styleName: string]: string | number} = {};
constructor(
public element: any, public keyframes: {[key: string]: string | number}[],
public options: {[key: string]: string | number},
previousPlayers: WebAnimationsPlayer[] = []) {
private previousPlayers: WebAnimationsPlayer[] = []) {
this._duration = <number>options['duration'];
this._delay = <number>options['delay'] || 0;
this.time = this._duration + this._delay;
this.previousStyles = {};
previousPlayers.forEach(player => {
let styles = player._captureStyles();
let styles = player.currentSnapshot;
Object.keys(styles).forEach(prop => this.previousStyles[prop] = styles[prop]);
});
}
@ -52,20 +56,7 @@ export class WebAnimationsPlayer implements AnimationPlayer {
if (this._initialized) return;
this._initialized = true;
const keyframes = this.keyframes.map(styles => {
const formattedKeyframe: {[key: string]: string | number} = {};
Object.keys(styles).forEach((prop, index) => {
let value = styles[prop];
if (value == AUTO_STYLE) {
value = _computeStyle(this.element, prop);
}
if (value != undefined) {
formattedKeyframe[prop] = value;
}
});
return formattedKeyframe;
});
const keyframes = this.keyframes.map(styles => copyStyles(styles, false));
const previousStyleProps = Object.keys(this.previousStyles);
if (previousStyleProps.length) {
let startingKeyframe = keyframes[0];
@ -90,11 +81,14 @@ export class WebAnimationsPlayer implements AnimationPlayer {
}
this._player = this._triggerWebAnimation(this.element, keyframes, this.options);
this._finalKeyframe =
keyframes.length ? _copyKeyframeStyles(keyframes[keyframes.length - 1]) : {};
this._finalKeyframe = keyframes.length ? keyframes[keyframes.length - 1] : {};
// this is required so that the player doesn't start to animate right away
this._resetDomPlayerState();
if (this._delay) {
this._resetDomPlayerState();
} else {
this._player.pause();
}
this._player.addEventListener('finish', () => this._onFinish());
}
@ -168,7 +162,9 @@ export class WebAnimationsPlayer implements AnimationPlayer {
getPosition(): number { return this._player.currentTime / this.time; }
private _captureStyles(): {[prop: string]: string | number} {
get totalTime(): number { return this._delay + this._duration; }
beforeDestroy() {
const styles: {[key: string]: string | number} = {};
if (this.hasStarted()) {
Object.keys(this._finalKeyframe).forEach(prop => {
@ -178,22 +174,10 @@ export class WebAnimationsPlayer implements AnimationPlayer {
}
});
}
return styles;
this.currentSnapshot = styles;
}
}
function _computeStyle(element: any, prop: string): string {
return (<any>window.getComputedStyle(element))[prop];
}
function _copyKeyframeStyles(styles: {[style: string]: string | number}):
{[style: string]: string | number} {
const newStyles: {[style: string]: string | number} = {};
Object.keys(styles).forEach(prop => {
if (prop != 'offset') {
newStyles[prop] = styles[prop];
}
});
return newStyles;
}

View File

@ -5,37 +5,62 @@
* 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 {AnimateTimings, ɵStyleData} from '@angular/animations';
import {AnimateTimings, AnimationMetadata, AnimationOptions, sequence, ɵStyleData} from '@angular/animations';
export const ONE_SECOND = 1000;
export function parseTimeExpression(exp: string | number, errors: string[]): AnimateTimings {
const regex = /^([\.\d]+)(m?s)(?:\s+([\.\d]+)(m?s))?(?:\s+([-a-z]+(?:\(.+?\))?))?$/i;
export const ENTER_CLASSNAME = 'ng-enter';
export const LEAVE_CLASSNAME = 'ng-leave';
export const ENTER_SELECTOR = '.ng-enter';
export const LEAVE_SELECTOR = '.ng-leave';
export const NG_TRIGGER_CLASSNAME = 'ng-trigger';
export const NG_TRIGGER_SELECTOR = '.ng-trigger';
export const NG_ANIMATING_CLASSNAME = 'ng-animating';
export const NG_ANIMATING_SELECTOR = '.ng-animating';
export function resolveTimingValue(value: string | number) {
if (typeof value == 'number') return value;
const matches = (value as string).match(/^(-?[\.\d]+)(m?s)/);
if (!matches || matches.length < 2) return 0;
return _convertTimeValueToMS(parseFloat(matches[1]), matches[2]);
}
function _convertTimeValueToMS(value: number, unit: string): number {
switch (unit) {
case 's':
return value * ONE_SECOND;
default: // ms or something else
return value;
}
}
export function resolveTiming(
timings: string | number | AnimateTimings, errors: any[], allowNegativeValues?: boolean) {
return timings.hasOwnProperty('duration') ?
<AnimateTimings>timings :
parseTimeExpression(<string|number>timings, errors, allowNegativeValues);
}
function parseTimeExpression(
exp: string | number, errors: string[], allowNegativeValues?: boolean): AnimateTimings {
const regex = /^(-?[\.\d]+)(m?s)(?:\s+(-?[\.\d]+)(m?s))?(?:\s+([-a-z]+(?:\(.+?\))?))?$/i;
let duration: number;
let delay: number = 0;
let easing: string|null = null;
let easing: string = '';
if (typeof exp === 'string') {
const matches = exp.match(regex);
if (matches === null) {
errors.push(`The provided timing value "${exp}" is invalid.`);
return {duration: 0, delay: 0, easing: null};
return {duration: 0, delay: 0, easing: ''};
}
let durationMatch = parseFloat(matches[1]);
const durationUnit = matches[2];
if (durationUnit == 's') {
durationMatch *= ONE_SECOND;
}
duration = Math.floor(durationMatch);
duration = _convertTimeValueToMS(parseFloat(matches[1]), matches[2]);
const delayMatch = matches[3];
const delayUnit = matches[4];
if (delayMatch != null) {
let delayVal: number = parseFloat(delayMatch);
if (delayUnit != null && delayUnit == 's') {
delayVal *= ONE_SECOND;
}
delay = Math.floor(delayVal);
delay = _convertTimeValueToMS(Math.floor(parseFloat(delayMatch)), matches[4]);
}
const easingVal = matches[5];
@ -46,9 +71,31 @@ export function parseTimeExpression(exp: string | number, errors: string[]): Ani
duration = <number>exp;
}
if (!allowNegativeValues) {
let containsErrors = false;
let startIndex = errors.length;
if (duration < 0) {
errors.push(`Duration values below 0 are not allowed for this animation step.`);
containsErrors = true;
}
if (delay < 0) {
errors.push(`Delay values below 0 are not allowed for this animation step.`);
containsErrors = true;
}
if (containsErrors) {
errors.splice(startIndex, 0, `The provided timing value "${exp}" is invalid.`);
}
}
return {duration, delay, easing};
}
export function copyObj(
obj: {[key: string]: any}, destination: {[key: string]: any} = {}): {[key: string]: any} {
Object.keys(obj).forEach(prop => { destination[prop] = obj[prop]; });
return destination;
}
export function normalizeStyles(styles: ɵStyleData | ɵStyleData[]): ɵStyleData {
const normalizedStyles: ɵStyleData = {};
if (Array.isArray(styles)) {
@ -69,7 +116,7 @@ export function copyStyles(
destination[prop] = styles[prop];
}
} else {
Object.keys(styles).forEach(prop => destination[prop] = styles[prop]);
copyObj(styles, destination);
}
return destination;
}
@ -89,3 +136,73 @@ export function eraseStyles(element: any, styles: ɵStyleData) {
});
}
}
export function normalizeAnimationEntry(steps: AnimationMetadata | AnimationMetadata[]):
AnimationMetadata {
if (Array.isArray(steps)) {
if (steps.length == 1) return steps[0];
return sequence(steps);
}
return steps as AnimationMetadata;
}
export function validateStyleParams(
value: string | number, options: AnimationOptions, errors: any[]) {
const params = options.params || {};
if (typeof value !== 'string') return;
const matches = value.toString().match(PARAM_REGEX);
if (matches) {
matches.forEach(varName => {
if (!params.hasOwnProperty(varName)) {
errors.push(
`Unable to resolve the local animation param ${varName} in the given list of values`);
}
});
}
}
const PARAM_REGEX = /\{\{\s*(.+?)\s*\}\}/g;
export function interpolateParams(
value: string | number, params: {[name: string]: any}, errors: any[]): string|number {
const original = value.toString();
const str = original.replace(PARAM_REGEX, (_, varName) => {
let localVal = params[varName];
// this means that the value was never overidden by the data passed in by the user
if (!params.hasOwnProperty(varName)) {
errors.push(`Please provide a value for the animation param ${varName}`);
localVal = '';
}
return localVal.toString();
});
// we do this to assert that numeric values stay as they are
return str == original ? value : str;
}
export function iteratorToArray(iterator: any): any[] {
const arr: any[] = [];
let item = iterator.next();
while (!item.done) {
arr.push(item.value);
item = iterator.next();
}
return arr;
}
export function mergeAnimationOptions(
source: AnimationOptions, destination: AnimationOptions): AnimationOptions {
if (source.params) {
const p0 = source.params;
if (!destination.params) {
destination.params = {};
}
const p1 = destination.params;
Object.keys(p0).forEach(param => {
if (!p1.hasOwnProperty(param)) {
p1[param] = p0[param];
}
});
}
return destination;
}

View File

@ -5,14 +5,38 @@
* 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 {AUTO_STYLE, AnimationMetadata, AnimationMetadataType, animate, group, keyframes, sequence, style, ɵStyleData} from '@angular/animations';
import {AUTO_STYLE, AnimationMetadata, AnimationMetadataType, animate, group, keyframes, query, sequence, style, ɵStyleData} from '@angular/animations';
import {AnimationOptions} from '@angular/core/src/animation/dsl';
import {Animation} from '../../src/dsl/animation';
import {buildAnimationAst} from '../../src/dsl/animation_ast_builder';
import {AnimationTimelineInstruction} from '../../src/dsl/animation_timeline_instruction';
import {validateAnimationSequence} from '../../src/dsl/animation_validator_visitor';
import {ElementInstructionMap} from '../../src/dsl/element_instruction_map';
function createDiv() {
return document.createElement('div');
}
export function main() {
describe('Animation', () => {
// these tests are only mean't to be run within the DOM (for now)
if (typeof Element == 'undefined') return;
let rootElement: any;
let subElement1: any;
let subElement2: any;
beforeEach(() => {
rootElement = createDiv();
subElement1 = createDiv();
subElement2 = createDiv();
document.body.appendChild(rootElement);
rootElement.appendChild(subElement1);
rootElement.appendChild(subElement2);
});
afterEach(() => { document.body.removeChild(rootElement); });
describe('validation', () => {
it('should throw an error if one or more but not all keyframes() styles contain offsets',
() => {
@ -90,6 +114,48 @@ export function main() {
validateAndThrowAnimationSequence(steps2);
}).toThrowError(/The provided timing value "500ms 500ms 500ms ease-out" is invalid/);
});
it('should throw if negative durations are used', () => {
const steps = [animate(-1000, style({opacity: 1}))];
expect(() => {
validateAndThrowAnimationSequence(steps);
}).toThrowError(/Duration values below 0 are not allowed for this animation step/);
const steps2 = [animate('-1s', style({opacity: 1}))];
expect(() => {
validateAndThrowAnimationSequence(steps2);
}).toThrowError(/Duration values below 0 are not allowed for this animation step/);
});
it('should throw if negative delays are used', () => {
const steps = [animate('1s -500ms', style({opacity: 1}))];
expect(() => {
validateAndThrowAnimationSequence(steps);
}).toThrowError(/Delay values below 0 are not allowed for this animation step/);
const steps2 = [animate('1s -0.5s', style({opacity: 1}))];
expect(() => {
validateAndThrowAnimationSequence(steps2);
}).toThrowError(/Delay values below 0 are not allowed for this animation step/);
});
it('should throw if keyframes() is not used inside of animate()', () => {
const steps = [keyframes([])];
expect(() => {
validateAndThrowAnimationSequence(steps);
}).toThrowError(/keyframes\(\) must be placed inside of a call to animate\(\)/);
const steps2 = [group([keyframes([])])];
expect(() => {
validateAndThrowAnimationSequence(steps2);
}).toThrowError(/keyframes\(\) must be placed inside of a call to animate\(\)/);
});
});
describe('keyframe building', () => {
@ -102,7 +168,7 @@ export function main() {
animate(1000, style({width: 200}))
];
const players = invokeAnimationSequence(steps);
const players = invokeAnimationSequence(rootElement, steps);
expect(players[0].keyframes).toEqual([
{height: AUTO_STYLE, width: 0, offset: 0},
{height: 50, width: 0, offset: .25},
@ -116,7 +182,7 @@ export function main() {
() => {
const steps = [animate(1000, style({width: 999}))];
const players = invokeAnimationSequence(steps);
const players = invokeAnimationSequence(rootElement, steps);
expect(players[0].keyframes).toEqual([
{width: AUTO_STYLE, offset: 0}, {width: 999, offset: 1}
]);
@ -128,7 +194,7 @@ export function main() {
animate(1000, style({width: 100, height: 400, opacity: 1}))
];
const players = invokeAnimationSequence(steps);
const players = invokeAnimationSequence(rootElement, steps);
expect(players[0].keyframes).toEqual([
{width: 200, height: 0, opacity: 0, offset: 0},
{width: 100, height: 400, opacity: 1, offset: 1}
@ -142,7 +208,7 @@ export function main() {
animate(1000, style({opacity: 1}))
];
const players = invokeAnimationSequence(steps);
const players = invokeAnimationSequence(rootElement, steps);
const keyframes = humanizeOffsets(players[0].keyframes, 4);
expect(keyframes).toEqual([
@ -159,7 +225,7 @@ export function main() {
animate('1s cubic-bezier(.29, .55 ,.53 ,1.53)', style({opacity: 1}))
];
const player = invokeAnimationSequence(steps)[0];
const player = invokeAnimationSequence(rootElement, steps)[0];
const firstKeyframe = player.keyframes[0];
const firstKeyframeEasing = firstKeyframe['easing'] as string;
expect(firstKeyframeEasing.replace(/\s+/g, '')).toEqual('cubic-bezier(.29,.55,.53,1.53)');
@ -170,34 +236,30 @@ export function main() {
it('should not produce extra timelines when multiple sequences are used within each other',
() => {
const steps = [
style({width: 0}), animate(1000, style({width: 100})), sequence([
style({width: 0}),
animate(1000, style({width: 100})),
sequence([
animate(1000, style({width: 200})),
sequence([animate(1000, style({width: 300}))])
sequence([
animate(1000, style({width: 300})),
]),
]),
animate(1000, style({width: 400})),
sequence([
animate(1000, style({width: 500})),
]),
animate(1000, style({width: 400})), sequence([animate(1000, style({width: 500}))])
];
const players = invokeAnimationSequence(steps);
expect(players[0].keyframes).toEqual([
const players = invokeAnimationSequence(rootElement, steps);
expect(players.length).toEqual(1);
const player = players[0];
expect(player.keyframes).toEqual([
{width: 0, offset: 0}, {width: 100, offset: .2}, {width: 200, offset: .4},
{width: 300, offset: .6}, {width: 400, offset: .8}, {width: 500, offset: 1}
]);
});
it('should produce a 1ms animation step if a style call exists before sequence within a call to animate()',
() => {
const steps = [
style({width: 100}), sequence([
animate(1000, style({width: 200})),
])
];
const players = invokeAnimationSequence(steps);
expect(humanizeOffsets(players[0].keyframes, 4)).toEqual([
{width: 100, offset: 0}, {width: 100, offset: .001}, {width: 200, offset: 1}
]);
});
it('should create a new timeline after a sequence if group() or keyframe() commands are used within',
() => {
const steps = [
@ -211,7 +273,7 @@ export function main() {
animate(1000, style({width: 500, height: 500}))
];
const players = invokeAnimationSequence(steps);
const players = invokeAnimationSequence(rootElement, steps);
expect(players.length).toEqual(4);
const finalPlayer = players[players.length - 1];
@ -219,6 +281,76 @@ export function main() {
{width: 200, height: 200, offset: 0}, {width: 500, height: 500, offset: 1}
]);
});
it('should push the start of a sequence if a delay option is provided', () => {
const steps = [
style({width: '0px'}), animate(1000, style({width: '100px'})),
sequence(
[
animate(1000, style({width: '200px'})),
],
{delay: 500})
];
const players = invokeAnimationSequence(rootElement, steps);
const finalPlayer = players[players.length - 1];
expect(finalPlayer.keyframes).toEqual([
{width: '100px', offset: 0},
{width: '200px', offset: 1},
]);
expect(finalPlayer.delay).toEqual(1500);
});
});
describe('subtitutions', () => {
it('should substitute in timing values', () => {
function makeAnimation(exp: string, values: {[key: string]: any}) {
const steps = [style({opacity: 0}), animate(exp, style({opacity: 1}))];
return invokeAnimationSequence(rootElement, steps, values);
}
let players = makeAnimation('{{ duration }}', buildParams({duration: '1234ms'}));
expect(players[0].duration).toEqual(1234);
players = makeAnimation('{{ duration }}', buildParams({duration: '9s 2s'}));
expect(players[0].duration).toEqual(11000);
players = makeAnimation('{{ duration }} 1s', buildParams({duration: '1.5s'}));
expect(players[0].duration).toEqual(2500);
players = makeAnimation(
'{{ duration }} {{ delay }}', buildParams({duration: '1s', delay: '2s'}));
expect(players[0].duration).toEqual(3000);
});
it('should allow multiple substitutions to occur within the same style value', () => {
const steps = [
style({transform: ''}),
animate(1000, style({transform: 'translateX({{ x }}) translateY({{ y }})'}))
];
const players =
invokeAnimationSequence(rootElement, steps, buildParams({x: '200px', y: '400px'}));
expect(players[0].keyframes).toEqual([
{offset: 0, transform: ''},
{offset: 1, transform: 'translateX(200px) translateY(400px)'}
]);
});
it('should throw an error when an input variable is not provided when invoked and is not a default value',
() => {
expect(() => {invokeAnimationSequence(rootElement, [style({color: '{{ color }}'})])})
.toThrowError(/Please provide a value for the animation param color/);
expect(
() => {invokeAnimationSequence(
rootElement,
[
style({color: '{{ start }}'}),
animate('{{ time }}', style({color: '{{ end }}'})),
],
buildParams({start: 'blue', end: 'red'}))})
.toThrowError(/Please provide a value for the animation param time/);
});
});
describe('keyframes()', () => {
@ -230,7 +362,7 @@ export function main() {
animate(1000, style({height: 0, opacity: 0}))
];
const players = invokeAnimationSequence(steps);
const players = invokeAnimationSequence(rootElement, steps);
expect(players.length).toEqual(3);
const player0 = players[0];
@ -267,7 +399,7 @@ export function main() {
animate(1000, style({color: 'green', opacity: 0}))
];
const players = invokeAnimationSequence(steps);
const players = invokeAnimationSequence(rootElement, steps);
const finalPlayer = players[players.length - 1];
expect(finalPlayer.keyframes).toEqual([
{opacity: 1, color: 'blue', offset: 0}, {opacity: 0, color: 'green', offset: 1}
@ -283,7 +415,7 @@ export function main() {
]))
];
const players = invokeAnimationSequence(steps);
const players = invokeAnimationSequence(rootElement, steps);
expect(players.length).toEqual(2);
const topPlayer = players[0];
@ -305,7 +437,7 @@ export function main() {
keyframes([style({opacity: .8, offset: .5}), style({opacity: 1, offset: 1})]))
];
const player = invokeAnimationSequence(steps)[1];
const player = invokeAnimationSequence(rootElement, steps)[1];
expect(player.easing).toEqual('ease-out');
});
@ -318,7 +450,7 @@ export function main() {
keyframes([style({opacity: .8, offset: .5}), style({opacity: 1, offset: 1})]))
];
const player = invokeAnimationSequence(steps)[1];
const player = invokeAnimationSequence(rootElement, steps)[1];
expect(player.delay).toEqual(2500);
});
@ -343,7 +475,7 @@ export function main() {
group([animate('2s', style({height: '500px', width: '500px'}))])
];
const players = invokeAnimationSequence(steps);
const players = invokeAnimationSequence(rootElement, steps);
expect(players.length).toEqual(5);
const firstPlayerKeyframes = players[0].keyframes;
@ -381,7 +513,7 @@ export function main() {
style({opacity: 1, offset: 1})
]));
const players = invokeAnimationSequence(steps);
const players = invokeAnimationSequence(rootElement, steps);
expect(players.length).toEqual(1);
const player = players[0];
@ -398,7 +530,7 @@ export function main() {
{type: AnimationMetadataType.Style, offset: 1, styles: {opacity: 1}},
]));
const players = invokeAnimationSequence(steps);
const players = invokeAnimationSequence(rootElement, steps);
expect(players.length).toEqual(1);
const player = players[0];
@ -417,7 +549,7 @@ export function main() {
animate(1000, style({width: 1000, height: 1000}))
];
const players = invokeAnimationSequence(steps);
const players = invokeAnimationSequence(rootElement, steps);
expect(players.length).toEqual(4);
const player0 = players[0];
@ -460,7 +592,7 @@ export function main() {
])
])];
const players = invokeAnimationSequence(steps);
const players = invokeAnimationSequence(rootElement, steps);
expect(players.length).toEqual(2);
const gPlayer1 = players[0];
@ -484,7 +616,7 @@ export function main() {
animate('1s 1s', style({height: 200, width: 200}))
];
const players = invokeAnimationSequence(steps);
const players = invokeAnimationSequence(rootElement, steps);
expect(players.length).toEqual(4);
const finalPlayer = players[players.length - 1];
@ -505,7 +637,7 @@ export function main() {
animate(2000, style({width: 0, opacity: 0}))
];
const players = invokeAnimationSequence(steps);
const players = invokeAnimationSequence(rootElement, steps);
const middlePlayer = players[2];
expect(middlePlayer.delay).toEqual(2000);
expect(middlePlayer.duration).toEqual(2000);
@ -514,6 +646,97 @@ export function main() {
expect(finalPlayer.delay).toEqual(6000);
expect(finalPlayer.duration).toEqual(2000);
});
it('should push the start of a group if a delay option is provided', () => {
const steps = [
style({width: '0px', height: '0px'}),
animate(1500, style({width: '100px', height: '100px'})),
group(
[
animate(1000, style({width: '200px'})),
animate(2000, style({height: '200px'})),
],
{delay: 300})
];
const players = invokeAnimationSequence(rootElement, steps);
const finalWidthPlayer = players[players.length - 2];
const finalHeightPlayer = players[players.length - 1];
expect(finalWidthPlayer.delay).toEqual(1800);
expect(finalWidthPlayer.keyframes).toEqual([
{width: '100px', offset: 0},
{width: '200px', offset: 1},
]);
expect(finalHeightPlayer.delay).toEqual(1800);
expect(finalHeightPlayer.keyframes).toEqual([
{height: '100px', offset: 0},
{height: '200px', offset: 1},
]);
});
});
describe('query()', () => {
it('should delay the query operation if a delay option is provided', () => {
const steps = [
style({opacity: 0}), animate(1000, style({opacity: 1})),
query(
'div',
[
style({width: 0}),
animate(500, style({width: 200})),
],
{delay: 200})
];
const players = invokeAnimationSequence(rootElement, steps);
const finalPlayer = players[players.length - 1];
expect(finalPlayer.delay).toEqual(1200);
});
it('should throw an error when an animation query returns zero elements', () => {
const steps =
[query('somethingFake', [style({opacity: 0}), animate(1000, style({opacity: 1}))])];
expect(() => { invokeAnimationSequence(rootElement, steps); })
.toThrowError(
/`query\("somethingFake"\)` returned zero elements\. \(Use `query\("somethingFake", \{ optional: true \}\)` if you wish to allow this\.\)/);
});
it('should allow a query to be skipped if it is set as optional and returns zero elements',
() => {
const steps = [query(
'somethingFake', [style({opacity: 0}), animate(1000, style({opacity: 1}))],
{optional: true})];
expect(() => { invokeAnimationSequence(rootElement, steps); }).not.toThrow();
const steps2 = [query(
'fakeSomethings', [style({opacity: 0}), animate(1000, style({opacity: 1}))],
{optional: true})];
expect(() => { invokeAnimationSequence(rootElement, steps2); }).not.toThrow();
});
it('should delay the query operation if a delay option is provided', () => {
const steps = [
style({opacity: 0}), animate(1300, style({opacity: 1})),
query(
'div',
[
style({width: 0}),
animate(500, style({width: 200})),
],
{delay: 300})
];
const players = invokeAnimationSequence(rootElement, steps);
const fp1 = players[players.length - 2];
const fp2 = players[players.length - 1];
expect(fp1.delay).toEqual(1600);
expect(fp2.delay).toEqual(1600);
});
});
describe('timing values', () => {
@ -522,7 +745,7 @@ export function main() {
const steps: AnimationMetadata[] =
[style({opacity: 0}), animate('3s 1s ease-out', style({opacity: 1}))];
const player = invokeAnimationSequence(steps)[0];
const player = invokeAnimationSequence(rootElement, steps)[0];
expect(player.keyframes).toEqual([
{opacity: 0, offset: 0}, {opacity: 0, offset: .25, easing: 'ease-out'},
{opacity: 1, offset: 1}
@ -535,7 +758,7 @@ export function main() {
animate('2s ease-out', style({width: 20})), animate('1s ease-in', style({width: 30}))
];
const players = invokeAnimationSequence(steps);
const players = invokeAnimationSequence(rootElement, steps);
expect(players.length).toEqual(1);
const player = players[0];
@ -560,7 +783,7 @@ export function main() {
])
];
const player = invokeAnimationSequence(steps)[0];
const player = invokeAnimationSequence(rootElement, steps)[0];
expect(player.duration).toEqual(1000);
expect(player.delay).toEqual(0);
});
@ -579,7 +802,7 @@ export function main() {
]))
];
const players = invokeAnimationSequence(steps);
const players = invokeAnimationSequence(rootElement, steps);
expect(players[0].delay).toEqual(0); // top-level animation
expect(players[1].delay).toEqual(1500); // first entry in group()
expect(players[2].delay).toEqual(1500); // second entry in group()
@ -595,7 +818,7 @@ export function main() {
const toStyles: ɵStyleData[] = [{background: 'red'}];
const player = invokeAnimationSequence(steps, fromStyles, toStyles)[0];
const player = invokeAnimationSequence(rootElement, steps, {}, fromStyles, toStyles)[0];
expect(player.duration).toEqual(0);
expect(player.keyframes).toEqual([]);
});
@ -608,7 +831,7 @@ export function main() {
const toStyles: ɵStyleData[] = [{background: 'red'}];
const players = invokeAnimationSequence(steps, fromStyles, toStyles);
const players = invokeAnimationSequence(rootElement, steps, {}, fromStyles, toStyles);
expect(players[0].keyframes).toEqual([
{background: 'blue', height: 100, offset: 0},
{background: 'red', height: AUTO_STYLE, offset: 1}
@ -623,7 +846,7 @@ export function main() {
const toStyles: ɵStyleData[] = [{background: 'red'}];
const players = invokeAnimationSequence(steps, fromStyles, toStyles);
const players = invokeAnimationSequence(rootElement, steps, {}, fromStyles, toStyles);
expect(players[0].keyframes).toEqual([
{background: 'blue', offset: 0, easing: 'ease-out'},
{background: 'red', offset: 1}
@ -642,16 +865,21 @@ function humanizeOffsets(keyframes: ɵStyleData[], digits: number = 3): ɵStyleD
}
function invokeAnimationSequence(
steps: AnimationMetadata | AnimationMetadata[], startingStyles: ɵStyleData[] = [],
destinationStyles: ɵStyleData[] = []): AnimationTimelineInstruction[] {
return new Animation(steps).buildTimelines(startingStyles, destinationStyles);
element: any, steps: AnimationMetadata | AnimationMetadata[], locals: {[key: string]: any} = {},
startingStyles: ɵStyleData[] = [], destinationStyles: ɵStyleData[] = [],
subInstructions?: ElementInstructionMap): AnimationTimelineInstruction[] {
return new Animation(steps).buildTimelines(
element, startingStyles, destinationStyles, locals, subInstructions);
}
function validateAndThrowAnimationSequence(steps: AnimationMetadata | AnimationMetadata[]) {
const ast =
Array.isArray(steps) ? sequence(<AnimationMetadata[]>steps) : <AnimationMetadata>steps;
const errors = validateAnimationSequence(ast);
const errors: any[] = [];
const ast = buildAnimationAst(steps, errors);
if (errors.length) {
throw new Error(errors.join('\n'));
}
}
function buildParams(params: {[name: string]: any}): AnimationOptions {
return <AnimationOptions>{params};
}

View File

@ -6,17 +6,25 @@
* found in the LICENSE file at https://angular.io/license
*/
import {animate, state, style, transition, trigger} from '@angular/animations';
import {buildTrigger} from '../../src/dsl/animation_trigger';
import {AnimationOptions, animate, state, style, transition} from '@angular/animations';
import {AnimationTransitionInstruction} from '@angular/animations/browser/src/dsl/animation_transition_instruction';
import {AnimationTrigger} from '@angular/animations/browser/src/dsl/animation_trigger';
function makeTrigger(name: string, steps: any) {
const triggerData = trigger(name, steps);
const triggerInstance = buildTrigger(triggerData.name, triggerData.definitions);
return triggerInstance;
}
import {makeTrigger} from '../shared';
export function main() {
describe('AnimationTrigger', () => {
// these tests are only mean't to be run within the DOM (for now)
if (typeof Element == 'undefined') return;
let element: any;
beforeEach(() => {
element = document.createElement('div');
document.body.appendChild(element);
});
afterEach(() => { document.body.removeChild(element); });
describe('trigger validation', () => {
it('should group errors together for an animation trigger', () => {
expect(() => {
@ -64,7 +72,7 @@ export function main() {
const result = makeTrigger(
'name', [transition('a => b', animate(1234)), transition('b => c', animate(5678))]);
const trans = result.matchTransition('b', 'c') !;
const trans = buildTransition(result, element, 'b', 'c') !;
expect(trans.timelines.length).toEqual(1);
const timeline = trans.timelines[0];
expect(timeline.duration).toEqual(5678);
@ -76,99 +84,148 @@ export function main() {
transition('* => *', animate(9999))
]);
let trans = result.matchTransition('b', 'c') !;
let trans = buildTransition(result, element, 'b', 'c') !;
expect(trans.timelines[0].duration).toEqual(5678);
trans = result.matchTransition('a', 'b') !;
trans = buildTransition(result, element, 'a', 'b') !;
expect(trans.timelines[0].duration).toEqual(1234);
trans = result.matchTransition('c', 'c') !;
trans = buildTransition(result, element, 'c', 'c') !;
expect(trans.timelines[0].duration).toEqual(9999);
});
it('should null when no results are found', () => {
const result = makeTrigger('name', [transition('a => b', animate(1111))]);
const trans = result.matchTransition('b', 'a');
expect(trans).toBeFalsy();
const trigger = result.matchTransition('b', 'a');
expect(trigger).toBeFalsy();
});
it('should allow a function to be used as a predicate for the transition', () => {
let returnValue = false;
const result = makeTrigger('name', [transition((from, to) => returnValue, animate(1111))]);
expect(result.matchTransition('a', 'b')).toBeFalsy();
expect(result.matchTransition('1', 2)).toBeFalsy();
expect(result.matchTransition(false, true)).toBeFalsy();
returnValue = true;
expect(result.matchTransition('a', 'b')).toBeTruthy();
});
it('should call each transition predicate function until the first one that returns true',
() => {
let count = 0;
function countAndReturn(value: boolean) {
return (fromState: any, toState: any) => {
count++;
return value;
};
}
const result = makeTrigger('name', [
transition(countAndReturn(false), animate(1111)),
transition(countAndReturn(false), animate(2222)),
transition(countAndReturn(true), animate(3333)),
transition(countAndReturn(true), animate(3333))
]);
const trans = result.matchTransition('a', 'b') !;
expect(trans.timelines[0].duration).toEqual(3333);
expect(count).toEqual(3);
});
it('should support bi-directional transition expressions', () => {
const result = makeTrigger('name', [transition('a <=> b', animate(2222))]);
const t1 = result.matchTransition('a', 'b') !;
const t1 = buildTransition(result, element, 'a', 'b') !;
expect(t1.timelines[0].duration).toEqual(2222);
const t2 = result.matchTransition('b', 'a') !;
const t2 = buildTransition(result, element, 'b', 'a') !;
expect(t2.timelines[0].duration).toEqual(2222);
});
it('should support multiple transition statements in one string', () => {
const result = makeTrigger('name', [transition('a => b, b => a, c => *', animate(1234))]);
const t1 = result.matchTransition('a', 'b') !;
const t1 = buildTransition(result, element, 'a', 'b') !;
expect(t1.timelines[0].duration).toEqual(1234);
const t2 = result.matchTransition('b', 'a') !;
const t2 = buildTransition(result, element, 'b', 'a') !;
expect(t2.timelines[0].duration).toEqual(1234);
const t3 = result.matchTransition('c', 'a') !;
const t3 = buildTransition(result, element, 'c', 'a') !;
expect(t3.timelines[0].duration).toEqual(1234);
});
describe('params', () => {
it('should support transition-level animation variable params', () => {
const result = makeTrigger(
'name',
[transition(
'a => b', [style({height: '{{ a }}'}), animate(1000, style({height: '{{ b }}'}))],
buildParams({a: '100px', b: '200px'}))]);
const trans = buildTransition(result, element, 'a', 'b') !;
const keyframes = trans.timelines[0].keyframes;
expect(keyframes).toEqual([{height: '100px', offset: 0}, {height: '200px', offset: 1}]);
});
it('should subtitute variable params provided directly within the transition match', () => {
const result = makeTrigger(
'name',
[transition(
'a => b', [style({height: '{{ a }}'}), animate(1000, style({height: '{{ b }}'}))],
buildParams({a: '100px', b: '200px'}))]);
const trans = buildTransition(result, element, 'a', 'b', buildParams({a: '300px'})) !;
const keyframes = trans.timelines[0].keyframes;
expect(keyframes).toEqual([{height: '300px', offset: 0}, {height: '200px', offset: 1}]);
});
});
it('should match `true` and `false` given boolean values', () => {
const result = makeTrigger('name', [
state('false', style({color: 'red'})), state('true', style({color: 'green'})),
transition('true <=> false', animate(1234))
]);
const trans = buildTransition(result, element, false, true) !;
expect(trans.timelines[0].duration).toEqual(1234);
});
it('should match `1` and `0` given boolean values', () => {
const result = makeTrigger('name', [
state('0', style({color: 'red'})), state('1', style({color: 'green'})),
transition('1 <=> 0', animate(4567))
]);
const trans = buildTransition(result, element, false, true) !;
expect(trans.timelines[0].duration).toEqual(4567);
});
it('should match `true` and `false` state styles on a `1 <=> 0` boolean transition given boolean values',
() => {
const result = makeTrigger('name', [
state('false', style({color: 'red'})), state('true', style({color: 'green'})),
transition('1 <=> 0', animate(4567))
]);
const trans = buildTransition(result, element, false, true) !;
expect(trans.timelines[0].keyframes).toEqual([
{offset: 0, color: 'red'}, {offset: 1, color: 'green'}
])
});
it('should match `1` and `0` state styles on a `true <=> false` boolean transition given boolean values',
() => {
const result = makeTrigger('name', [
state('0', style({color: 'orange'})), state('1', style({color: 'blue'})),
transition('true <=> false', animate(4567))
]);
const trans = buildTransition(result, element, false, true) !;
expect(trans.timelines[0].keyframes).toEqual([
{offset: 0, color: 'orange'}, {offset: 1, color: 'blue'}
])
});
describe('aliases', () => {
it('should alias the :enter transition as void => *', () => {
const result = makeTrigger('name', [transition(':enter', animate(3333))]);
const trans = result.matchTransition('void', 'something') !;
const trans = buildTransition(result, element, 'void', 'something') !;
expect(trans.timelines[0].duration).toEqual(3333);
});
it('should alias the :leave transition as * => void', () => {
const result = makeTrigger('name', [transition(':leave', animate(3333))]);
const trans = result.matchTransition('something', 'void') !;
const trans = buildTransition(result, element, 'something', 'void') !;
expect(trans.timelines[0].duration).toEqual(3333);
});
});
});
});
}
function buildTransition(
trigger: AnimationTrigger, element: any, fromState: any, toState: any,
params?: AnimationOptions): AnimationTransitionInstruction|null {
const trans = trigger.matchTransition(fromState, toState) !;
if (trans) {
return trans.build(element, fromState, toState, params) !;
}
return null;
}
function buildParams(params: {[name: string]: any}): AnimationOptions {
return <AnimationOptions>{params};
}

View File

@ -0,0 +1,121 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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 {AnimationMetadata, animate, style} from '@angular/animations';
import {AnimationStyleNormalizer, NoopAnimationStyleNormalizer} from '../../src/dsl/style_normalization/animation_style_normalizer';
import {AnimationDriver} from '../../src/render/animation_driver';
import {TimelineAnimationEngine} from '../../src/render/timeline_animation_engine';
import {MockAnimationDriver, MockAnimationPlayer} from '../../testing/src/mock_animation_driver';
export function main() {
const defaultDriver = new MockAnimationDriver();
function makeEngine(driver?: AnimationDriver, normalizer?: AnimationStyleNormalizer) {
return new TimelineAnimationEngine(
driver || defaultDriver, normalizer || new NoopAnimationStyleNormalizer());
}
// these tests are only mean't to be run within the DOM
if (typeof Element == 'undefined') return;
describe('TimelineAnimationEngine', () => {
let element: any;
beforeEach(() => {
MockAnimationDriver.log = [];
element = document.createElement('div');
document.body.appendChild(element);
});
afterEach(() => document.body.removeChild(element));
it('should animate a timeline', () => {
const engine = makeEngine();
const steps = [style({height: 100}), animate(1000, style({height: 0}))];
expect(MockAnimationDriver.log.length).toEqual(0);
invokeAnimation(engine, element, steps);
expect(MockAnimationDriver.log.length).toEqual(1);
});
it('should not destroy timeline-based animations after they have finished', () => {
const engine = makeEngine();
const log: string[] = [];
function capture(value: string) {
return () => { log.push(value); };
}
const steps = [style({height: 0}), animate(1000, style({height: 500}))];
const player = invokeAnimation(engine, element, steps);
player.onDone(capture('done'));
player.onDestroy(capture('destroy'));
expect(log).toEqual([]);
player.finish();
expect(log).toEqual(['done']);
player.destroy();
expect(log).toEqual(['done', 'destroy']);
});
it('should normalize the style values that are animateTransitioned within an a timeline animation',
() => {
const engine = makeEngine(defaultDriver, new SuffixNormalizer('-normalized'));
const steps = [
style({width: '333px'}),
animate(1000, style({width: '999px'})),
];
const player = invokeAnimation(engine, element, steps) as MockAnimationPlayer;
expect(player.keyframes).toEqual([
{'width-normalized': '333px-normalized', offset: 0},
{'width-normalized': '999px-normalized', offset: 1}
]);
});
it('should normalize `*` values', () => {
const driver = new SuperMockDriver();
const engine = makeEngine(driver);
const steps = [
style({width: '*'}),
animate(1000, style({width: '999px'})),
];
const player = invokeAnimation(engine, element, steps) as MockAnimationPlayer;
expect(player.keyframes).toEqual([{width: '*star*', offset: 0}, {width: '999px', offset: 1}]);
});
});
}
function invokeAnimation(
engine: TimelineAnimationEngine, element: any, steps: AnimationMetadata | AnimationMetadata[],
id: string = 'id') {
engine.register(id, steps);
return engine.create(id, element);
}
class SuffixNormalizer extends AnimationStyleNormalizer {
constructor(private _suffix: string) { super(); }
normalizePropertyName(propertyName: string, errors: string[]): string {
return propertyName + this._suffix;
}
normalizeStyleValue(
userProvidedProperty: string, normalizedProperty: string, value: string|number,
errors: string[]): string {
return value + this._suffix;
}
}
class SuperMockDriver extends MockAnimationDriver {
computeStyle(element: any, prop: string, defaultValue?: string): string { return '*star*'; }
}

View File

@ -5,20 +5,16 @@
* 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 {AnimationEvent, NoopAnimationPlayer, animate, keyframes, state, style, transition, trigger} from '@angular/animations';
import {el} from '@angular/platform-browser/testing/src/browser_util';
import {AnimationEvent, AnimationMetadata, AnimationTriggerMetadata, NoopAnimationPlayer, animate, state, style, transition, trigger} from '@angular/animations';
import {buildAnimationKeyframes} from '../../src/dsl/animation_timeline_visitor';
import {TriggerAst} from '../../src/dsl/animation_ast';
import {buildAnimationAst} from '../../src/dsl/animation_ast_builder';
import {buildTrigger} from '../../src/dsl/animation_trigger';
import {AnimationStyleNormalizer, NoopAnimationStyleNormalizer} from '../../src/dsl/style_normalization/animation_style_normalizer';
import {DomAnimationEngine} from '../../src/render/dom_animation_engine';
import {TransitionAnimationEngine} from '../../src/render/transition_animation_engine';
import {MockAnimationDriver, MockAnimationPlayer} from '../../testing/src/mock_animation_driver';
function makeTrigger(name: string, steps: any) {
const triggerData = trigger(name, steps);
const triggerInstance = buildTrigger(triggerData.name, triggerData.definitions);
return triggerInstance;
}
const DEFAULT_NAMESPACE_ID = 'id';
export function main() {
const driver = new MockAnimationDriver();
@ -26,23 +22,30 @@ export function main() {
// these tests are only mean't to be run within the DOM
if (typeof Element == 'undefined') return;
describe('DomAnimationEngine', () => {
describe('TransitionAnimationEngine', () => {
let element: any;
beforeEach(() => {
MockAnimationDriver.log = [];
element = el('<div></div>');
element = document.createElement('div');
document.body.appendChild(element);
});
afterEach(() => { document.body.removeChild(element); });
function makeEngine(normalizer?: AnimationStyleNormalizer) {
return new DomAnimationEngine(driver, normalizer || new NoopAnimationStyleNormalizer());
const engine =
new TransitionAnimationEngine(driver, normalizer || new NoopAnimationStyleNormalizer());
engine.createNamespace(DEFAULT_NAMESPACE_ID, element);
return engine;
}
describe('trigger registration', () => {
it('should ignore and not throw an error if the same trigger is registered twice', () => {
// TODO (matsko): ask why this is avoided
const engine = makeEngine();
engine.registerTrigger(trigger('trig', []));
expect(() => { engine.registerTrigger(trigger('trig', [])); }).not.toThrow();
registerTrigger(element, engine, trigger('trig', []));
expect(() => { registerTrigger(element, engine, trigger('trig', [])); }).not.toThrow();
});
});
@ -54,11 +57,10 @@ export function main() {
transition('* => *', [style({height: '0px'}), animate(1000, style({height: '100px'}))])
]);
engine.registerTrigger(trig);
expect(engine.queuedPlayers.length).toEqual(0);
engine.setProperty(element, 'myTrigger', 'value');
expect(engine.queuedPlayers.length).toEqual(1);
registerTrigger(element, engine, trig);
setProperty(element, engine, 'myTrigger', 'value');
engine.flush();
expect(engine.players.length).toEqual(1);
const player = MockAnimationDriver.log.pop() as MockAnimationPlayer;
expect(player.keyframes).toEqual([
@ -66,43 +68,6 @@ export function main() {
]);
});
it('should always invoke an animation even if the property change is not matched', () => {
const engine = makeEngine();
const trig = trigger(
'myTrigger',
[transition(
'yes => no', [style({height: '0px'}), animate(1000, style({height: '100px'}))])]);
engine.registerTrigger(trig);
expect(engine.queuedPlayers.length).toEqual(0);
engine.setProperty(element, 'myTrigger', 'no');
expect(engine.queuedPlayers.length).toEqual(1);
expect(engine.queuedPlayers.pop() instanceof NoopAnimationPlayer).toBe(true);
engine.flush();
engine.setProperty(element, 'myTrigger', 'yes');
expect(engine.queuedPlayers.length).toEqual(1);
expect(engine.queuedPlayers.pop() instanceof NoopAnimationPlayer).toBe(true);
});
it('should not initialize the animation until the engine has been flushed', () => {
const engine = makeEngine();
engine.registerTrigger(trigger(
'trig', [transition('* => something', [animate(1000, style({color: 'gold'}))])]));
engine.setProperty(element, 'trig', 'something');
const player = engine.queuedPlayers.pop() as MockAnimationPlayer;
let initialized = false;
player.onInit(() => initialized = true);
expect(initialized).toBe(false);
engine.flush();
expect(initialized).toBe(true);
});
it('should not queue an animation if the property value has not changed at all', () => {
const engine = makeEngine();
@ -110,25 +75,71 @@ export function main() {
transition('* => *', [style({height: '0px'}), animate(1000, style({height: '100px'}))])
]);
engine.registerTrigger(trig);
expect(engine.queuedPlayers.length).toEqual(0);
registerTrigger(element, engine, trig);
engine.flush();
expect(engine.players.length).toEqual(0);
engine.setProperty(element, 'myTrigger', 'abc');
expect(engine.queuedPlayers.length).toEqual(1);
setProperty(element, engine, 'myTrigger', 'abc');
engine.flush();
expect(engine.players.length).toEqual(1);
engine.setProperty(element, 'myTrigger', 'abc');
expect(engine.queuedPlayers.length).toEqual(1);
setProperty(element, engine, 'myTrigger', 'abc');
engine.flush();
expect(engine.players.length).toEqual(1);
});
it('should throw an error if an animation property without a matching trigger is changed',
() => {
const engine = makeEngine();
expect(() => {
engine.setProperty(element, 'myTrigger', 'no');
setProperty(element, engine, 'myTrigger', 'no');
}).toThrowError(/The provided animation trigger "myTrigger" has not been registered!/);
});
});
describe('removal operations', () => {
it('should cleanup all inner state that\'s tied to an element once removed', () => {
const engine = makeEngine();
const trig = trigger('myTrigger', [
transition(':leave', [style({height: '0px'}), animate(1000, style({height: '100px'}))])
]);
registerTrigger(element, engine, trig);
setProperty(element, engine, 'myTrigger', 'value');
engine.flush();
expect(engine.elementContainsData(DEFAULT_NAMESPACE_ID, element)).toBeTruthy();
engine.removeNode(DEFAULT_NAMESPACE_ID, element, true);
engine.flush();
expect(engine.elementContainsData(DEFAULT_NAMESPACE_ID, element)).toBeTruthy();
});
it('should create and recreate a namespace for a host element with the same component source',
() => {
const engine = makeEngine();
const trig =
trigger('myTrigger', [transition('* => *', animate(1234, style({color: 'red'})))]);
registerTrigger(element, engine, trig);
setProperty(element, engine, 'myTrigger', 'value');
engine.flush();
expect((engine.players[0].getRealPlayer() as MockAnimationPlayer).duration)
.toEqual(1234);
engine.destroy(DEFAULT_NAMESPACE_ID, null);
registerTrigger(element, engine, trig);
setProperty(element, engine, 'myTrigger', 'value');
engine.flush();
expect((engine.players[0].getRealPlayer() as MockAnimationPlayer).duration)
.toEqual(1234);
});
});
describe('event listeners', () => {
it('should listen to the onStart operation for the animation', () => {
const engine = makeEngine();
@ -138,9 +149,9 @@ export function main() {
]);
let count = 0;
engine.registerTrigger(trig);
engine.listen(element, 'myTrigger', 'start', () => count++);
engine.setProperty(element, 'myTrigger', 'value');
registerTrigger(element, engine, trig);
listen(element, engine, 'myTrigger', 'start', () => count++);
setProperty(element, engine, 'myTrigger', 'value');
expect(count).toEqual(0);
engine.flush();
@ -155,33 +166,31 @@ export function main() {
]);
let count = 0;
engine.registerTrigger(trig);
engine.listen(element, 'myTrigger', 'done', () => count++);
engine.setProperty(element, 'myTrigger', 'value');
registerTrigger(element, engine, trig);
listen(element, engine, 'myTrigger', 'done', () => count++);
setProperty(element, engine, 'myTrigger', 'value');
expect(count).toEqual(0);
engine.flush();
expect(count).toEqual(0);
const player = engine.activePlayers.pop() !;
player.finish();
engine.players[0].finish();
expect(count).toEqual(1);
});
it('should throw an error when an event is listened to that isn\'t supported', () => {
const engine = makeEngine();
const trig = trigger('myTrigger', []);
engine.registerTrigger(trig);
registerTrigger(element, engine, trig);
expect(() => { engine.listen(element, 'myTrigger', 'explode', () => {}); })
expect(() => { listen(element, engine, 'myTrigger', 'explode', () => {}); })
.toThrowError(
/The provided animation trigger event "explode" for the animation trigger "myTrigger" is not supported!/);
});
it('should throw an error when an event is listened for a trigger that doesn\'t exist', () => {
const engine = makeEngine();
expect(() => { engine.listen(element, 'myTrigger', 'explode', () => {}); })
expect(() => { listen(element, engine, 'myTrigger', 'explode', () => {}); })
.toThrowError(
/Unable to listen on the animation trigger event "explode" because the animation trigger "myTrigger" doesn\'t exist!/);
});
@ -189,8 +198,8 @@ export function main() {
it('should throw an error when an undefined event is listened for', () => {
const engine = makeEngine();
const trig = trigger('myTrigger', []);
engine.registerTrigger(trig);
expect(() => { engine.listen(element, 'myTrigger', '', () => {}); })
registerTrigger(element, engine, trig);
expect(() => { listen(element, engine, 'myTrigger', '', () => {}); })
.toThrowError(
/Unable to listen on the animation trigger "myTrigger" because the provided event is undefined!/);
});
@ -203,16 +212,16 @@ export function main() {
[transition(
'* => *', [style({height: '0px'}), animate(1000, style({height: '100px'}))])]);
engine.registerTrigger(trig);
registerTrigger(element, engine, trig);
let count = 0;
engine.listen(element, 'myTrigger', 'start', () => count++);
listen(element, engine, 'myTrigger', 'start', () => count++);
engine.setProperty(element, 'myTrigger', '123');
setProperty(element, engine, 'myTrigger', '123');
engine.flush();
expect(count).toEqual(1);
engine.setProperty(element, 'myTrigger', '456');
setProperty(element, engine, 'myTrigger', '456');
engine.flush();
expect(count).toEqual(2);
});
@ -224,61 +233,64 @@ export function main() {
'myTrigger1',
[transition(
'* => 123', [style({height: '0px'}), animate(1000, style({height: '100px'}))])]);
engine.registerTrigger(trig1);
registerTrigger(element, engine, trig1);
const trig2 = trigger(
'myTrigger2',
[transition(
'* => 123', [style({width: '0px'}), animate(1000, style({width: '100px'}))])]);
engine.registerTrigger(trig2);
registerTrigger(element, engine, trig2);
let count = 0;
engine.listen(element, 'myTrigger1', 'start', () => count++);
listen(element, engine, 'myTrigger1', 'start', () => count++);
engine.setProperty(element, 'myTrigger1', '123');
setProperty(element, engine, 'myTrigger1', '123');
engine.flush();
expect(count).toEqual(1);
engine.setProperty(element, 'myTrigger2', '123');
setProperty(element, engine, 'myTrigger2', '123');
engine.flush();
expect(count).toEqual(1);
});
it('should allow a listener to be deregistered', () => {
it('should allow a listener to be deregistered, but only after a flush occurs', () => {
const engine = makeEngine();
const trig = trigger(
'myTrigger',
[transition(
'* => 123', [style({height: '0px'}), animate(1000, style({height: '100px'}))])]);
engine.registerTrigger(trig);
registerTrigger(element, engine, trig);
let count = 0;
const deregisterFn = engine.listen(element, 'myTrigger', 'start', () => count++);
engine.setProperty(element, 'myTrigger', '123');
const deregisterFn = listen(element, engine, 'myTrigger', 'start', () => count++);
setProperty(element, engine, 'myTrigger', '123');
engine.flush();
expect(count).toEqual(1);
deregisterFn();
engine.setProperty(element, 'myTrigger', '456');
engine.flush();
setProperty(element, engine, 'myTrigger', '456');
engine.flush();
expect(count).toEqual(1);
});
it('should trigger a listener callback with an AnimationEvent argument', () => {
const engine = makeEngine();
engine.registerTrigger(trigger(
'myTrigger',
[transition(
'* => *', [style({height: '0px'}), animate(1234, style({height: '100px'}))])]));
registerTrigger(
element, engine, trigger('myTrigger', [
transition(
'* => *', [style({height: '0px'}), animate(1234, style({height: '100px'}))])
]));
// we do this so that the next transition has a starting value that isnt null
engine.setProperty(element, 'myTrigger', '123');
setProperty(element, engine, 'myTrigger', '123');
engine.flush();
let capture: AnimationEvent = null !;
engine.listen(element, 'myTrigger', 'start', (e) => capture = e);
engine.listen(element, 'myTrigger', 'done', (e) => capture = e);
engine.setProperty(element, 'myTrigger', '456');
listen(element, engine, 'myTrigger', 'start', e => capture = e);
listen(element, engine, 'myTrigger', 'done', e => capture = e);
setProperty(element, engine, 'myTrigger', '456');
engine.flush();
expect(capture).toEqual({
@ -291,7 +303,7 @@ export function main() {
});
capture = null !;
const player = engine.activePlayers.pop() !;
const player = engine.players.pop() !;
player.finish();
expect(capture).toEqual({
@ -305,102 +317,46 @@ export function main() {
});
});
describe('instructions', () => {
it('should animate a transition instruction', () => {
const engine = makeEngine();
const trig = makeTrigger('something', [
state('on', style({height: 100})), state('off', style({height: 0})),
transition('on => off', animate(9876))
]);
const instruction = trig.matchTransition('on', 'off') !;
expect(MockAnimationDriver.log.length).toEqual(0);
engine.animateTransition(element, instruction);
expect(MockAnimationDriver.log.length).toEqual(1);
});
it('should animate a timeline instruction', () => {
const engine = makeEngine();
const timelines =
buildAnimationKeyframes([style({height: 100}), animate(1000, style({height: 0}))]);
expect(MockAnimationDriver.log.length).toEqual(0);
engine.animateTimeline(element, timelines);
expect(MockAnimationDriver.log.length).toEqual(1);
});
it('should animate an array of animation instructions', () => {
const engine = makeEngine();
const instructions = buildAnimationKeyframes([
style({height: 100}), animate(1000, style({height: 0})),
animate(1000, keyframes([style({width: 0}), style({width: 1000})]))
]);
expect(MockAnimationDriver.log.length).toEqual(0);
engine.animateTimeline(element, instructions);
expect(MockAnimationDriver.log.length).toBeGreaterThan(0);
});
});
describe('removals / insertions', () => {
it('should allow text nodes to be removed through the engine', () => {
const engine = makeEngine();
const node = document.createTextNode('hello');
element.appendChild(node);
let called = false;
engine.onRemove(node, () => called = true);
expect(called).toBeTruthy();
});
it('should allow text nodes to be inserted through the engine', () => {
const engine = makeEngine();
const node = document.createTextNode('hello');
let called = false;
engine.onInsert(node, () => called = true);
expect(called).toBeTruthy();
});
});
describe('transition operations', () => {
it('should persist the styles on the element as actual styles once the animation is complete',
() => {
const engine = makeEngine();
const trig = makeTrigger('something', [
const trig = trigger('something', [
state('on', style({height: '100px'})), state('off', style({height: '0px'})),
transition('on => off', animate(9876))
]);
const instruction = trig.matchTransition('on', 'off') !;
const player = engine.animateTransition(element, instruction);
registerTrigger(element, engine, trig);
setProperty(element, engine, trig.name, 'on');
setProperty(element, engine, trig.name, 'off');
engine.flush();
expect(element.style.height).not.toEqual('0px');
player.finish();
engine.players[0].finish();
expect(element.style.height).toEqual('0px');
});
it('should remove all existing state styling from an element when a follow-up transition occurs on the same trigger',
() => {
const engine = makeEngine();
const trig = makeTrigger('something', [
const trig = trigger('something', [
state('a', style({height: '100px'})), state('b', style({height: '500px'})),
state('c', style({width: '200px'})), transition('* => *', animate(9876))
]);
const instruction1 = trig.matchTransition('a', 'b') !;
const player1 = engine.animateTransition(element, instruction1);
registerTrigger(element, engine, trig);
setProperty(element, engine, trig.name, 'a');
setProperty(element, engine, trig.name, 'b');
engine.flush();
const player1 = engine.players[0];
player1.finish();
expect(element.style.height).toEqual('500px');
const instruction2 = trig.matchTransition('b', 'c') !;
const player2 = engine.animateTransition(element, instruction2);
setProperty(element, engine, trig.name, 'c');
engine.flush();
const player2 = engine.players[0];
expect(element.style.height).not.toEqual('500px');
player2.finish();
expect(element.style.width).toEqual('200px');
@ -410,26 +366,33 @@ export function main() {
it('should allow two animation transitions with different triggers to animate in parallel',
() => {
const engine = makeEngine();
const trig1 = makeTrigger('something1', [
const trig1 = trigger('something1', [
state('a', style({width: '100px'})), state('b', style({width: '200px'})),
transition('* => *', animate(1000))
]);
const trig2 = makeTrigger('something2', [
const trig2 = trigger('something2', [
state('x', style({height: '500px'})), state('y', style({height: '1000px'})),
transition('* => *', animate(2000))
]);
registerTrigger(element, engine, trig1);
registerTrigger(element, engine, trig2);
let doneCount = 0;
function doneCallback() { doneCount++; }
const instruction1 = trig1.matchTransition('a', 'b') !;
const instruction2 = trig2.matchTransition('x', 'y') !;
const player1 = engine.animateTransition(element, instruction1);
setProperty(element, engine, trig1.name, 'a');
setProperty(element, engine, trig1.name, 'b');
setProperty(element, engine, trig2.name, 'x');
setProperty(element, engine, trig2.name, 'y');
engine.flush();
const player1 = engine.players[0] !;
player1.onDone(doneCallback);
expect(doneCount).toEqual(0);
const player2 = engine.animateTransition(element, instruction2);
const player2 = engine.players[1] !;
player2.onDone(doneCallback);
expect(doneCount).toEqual(0);
@ -446,18 +409,23 @@ export function main() {
it('should cancel a previously running animation when a follow-up transition kicks off on the same trigger',
() => {
const engine = makeEngine();
const trig = makeTrigger('something', [
const trig = trigger('something', [
state('x', style({opacity: 0})), state('y', style({opacity: .5})),
state('z', style({opacity: 1})), transition('* => *', animate(1000))
]);
const instruction1 = trig.matchTransition('x', 'y') !;
const instruction2 = trig.matchTransition('y', 'z') !;
registerTrigger(element, engine, trig);
setProperty(element, engine, trig.name, 'x');
setProperty(element, engine, trig.name, 'y');
engine.flush();
expect(parseFloat(element.style.opacity)).not.toEqual(.5);
const player1 = engine.animateTransition(element, instruction1);
const player2 = engine.animateTransition(element, instruction2);
const player1 = engine.players[0];
setProperty(element, engine, trig.name, 'z');
engine.flush();
const player2 = engine.players[0];
expect(parseFloat(element.style.opacity)).toEqual(.5);
@ -471,65 +439,73 @@ export function main() {
it('should pass in the previously running players into the follow-up transition player when cancelled',
() => {
const engine = makeEngine();
const trig = makeTrigger('something', [
const trig = trigger('something', [
state('x', style({opacity: 0})), state('y', style({opacity: .5})),
state('z', style({opacity: 1})), transition('* => *', animate(1000))
]);
const instruction1 = trig.matchTransition('x', 'y') !;
const instruction2 = trig.matchTransition('y', 'z') !;
const instruction3 = trig.matchTransition('z', 'x') !;
const player1 = engine.animateTransition(element, instruction1);
registerTrigger(element, engine, trig);
setProperty(element, engine, trig.name, 'x');
setProperty(element, engine, trig.name, 'y');
engine.flush();
const player1 = MockAnimationDriver.log.pop() !as MockAnimationPlayer;
player1.setPosition(0.5);
const player2 = <MockAnimationPlayer>engine.animateTransition(element, instruction2);
setProperty(element, engine, trig.name, 'z');
engine.flush();
const player2 = MockAnimationDriver.log.pop() !as MockAnimationPlayer;
expect(player2.previousPlayers).toEqual([player1]);
player2.finish();
const player3 = <MockAnimationPlayer>engine.animateTransition(element, instruction3);
setProperty(element, engine, trig.name, 'x');
engine.flush();
const player3 = MockAnimationDriver.log.pop() !as MockAnimationPlayer;
expect(player3.previousPlayers).toEqual([]);
});
it('should cancel all existing players if a removal animation is set to occur', () => {
const engine = makeEngine();
const trig = makeTrigger('something', [
const trig = trigger('something', [
state('m', style({opacity: 0})), state('n', style({opacity: 1})),
transition('* => *', animate(1000))
]);
registerTrigger(element, engine, trig);
setProperty(element, engine, trig.name, 'm');
setProperty(element, engine, trig.name, 'n');
engine.flush();
let doneCount = 0;
function doneCallback() { doneCount++; }
const instruction1 = trig.matchTransition('m', 'n') !;
const instructions2 =
buildAnimationKeyframes([style({height: 0}), animate(1000, style({height: 100}))]) !;
const instruction3 = trig.matchTransition('n', 'void') !;
const player1 = engine.animateTransition(element, instruction1);
const player1 = engine.players[0];
player1.onDone(doneCallback);
const player2 = engine.animateTimeline(element, instructions2);
player2.onDone(doneCallback);
engine.flush();
expect(doneCount).toEqual(0);
const player3 = engine.animateTransition(element, instruction3);
expect(doneCount).toEqual(2);
setProperty(element, engine, trig.name, 'void');
engine.flush();
expect(doneCount).toEqual(1);
});
it('should only persist styles that exist in the final state styles and not the last keyframe',
() => {
const engine = makeEngine();
const trig = makeTrigger('something', [
const trig = trigger('something', [
state('0', style({width: '0px'})), state('1', style({width: '100px'})),
transition('* => *', [animate(1000, style({height: '200px'}))])
]);
const instruction = trig.matchTransition('0', '1') !;
const player = engine.animateTransition(element, instruction);
registerTrigger(element, engine, trig);
setProperty(element, engine, trig.name, '0');
setProperty(element, engine, trig.name, '1');
engine.flush();
const player = engine.players[0] !;
expect(element.style.width).not.toEqual('100px');
player.finish();
@ -540,104 +516,74 @@ export function main() {
it('should default to using styling from the `*` state if a matching state is not found',
() => {
const engine = makeEngine();
const trig = makeTrigger('something', [
const trig = trigger('something', [
state('a', style({opacity: 0})), state('*', style({opacity: .5})),
transition('* => *', animate(1000))
]);
const instruction = trig.matchTransition('a', 'z') !;
engine.animateTransition(element, instruction).finish();
registerTrigger(element, engine, trig);
setProperty(element, engine, trig.name, 'a');
setProperty(element, engine, trig.name, 'z');
engine.flush();
engine.players[0].finish();
expect(parseFloat(element.style.opacity)).toEqual(.5);
});
it('should treat `void` as `void`', () => {
const engine = makeEngine();
const trig = makeTrigger('something', [
const trig = trigger('something', [
state('a', style({opacity: 0})), state('void', style({opacity: .8})),
transition('* => *', animate(1000))
]);
const instruction = trig.matchTransition('a', 'void') !;
engine.animateTransition(element, instruction).finish();
registerTrigger(element, engine, trig);
setProperty(element, engine, trig.name, 'a');
setProperty(element, engine, trig.name, 'void');
engine.flush();
engine.players[0].finish();
expect(parseFloat(element.style.opacity)).toEqual(.8);
});
});
describe('timeline operations', () => {
it('should not destroy timeline-based animations after they have finished', () => {
const engine = makeEngine();
const log: string[] = [];
function capture(value: string) {
return () => { log.push(value); };
}
const instructions =
buildAnimationKeyframes([style({height: 0}), animate(1000, style({height: 500}))]);
const player = engine.animateTimeline(element, instructions);
player.onDone(capture('done'));
player.onDestroy(capture('destroy'));
expect(log).toEqual([]);
player.finish();
expect(log).toEqual(['done']);
player.destroy();
expect(log).toEqual(['done', 'destroy']);
});
});
describe('style normalizer', () => {
it('should normalize the style values that are animateTransitioned within an a transition animation',
() => {
const engine = makeEngine(new SuffixNormalizer('-normalized'));
const trig = makeTrigger('something', [
const trig = trigger('something', [
state('on', style({height: 100})), state('off', style({height: 0})),
transition('on => off', animate(9876))
]);
const instruction = trig.matchTransition('on', 'off') !;
const player = <MockAnimationPlayer>engine.animateTransition(element, instruction);
registerTrigger(element, engine, trig);
setProperty(element, engine, trig.name, 'on');
setProperty(element, engine, trig.name, 'off');
engine.flush();
const player = MockAnimationDriver.log.pop() as MockAnimationPlayer;
expect(player.keyframes).toEqual([
{'height-normalized': '100-normalized', offset: 0},
{'height-normalized': '0-normalized', offset: 1}
]);
});
it('should normalize the style values that are animateTransitioned within an a timeline animation',
() => {
const engine = makeEngine(new SuffixNormalizer('-normalized'));
const instructions = buildAnimationKeyframes([
style({width: '333px'}),
animate(1000, style({width: '999px'})),
]);
const player = <MockAnimationPlayer>engine.animateTimeline(element, instructions);
expect(player.keyframes).toEqual([
{'width-normalized': '333px-normalized', offset: 0},
{'width-normalized': '999px-normalized', offset: 1}
]);
});
it('should throw an error when normalization fails within a transition animation', () => {
const engine = makeEngine(new ExactCssValueNormalizer({left: '100px'}));
const trig = makeTrigger('something', [
const trig = trigger('something', [
state('a', style({left: '0px', width: '200px'})),
state('b', style({left: '100px', width: '100px'})), transition('a => b', animate(9876))
]);
const instruction = trig.matchTransition('a', 'b') !;
registerTrigger(element, engine, trig);
setProperty(element, engine, trig.name, 'a');
setProperty(element, engine, trig.name, 'b');
let errorMessage = '';
try {
engine.animateTransition(element, instruction);
engine.flush();
} catch (e) {
errorMessage = e.toString();
}
@ -652,15 +598,18 @@ export function main() {
it('should perform insert operations immediately ', () => {
const engine = makeEngine();
let container = <any>el('<div></div>');
let child1 = <any>el('<div></div>');
let child2 = <any>el('<div></div>');
const child1 = document.createElement('div');
const child2 = document.createElement('div');
element.appendChild(child1);
element.appendChild(child2);
engine.onInsert(container, () => container.appendChild(child1));
engine.onInsert(container, () => container.appendChild(child2));
element.appendChild(child1);
engine.insertNode(DEFAULT_NAMESPACE_ID, child1, element, true);
element.appendChild(child2);
engine.insertNode(DEFAULT_NAMESPACE_ID, child2, element, true);
expect(container.contains(child1)).toBe(true);
expect(container.contains(child2)).toBe(true);
expect(element.contains(child1)).toBe(true);
expect(element.contains(child2)).toBe(true);
});
});
});
@ -700,3 +649,27 @@ class ExactCssValueNormalizer extends AnimationStyleNormalizer {
return expectedValue;
}
}
function registerTrigger(
element: any, engine: TransitionAnimationEngine, metadata: AnimationTriggerMetadata,
id: string = DEFAULT_NAMESPACE_ID) {
const errors: any[] = [];
const name = metadata.name;
const ast = buildAnimationAst(metadata as AnimationMetadata, errors) as TriggerAst;
if (errors.length) {
}
const trigger = buildTrigger(name, ast);
engine.register(id, element, name, trigger)
}
function setProperty(
element: any, engine: TransitionAnimationEngine, property: string, value: any,
id: string = DEFAULT_NAMESPACE_ID) {
engine.trigger(id, element, property, value);
}
function listen(
element: any, engine: TransitionAnimationEngine, eventName: string, phaseName: string,
callback: (event: any) => any, id: string = DEFAULT_NAMESPACE_ID) {
return engine.listen(id, element, eventName, phaseName, callback);
}

View File

@ -1,78 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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 {DOMAnimation} from '../../src/render/web_animations/dom_animation';
import {WebAnimationsPlayer} from '../../src/render/web_animations/web_animations_player';
export function main() {
describe('WebAnimationsPlayer', function() {
// these tests are only mean't to be run within the DOM
if (typeof Element == 'undefined') return;
let element: any;
beforeEach(() => {
element = document.createElement('div');
document.body.appendChild(element);
});
afterEach(() => { document.body.removeChild(element); });
it('should properly balance any previous player styles into the animation keyframes', () => {
element.style.height = '666px';
element.style.width = '333px';
const prevPlayer1 = new MockWebAnimationsPlayer(
element, [{width: '0px', offset: 0}, {width: '200px', offset: 1}], {});
prevPlayer1.play();
prevPlayer1.finish();
const prevPlayer2 = new MockWebAnimationsPlayer(
element, [{height: '0px', offset: 0}, {height: '200px', offset: 1}], {});
prevPlayer2.play();
prevPlayer2.finish();
// what needs to happen here is the player below should
// examine which styles are present in the provided previous
// players and use them as input data for the keyframes of
// the new player. Given that the players are in their finished
// state, the styles are copied over as the starting keyframe
// for the animation and if the styles are missing in later keyframes
// then the styling is resolved by computing the styles
const player = new MockWebAnimationsPlayer(
element, [{width: '100px', offset: 0}, {width: '500px', offset: 1}], {},
[prevPlayer1, prevPlayer2]);
player.init();
expect(player.capturedKeyframes).toEqual([
{height: '200px', width: '200px', offset: 0},
{height: '666px', width: '500px', offset: 1}
]);
});
});
}
class MockWebAnimationsPlayer extends WebAnimationsPlayer {
capturedKeyframes: any[];
_triggerWebAnimation(element: any, keyframes: any[], options: any): any {
this.capturedKeyframes = keyframes;
return new MockDOMAnimation();
}
}
class MockDOMAnimation implements DOMAnimation {
onfinish = (callback: (e: any) => any) => {};
position = 0;
currentTime = 0;
cancel(): void {}
play(): void {}
pause(): void {}
finish(): void {}
addEventListener(eventName: string, handler: (event: any) => any): any { return null; }
dispatchEvent(eventName: string): any { return null; }
}

View File

@ -0,0 +1,26 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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 {trigger} from '@angular/animations';
import {TriggerAst} from '../src/dsl/animation_ast';
import {buildAnimationAst} from '../src/dsl/animation_ast_builder';
import {AnimationTrigger, buildTrigger} from '../src/dsl/animation_trigger';
export function makeTrigger(
name: string, steps: any, skipErrors: boolean = false): AnimationTrigger {
const errors: any[] = [];
const triggerData = trigger(name, steps);
const triggerAst = buildAnimationAst(triggerData, errors) as TriggerAst;
if (!skipErrors && errors.length) {
const LINE_START = '\n - ';
throw new Error(
`Animation parsing for the ${name} trigger have failed:${LINE_START}${errors.join(LINE_START)}`);
}
return buildTrigger(name, triggerAst);
}

View File

@ -15,6 +15,10 @@ import {AnimationDriver} from '../../src/render/animation_driver';
export class MockAnimationDriver implements AnimationDriver {
static log: AnimationPlayer[] = [];
computeStyle(element: any, prop: string, defaultValue?: string): string {
return defaultValue || '';
}
animate(
element: any, keyframes: {[key: string]: string | number}[], duration: number, delay: number,
easing: string, previousPlayers: any[] = []): MockAnimationPlayer {
@ -30,8 +34,10 @@ export class MockAnimationDriver implements AnimationDriver {
*/
export class MockAnimationPlayer extends NoopAnimationPlayer {
private __finished = false;
private __started = false;
public previousStyles: {[key: string]: string | number} = {};
private _onInitFns: (() => any)[] = [];
public currentSnapshot: ɵStyleData = {};
constructor(
public element: any, public keyframes: {[key: string]: string | number}[],
@ -40,10 +46,12 @@ export class MockAnimationPlayer extends NoopAnimationPlayer {
super();
previousPlayers.forEach(player => {
if (player instanceof MockAnimationPlayer) {
const styles = player._captureStyles();
Object.keys(styles).forEach(prop => { this.previousStyles[prop] = styles[prop]; });
const styles = player.currentSnapshot;
Object.keys(styles).forEach(prop => this.previousStyles[prop] = styles[prop]);
}
});
this.totalTime = delay + duration;
}
/* @internal */
@ -66,7 +74,17 @@ export class MockAnimationPlayer extends NoopAnimationPlayer {
this.__finished = true;
}
private _captureStyles(): {[styleName: string]: string | number} {
/* @internal */
triggerMicrotask() {}
play(): void {
super.play();
this.__started = true;
}
hasStarted() { return this.__started; }
beforeDestroy() {
const captures: ɵStyleData = {};
Object.keys(this.previousStyles).forEach(prop => {
@ -86,6 +104,6 @@ export class MockAnimationPlayer extends NoopAnimationPlayer {
});
}
return captures;
this.currentSnapshot = captures;
}
}

View File

@ -0,0 +1,23 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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 {AnimationMetadata, AnimationOptions} from './animation_metadata';
import {AnimationPlayer} from './players/animation_player';
/**
* @experimental Animation support is experimental.
*/
export abstract class AnimationBuilder {
abstract build(animation: AnimationMetadata|AnimationMetadata[]): Animation;
}
/**
* @experimental Animation support is experimental.
*/
export abstract class Animation {
abstract create(element: any, options?: AnimationOptions): AnimationPlayer;
}

View File

@ -16,17 +16,32 @@ export declare type AnimateTimings = {
easing: string | null
};
/**
* @experimental Animation support is experimental.
*/
export declare interface AnimationOptions {
delay?: number|string;
duration?: number|string;
params?: {[name: string]: any};
}
/**
* @experimental Animation support is experimental.
*/
export const enum AnimationMetadataType {
State,
Transition,
Sequence,
Group,
Animate,
KeyframeSequence,
Style
State = 0,
Transition = 1,
Sequence = 2,
Group = 3,
Animate = 4,
Keyframes = 5,
Style = 6,
Trigger = 7,
Reference = 8,
AnimateChild = 9,
AnimateRef = 10,
Query = 11,
Stagger = 12
}
/**
@ -42,9 +57,10 @@ export interface AnimationMetadata { type: AnimationMetadataType; }
/**
* @experimental Animation support is experimental.
*/
export interface AnimationTriggerMetadata {
export interface AnimationTriggerMetadata extends AnimationMetadata {
name: string;
definitions: AnimationMetadata[];
options: {params?: {[name: string]: any}}|null;
}
/**
@ -65,8 +81,26 @@ export interface AnimationStateMetadata extends AnimationMetadata {
* @experimental Animation support is experimental.
*/
export interface AnimationTransitionMetadata extends AnimationMetadata {
expr: string|((fromState: string, toState: string) => boolean);
expr: string;
animation: AnimationMetadata|AnimationMetadata[];
options: AnimationOptions|null;
}
/**
* @experimental Animation support is experimental.
*/
export interface AnimationReferenceMetadata extends AnimationMetadata {
animation: AnimationMetadata|AnimationMetadata[];
options: AnimationOptions|null;
}
/**
* @experimental Animation support is experimental.
*/
export interface AnimationQueryMetadata extends AnimationMetadata {
selector: string;
animation: AnimationMetadata|AnimationMetadata[];
options: AnimationQueryOptions|null;
}
/**
@ -86,8 +120,8 @@ export interface AnimationKeyframesSequenceMetadata extends AnimationMetadata {
* @experimental Animation support is experimental.
*/
export interface AnimationStyleMetadata extends AnimationMetadata {
styles: {[key: string]: string | number}|{[key: string]: string | number}[];
offset?: number;
styles: '*'|{[key: string]: string | number}|Array<{[key: string]: string | number}|'*'>;
offset: number|null;
}
/**
@ -101,13 +135,31 @@ export interface AnimationAnimateMetadata extends AnimationMetadata {
styles: AnimationStyleMetadata|AnimationKeyframesSequenceMetadata|null;
}
/**
* @experimental Animation support is experimental.
*/
export interface AnimationAnimateChildMetadata extends AnimationMetadata {
options: AnimationOptions|null;
}
/**
* @experimental Animation support is experimental.
*/
export interface AnimationAnimateRefMetadata extends AnimationMetadata {
animation: AnimationReferenceMetadata;
options: AnimationOptions|null;
}
/**
* Metadata representing the entry of animations. Instances of this class are provided via the
* animation DSL when the {@link sequence sequence animation function} is called.
*
* @experimental Animation support is experimental.
*/
export interface AnimationSequenceMetadata extends AnimationMetadata { steps: AnimationMetadata[]; }
export interface AnimationSequenceMetadata extends AnimationMetadata {
steps: AnimationMetadata[];
options: AnimationOptions|null;
}
/**
* Metadata representing the entry of animations. Instances of this class are provided via the
@ -115,7 +167,18 @@ export interface AnimationSequenceMetadata extends AnimationMetadata { steps: An
*
* @experimental Animation support is experimental.
*/
export interface AnimationGroupMetadata extends AnimationMetadata { steps: AnimationMetadata[]; }
export interface AnimationGroupMetadata extends AnimationMetadata {
steps: AnimationMetadata[];
options: AnimationOptions|null;
}
/**
* @experimental Animation support is experimental.
*/
export interface AnimationStaggerMetadata extends AnimationMetadata {
timings: string|number;
animation: AnimationMetadata|AnimationMetadata[];
}
/**
* `trigger` is an animation-specific function that is designed to be used inside of Angular's
@ -169,7 +232,7 @@ export interface AnimationGroupMetadata extends AnimationMetadata { steps: Anima
* @experimental Animation support is experimental.
*/
export function trigger(name: string, definitions: AnimationMetadata[]): AnimationTriggerMetadata {
return {name, definitions};
return {type: AnimationMetadataType.Trigger, name, definitions, options: {}};
}
/**
@ -220,7 +283,7 @@ export function trigger(name: string, definitions: AnimationMetadata[]): Animati
export function animate(
timings: string | number, styles: AnimationStyleMetadata | AnimationKeyframesSequenceMetadata |
null = null): AnimationAnimateMetadata {
return {type: AnimationMetadataType.Animate, styles: styles, timings: timings};
return {type: AnimationMetadataType.Animate, styles, timings};
}
/**
@ -254,8 +317,9 @@ export function animate(
*
* @experimental Animation support is experimental.
*/
export function group(steps: AnimationMetadata[]): AnimationGroupMetadata {
return {type: AnimationMetadataType.Group, steps: steps};
export function group(
steps: AnimationMetadata[], options: AnimationOptions | null = null): AnimationGroupMetadata {
return {type: AnimationMetadataType.Group, steps, options};
}
/**
@ -292,8 +356,9 @@ export function group(steps: AnimationMetadata[]): AnimationGroupMetadata {
*
* @experimental Animation support is experimental.
*/
export function sequence(steps: AnimationMetadata[]): AnimationSequenceMetadata {
return {type: AnimationMetadataType.Sequence, steps: steps};
export function sequence(steps: AnimationMetadata[], options: AnimationOptions | null = null):
AnimationSequenceMetadata {
return {type: AnimationMetadataType.Sequence, steps, options};
}
/**
@ -339,9 +404,9 @@ export function sequence(steps: AnimationMetadata[]): AnimationSequenceMetadata
* @experimental Animation support is experimental.
*/
export function style(
tokens: {[key: string]: string | number} |
Array<{[key: string]: string | number}>): AnimationStyleMetadata {
return {type: AnimationMetadataType.Style, styles: tokens};
tokens: '*' | {[key: string]: string | number} |
Array<'*'|{[key: string]: string | number}>): AnimationStyleMetadata {
return {type: AnimationMetadataType.Style, styles: tokens, offset: null};
}
/**
@ -393,7 +458,7 @@ export function style(
* @experimental Animation support is experimental.
*/
export function state(name: string, styles: AnimationStyleMetadata): AnimationStateMetadata {
return {type: AnimationMetadataType.State, name: name, styles: styles};
return {type: AnimationMetadataType.State, name, styles};
}
/**
@ -442,7 +507,7 @@ export function state(name: string, styles: AnimationStyleMetadata): AnimationSt
* @experimental Animation support is experimental.
*/
export function keyframes(steps: AnimationStyleMetadata[]): AnimationKeyframesSequenceMetadata {
return {type: AnimationMetadataType.KeyframeSequence, steps: steps};
return {type: AnimationMetadataType.Keyframes, steps};
}
/**
@ -553,7 +618,59 @@ export function keyframes(steps: AnimationStyleMetadata[]): AnimationKeyframesSe
* @experimental Animation support is experimental.
*/
export function transition(
stateChangeExpr: string | ((fromState: string, toState: string) => boolean),
steps: AnimationMetadata | AnimationMetadata[]): AnimationTransitionMetadata {
return {type: AnimationMetadataType.Transition, expr: stateChangeExpr, animation: steps};
stateChangeExpr: string, steps: AnimationMetadata | AnimationMetadata[],
options: AnimationOptions | null = null): AnimationTransitionMetadata {
return {type: AnimationMetadataType.Transition, expr: stateChangeExpr, animation: steps, options};
}
/**
* @experimental Animation support is experimental.
*/
export function animation(
steps: AnimationMetadata | AnimationMetadata[],
options: AnimationOptions | null = null): AnimationReferenceMetadata {
return {type: AnimationMetadataType.Reference, animation: steps, options};
}
/**
* @experimental Animation support is experimental.
*/
export function animateChild(options: AnimationOptions | null = null):
AnimationAnimateChildMetadata {
return {type: AnimationMetadataType.AnimateChild, options};
}
/**
* @experimental Animation support is experimental.
*/
export function useAnimation(
animation: AnimationReferenceMetadata,
options: AnimationOptions | null = null): AnimationAnimateRefMetadata {
return {type: AnimationMetadataType.AnimateRef, animation, options};
}
/**
* @experimental Animation support is experimental.
*/
export declare interface AnimationQueryOptions extends AnimationOptions {
optional?: boolean;
limit?: number;
}
/**
* @experimental Animation support is experimental.
*/
export function query(
selector: string, animation: AnimationMetadata | AnimationMetadata[],
options: AnimationQueryOptions | null = null): AnimationQueryMetadata {
return {type: AnimationMetadataType.Query, selector, animation, options};
}
/**
* @experimental Animation support is experimental.
*/
export function stagger(
timings: string | number,
animation: AnimationMetadata | AnimationMetadata[]): AnimationStaggerMetadata {
return {type: AnimationMetadataType.Stagger, timings, animation};
}

View File

@ -11,8 +11,9 @@
* @description
* Entry point for all animation APIs of the animation package.
*/
export {Animation, AnimationBuilder} from './animation_builder';
export {AnimationEvent} from './animation_event';
export {AUTO_STYLE, AnimateTimings, AnimationAnimateMetadata, AnimationGroupMetadata, AnimationKeyframesSequenceMetadata, AnimationMetadata, AnimationMetadataType, AnimationSequenceMetadata, AnimationStateMetadata, AnimationStyleMetadata, AnimationTransitionMetadata, AnimationTriggerMetadata, animate, group, keyframes, sequence, state, style, transition, trigger, ɵStyleData} from './animation_metadata';
export {AUTO_STYLE, AnimateTimings, AnimationAnimateChildMetadata, AnimationAnimateMetadata, AnimationAnimateRefMetadata, AnimationGroupMetadata, AnimationKeyframesSequenceMetadata, AnimationMetadata, AnimationMetadataType, AnimationOptions, AnimationQueryMetadata, AnimationQueryOptions, AnimationReferenceMetadata, AnimationSequenceMetadata, AnimationStaggerMetadata, AnimationStateMetadata, AnimationStyleMetadata, AnimationTransitionMetadata, AnimationTriggerMetadata, animate, animateChild, animation, group, keyframes, query, sequence, stagger, state, style, transition, trigger, useAnimation, ɵStyleData} from './animation_metadata';
export {AnimationPlayer, NoopAnimationPlayer} from './players/animation_player';
export * from './private_export';

View File

@ -18,22 +18,38 @@ export class AnimationGroupPlayer implements AnimationPlayer {
private _onDestroyFns: Function[] = [];
public parentPlayer: AnimationPlayer|null = null;
public totalTime: number = 0;
constructor(private _players: AnimationPlayer[]) {
let count = 0;
let doneCount = 0;
let destroyCount = 0;
let startCount = 0;
const total = this._players.length;
if (total == 0) {
scheduleMicroTask(() => this._onFinish());
} else {
this._players.forEach(player => {
player.parentPlayer = this;
player.onDone(() => {
if (++count >= total) {
if (++doneCount >= total) {
this._onFinish();
}
});
player.onDestroy(() => {
if (++destroyCount >= total) {
this._onDestroy();
}
});
player.onStart(() => {
if (++startCount >= total) {
this._onStart();
}
});
});
}
this.totalTime = this._players.reduce((time, player) => Math.max(time, player.totalTime), 0);
}
private _onFinish() {
@ -48,6 +64,14 @@ export class AnimationGroupPlayer implements AnimationPlayer {
onStart(fn: () => void): void { this._onStartFns.push(fn); }
private _onStart() {
if (!this.hasStarted()) {
this._onStartFns.forEach(fn => fn());
this._onStartFns = [];
this._started = true;
}
}
onDone(fn: () => void): void { this._onDoneFns.push(fn); }
onDestroy(fn: () => void): void { this._onDestroyFns.push(fn); }
@ -58,11 +82,7 @@ export class AnimationGroupPlayer implements AnimationPlayer {
if (!this.parentPlayer) {
this.init();
}
if (!this.hasStarted()) {
this._onStartFns.forEach(fn => fn());
this._onStartFns = [];
this._started = true;
}
this._onStart();
this._players.forEach(player => player.play());
}
@ -75,11 +95,13 @@ export class AnimationGroupPlayer implements AnimationPlayer {
this._players.forEach(player => player.finish());
}
destroy(): void {
destroy(): void { this._onDestroy(); }
private _onDestroy() {
if (!this._destroyed) {
this._destroyed = true;
this._onFinish();
this._players.forEach(player => player.destroy());
this._destroyed = true;
this._onDestroyFns.forEach(fn => fn());
this._onDestroyFns = [];
}
@ -93,7 +115,11 @@ export class AnimationGroupPlayer implements AnimationPlayer {
}
setPosition(p: number): void {
this._players.forEach(player => { player.setPosition(p); });
const timeAtPosition = p * this.totalTime;
this._players.forEach(player => {
const position = player.totalTime ? Math.min(1, timeAtPosition / player.totalTime) : 1;
player.setPosition(position);
});
}
getPosition(): number {

View File

@ -26,6 +26,8 @@ export abstract class AnimationPlayer {
abstract getPosition(): number;
get parentPlayer(): AnimationPlayer|null { throw new Error('NOT IMPLEMENTED: Base Class'); }
set parentPlayer(player: AnimationPlayer|null) { throw new Error('NOT IMPLEMENTED: Base Class'); }
get totalTime(): number { throw new Error('NOT IMPLEMENTED: Base Class'); }
beforeDestroy?: () => any;
}
/**
@ -39,6 +41,7 @@ export class NoopAnimationPlayer implements AnimationPlayer {
private _destroyed = false;
private _finished = false;
public parentPlayer: AnimationPlayer|null = null;
public totalTime = 0;
constructor() {}
private _onFinish() {
if (!this._finished) {
@ -54,15 +57,20 @@ export class NoopAnimationPlayer implements AnimationPlayer {
init(): void {}
play(): void {
if (!this.hasStarted()) {
scheduleMicroTask(() => this._onFinish());
this.triggerMicrotask();
this._onStart();
}
this._started = true;
}
/* @internal */
triggerMicrotask() { scheduleMicroTask(() => this._onFinish()); }
private _onStart() {
this._onStartFns.forEach(fn => fn());
this._onStartFns = [];
}
pause(): void {}
restart(): void {}
finish(): void { this._onFinish(); }

View File

@ -6,3 +6,4 @@
* found in the LICENSE file at https://angular.io/license
*/
export {AnimationGroupPlayer as ɵAnimationGroupPlayer} from './players/animation_group_player';
export const ɵPRE_STYLE = '!';

View File

@ -53,8 +53,8 @@ export interface AnimationKeyframesSequenceMetadata extends AnimationMetadata {
* @deprecated This symbol has moved. Please Import from @angular/animations instead!
*/
export interface AnimationStyleMetadata extends AnimationMetadata {
styles: {[key: string]: string | number}|{[key: string]: string | number}[];
offset?: number;
styles: '*'|{[key: string]: string | number}|Array<{[key: string]: string | number}|'*'>;
offset: number|null;
}
/**
@ -131,9 +131,8 @@ export function keyframes(steps: AnimationStyleMetadata[]): AnimationKeyframesSe
/**
* @deprecated This symbol has moved. Please Import from @angular/animations instead!
*/
export function transition(
stateChangeExpr: string | ((fromState: string, toState: string) => boolean),
steps: AnimationMetadata | AnimationMetadata[]): AnimationTransitionMetadata {
export function transition(stateChangeExpr: string, steps: AnimationMetadata | AnimationMetadata[]):
AnimationTransitionMetadata {
return _transition(stateChangeExpr, steps);
}

View File

@ -5,7 +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 {AUTO_STYLE, AnimationEvent, animate, group, keyframes, state, style, transition, trigger} from '@angular/animations';
import {AUTO_STYLE, AnimationEvent, AnimationOptions, animate, animateChild, group, keyframes, query, state, style, transition, trigger} from '@angular/animations';
import {AnimationDriver, ɵAnimationEngine, ɵNoopAnimationDriver} from '@angular/animations/browser';
import {MockAnimationDriver, MockAnimationPlayer} from '@angular/animations/browser/testing';
import {Component, HostBinding, HostListener, RendererFactory2, ViewChild} from '@angular/core';
@ -15,6 +15,9 @@ import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {TestBed, fakeAsync, flushMicrotasks} from '../../testing';
const DEFAULT_NAMESPACE_ID = 'id';
const DEFAULT_COMPONENT_ID = '1';
export function main() {
// these tests are only mean't to be run within the DOM (for now)
if (typeof Element == 'undefined') return;
@ -65,6 +68,85 @@ export function main() {
]);
});
it('should not cancel the previous transition if a follow-up transition is not matched',
fakeAsync(() => {
@Component({
selector: 'if-cmp',
template: `
<div [@myAnimation]="exp" (@myAnimation.start)="callback($event)" (@myAnimation.done)="callback($event)"></div>
`,
animations: [trigger(
'myAnimation',
[transition(
'a => b', [style({'opacity': '0'}), animate(500, style({'opacity': '1'}))])])],
})
class Cmp {
exp: any;
startEvent: any;
doneEvent: any;
callback(event: any) {
if (event.phaseName == 'done') {
this.doneEvent = event;
} else {
this.startEvent = event;
}
}
}
TestBed.configureTestingModule({declarations: [Cmp]});
const engine = TestBed.get(ɵAnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
cmp.exp = 'a';
fixture.detectChanges();
engine.flush();
expect(getLog().length).toEqual(0);
expect(engine.players.length).toEqual(0);
flushMicrotasks();
expect(cmp.startEvent.toState).toEqual('a');
expect(cmp.startEvent.totalTime).toEqual(0);
expect(cmp.startEvent.toState).toEqual('a');
expect(cmp.startEvent.totalTime).toEqual(0);
resetLog();
cmp.exp = 'b';
fixture.detectChanges();
engine.flush();
const players = getLog();
expect(players.length).toEqual(1);
expect(engine.players.length).toEqual(1);
flushMicrotasks();
expect(cmp.startEvent.toState).toEqual('b');
expect(cmp.startEvent.totalTime).toEqual(500);
expect(cmp.startEvent.toState).toEqual('b');
expect(cmp.startEvent.totalTime).toEqual(500);
resetLog();
let completed = false;
players[0].onDone(() => completed = true);
cmp.exp = 'c';
fixture.detectChanges();
engine.flush();
expect(engine.players.length).toEqual(1);
expect(getLog().length).toEqual(0);
flushMicrotasks();
expect(cmp.startEvent.toState).toEqual('c');
expect(cmp.startEvent.totalTime).toEqual(0);
expect(cmp.startEvent.toState).toEqual('c');
expect(cmp.startEvent.totalTime).toEqual(0);
expect(completed).toBe(false);
}));
it('should only turn a view removal as into `void` state transition', () => {
@Component({
selector: 'if-cmp',
@ -285,7 +367,7 @@ export function main() {
{opacity: '0', offset: 1},
]);
flushMicrotasks();
player.finish();
expect(fixture.debugElement.nativeElement.children.length).toBe(0);
}));
@ -335,6 +417,7 @@ export function main() {
{opacity: '0', offset: 1},
]);
player.finish();
flushMicrotasks();
expect(fixture.debugElement.nativeElement.children.length).toBe(0);
}));
@ -387,15 +470,17 @@ export function main() {
const [p1, p2] = getLog();
expect(p1.keyframes).toEqual([
{height: '100px', offset: 0},
{height: '0px', offset: 1},
]);
expect(p2.keyframes).toEqual([
{width: '100px', offset: 0},
{width: '0px', offset: 1},
]);
expect(p2.keyframes).toEqual([
{height: '100px', offset: 0},
{height: '0px', offset: 1},
]);
p1.finish();
p2.finish();
flushMicrotasks();
expect(fixture.debugElement.nativeElement.children.length).toBe(0);
}));
@ -621,7 +706,11 @@ export function main() {
template: `
<div #green @green></div>
`,
animations: [trigger('green', [state('*', style({backgroundColor: 'green'}))])]
animations: [trigger(
'green',
[
state('*', style({backgroundColor: 'green'})), transition('* => *', animate(500))
])]
})
class Cmp {
@ViewChild('green') public element: any;
@ -635,7 +724,7 @@ export function main() {
fixture.detectChanges();
engine.flush();
const player = engine.activePlayers.pop();
const player = engine.players.pop();
player.finish();
expect(getDOM().hasStyle(cmp.element.nativeElement, 'background-color', 'green'))
@ -774,13 +863,219 @@ export function main() {
fixture.detectChanges();
engine.flush();
expect(getLog().length).toEqual(0);
resetLog();
});
it('should cancel all active inner child animations when a parent removal animation is set to go',
() => {
@Component({
selector: 'ani-cmp',
template: `
<div *ngIf="exp1" @parent>
<div [@child]="exp2" class="child1"></div>
<div [@child]="exp2" class="child2"></div>
</div>
`,
animations: [
trigger('parent', [transition(
':leave',
[style({opacity: 0}), animate(1000, style({opacity: 1}))])]),
trigger('child', [transition(
'a => b',
[style({opacity: 0}), animate(1000, style({opacity: 1}))])])
]
})
class Cmp {
public exp1: any;
public exp2: any;
}
TestBed.configureTestingModule({declarations: [Cmp]});
const engine = TestBed.get(ɵAnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
cmp.exp1 = true;
cmp.exp2 = 'a';
fixture.detectChanges();
engine.flush();
resetLog();
cmp.exp2 = 'b';
fixture.detectChanges();
engine.flush();
let players = getLog();
expect(players.length).toEqual(2);
const [p1, p2] = players;
let count = 0;
p1.onDone(() => count++);
p2.onDone(() => count++);
cmp.exp1 = false;
fixture.detectChanges();
engine.flush();
expect(count).toEqual(2);
});
it('should destroy inner animations when a parent node is set for removal', () => {
@Component({
selector: 'ani-cmp',
template: `
<div #parent class="parent">
<div [@child]="exp" class="child1"></div>
<div [@child]="exp" class="child2"></div>
</div>
`,
animations: [trigger(
'child',
[transition('a => b', [style({opacity: 0}), animate(1000, style({opacity: 1}))])])]
})
class Cmp {
public exp: any;
@ViewChild('parent') public parentElement: any;
}
TestBed.configureTestingModule({declarations: [Cmp]});
const engine = TestBed.get(ɵAnimationEngine) as ɵAnimationEngine;
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
const someTrigger = trigger('someTrigger', []);
engine.registerTrigger(
DEFAULT_COMPONENT_ID, DEFAULT_NAMESPACE_ID, fixture.nativeElement, someTrigger.name,
someTrigger);
cmp.exp = 'a';
fixture.detectChanges();
engine.flush();
resetLog();
cmp.exp = 'b';
fixture.detectChanges();
engine.flush();
const players = getLog();
expect(players.length).toEqual(2);
const [p1, p2] = players;
let count = 0;
p1.onDone(() => count++);
p2.onDone(() => count++);
engine.onRemove(DEFAULT_NAMESPACE_ID, cmp.parentElement.nativeElement, null);
expect(count).toEqual(2);
});
it('should always make children wait for the parent animation to finish before any removals occur',
() => {
@Component({
selector: 'ani-cmp',
template: `
<div #parent [@parent]="exp1" class="parent">
<div #child1 *ngIf="exp2" class="child1"></div>
<div #child2 *ngIf="exp2" class="child2"></div>
</div>
`,
animations: [trigger(
'parent',
[transition(
'a => b', [style({opacity: 0}), animate(1000, style({opacity: 1}))])])]
})
class Cmp {
public exp1: any;
public exp2: any;
@ViewChild('parent') public parent: any;
@ViewChild('child1') public child1Elm: any;
@ViewChild('child2') public child2Elm: any;
}
TestBed.configureTestingModule({declarations: [Cmp]});
const engine = TestBed.get(ɵAnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
cmp.exp1 = 'a';
cmp.exp2 = true;
fixture.detectChanges();
engine.flush();
resetLog();
cmp.exp1 = 'b';
fixture.detectChanges();
engine.flush();
const player = getLog()[0];
const p = cmp.parent.nativeElement;
const c1 = cmp.child1Elm.nativeElement;
const c2 = cmp.child2Elm.nativeElement;
expect(p.contains(c1)).toBeTruthy();
expect(p.contains(c2)).toBeTruthy();
cmp.exp2 = false;
fixture.detectChanges();
engine.flush();
expect(p.contains(c1)).toBeTruthy();
expect(p.contains(c2)).toBeTruthy();
player.finish();
expect(p.contains(c1)).toBeFalsy();
expect(p.contains(c2)).toBeFalsy();
});
it('should substitute in values if the provided state match is an object with values', () => {
@Component({
selector: 'ani-cmp',
template: `
<div [@myAnimation]="exp"></div>
`,
animations: [trigger(
'myAnimation',
[transition(
'a => b',
[style({opacity: '{{ start }}'}), animate(1000, style({opacity: '{{ end }}'}))],
buildParams({start: '0', end: '1'}))])]
})
class Cmp {
public exp: any;
}
TestBed.configureTestingModule({declarations: [Cmp]});
const engine = TestBed.get(ɵAnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
cmp.exp = {value: 'a'};
fixture.detectChanges();
engine.flush();
resetLog();
cmp.exp = {value: 'b', params: {start: .3, end: .6}};
fixture.detectChanges();
engine.flush();
const player = getLog().pop() !;
expect(player.keyframes).toEqual([
{opacity: '0.3', offset: 0}, {opacity: '0.6', offset: 1}
]);
});
});
describe('animation listeners', () => {
it('should trigger a `start` state change listener for when the animation changes state from void => state',
() => {
fakeAsync(() => {
@Component({
selector: 'if-cmp',
template: `
@ -806,17 +1101,17 @@ export function main() {
const cmp = fixture.componentInstance;
cmp.exp = 'true';
fixture.detectChanges();
engine.flush();
flushMicrotasks();
expect(cmp.event.triggerName).toEqual('myAnimation');
expect(cmp.event.phaseName).toEqual('start');
expect(cmp.event.totalTime).toEqual(500);
expect(cmp.event.fromState).toEqual('void');
expect(cmp.event.toState).toEqual('true');
});
}));
it('should trigger a `done` state change listener for when the animation changes state from a => b',
() => {
fakeAsync(() => {
@Component({
selector: 'if-cmp',
template: `
@ -846,75 +1141,79 @@ export function main() {
expect(cmp.event).toBeFalsy();
const player = engine.activePlayers.pop();
const player = engine.players.pop();
player.finish();
flushMicrotasks();
expect(cmp.event.triggerName).toEqual('myAnimation123');
expect(cmp.event.phaseName).toEqual('done');
expect(cmp.event.totalTime).toEqual(999);
expect(cmp.event.fromState).toEqual('void');
expect(cmp.event.toState).toEqual('b');
});
}));
it('should handle callbacks for multiple triggers running simultaneously', () => {
@Component({
selector: 'if-cmp',
template: `
it('should handle callbacks for multiple triggers running simultaneously', fakeAsync(() => {
@Component({
selector: 'if-cmp',
template: `
<div [@ani1]="exp1" (@ani1.done)="callback1($event)"></div>
<div [@ani2]="exp2" (@ani2.done)="callback2($event)"></div>
`,
animations: [
trigger(
'ani1',
[
transition(
'* => a', [style({'opacity': '0'}), animate(999, style({'opacity': '1'}))]),
]),
trigger(
'ani2',
[
transition(
'* => b', [style({'width': '0px'}), animate(999, style({'width': '100px'}))]),
])
],
})
class Cmp {
exp1: any = false;
exp2: any = false;
event1: AnimationEvent;
event2: AnimationEvent;
callback1 = (event: any) => { this.event1 = event; };
callback2 = (event: any) => { this.event2 = event; };
}
animations: [
trigger(
'ani1',
[
transition(
'* => a',
[style({'opacity': '0'}), animate(999, style({'opacity': '1'}))]),
]),
trigger(
'ani2',
[
transition(
'* => b',
[style({'width': '0px'}), animate(999, style({'width': '100px'}))]),
])
],
})
class Cmp {
exp1: any = false;
exp2: any = false;
event1: AnimationEvent;
event2: AnimationEvent;
callback1 = (event: any) => { this.event1 = event; };
callback2 = (event: any) => { this.event2 = event; };
}
TestBed.configureTestingModule({declarations: [Cmp]});
TestBed.configureTestingModule({declarations: [Cmp]});
const engine = TestBed.get(ɵAnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
const engine = TestBed.get(ɵAnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
cmp.exp1 = 'a';
cmp.exp2 = 'b';
fixture.detectChanges();
engine.flush();
cmp.exp1 = 'a';
cmp.exp2 = 'b';
fixture.detectChanges();
engine.flush();
expect(cmp.event1).toBeFalsy();
expect(cmp.event2).toBeFalsy();
expect(cmp.event1).toBeFalsy();
expect(cmp.event2).toBeFalsy();
const player1 = engine.activePlayers[0];
const player2 = engine.activePlayers[1];
const player1 = engine.players[0];
const player2 = engine.players[1];
player1.finish();
expect(cmp.event1.triggerName).toBeTruthy('ani1');
expect(cmp.event2).toBeFalsy();
player1.finish();
player2.finish();
expect(cmp.event1).toBeFalsy();
expect(cmp.event2).toBeFalsy();
player2.finish();
expect(cmp.event1.triggerName).toBeTruthy('ani1');
expect(cmp.event2.triggerName).toBeTruthy('ani2');
});
flushMicrotasks();
expect(cmp.event1.triggerName).toBeTruthy('ani1');
expect(cmp.event2.triggerName).toBeTruthy('ani2');
}));
it('should handle callbacks for multiple triggers running simultaneously on the same element',
() => {
fakeAsync(() => {
@Component({
selector: 'if-cmp',
template: `
@ -960,20 +1259,21 @@ export function main() {
expect(cmp.event1).toBeFalsy();
expect(cmp.event2).toBeFalsy();
const player1 = engine.activePlayers[0];
const player2 = engine.activePlayers[1];
const player1 = engine.players[0];
const player2 = engine.players[1];
player1.finish();
expect(cmp.event1.triggerName).toBeTruthy('ani1');
player2.finish();
expect(cmp.event1).toBeFalsy();
expect(cmp.event2).toBeFalsy();
player2.finish();
flushMicrotasks();
expect(cmp.event1.triggerName).toBeTruthy('ani1');
expect(cmp.event2.triggerName).toBeTruthy('ani2');
});
}));
it('should trigger a state change listener for when the animation changes state from void => state on the host element',
() => {
fakeAsync(() => {
@Component({
selector: 'my-cmp',
template: `...`,
@ -1000,14 +1300,14 @@ export function main() {
const cmp = fixture.componentInstance;
cmp.exp = 'TRUE';
fixture.detectChanges();
engine.flush();
flushMicrotasks();
expect(cmp.event.triggerName).toEqual('myAnimation2');
expect(cmp.event.phaseName).toEqual('start');
expect(cmp.event.totalTime).toEqual(1000);
expect(cmp.event.fromState).toEqual('void');
expect(cmp.event.toState).toEqual('TRUE');
});
}));
it('should always fire callbacks even when a transition is not detected', fakeAsync(() => {
@Component({
@ -1044,7 +1344,8 @@ export function main() {
expect(cmp.log).toEqual(['start => b', 'done => b']);
}));
it('should fire callback events for leave animations', fakeAsync(() => {
it('should fire callback events for leave animations even if there is no leave transition',
fakeAsync(() => {
@Component({
selector: 'my-cmp',
template: `
@ -1082,6 +1383,233 @@ export function main() {
expect(cmp.log).toEqual(['start => void', 'done => void']);
}));
it('should fire callbacks on a sub animation once it starts and finishes', fakeAsync(() => {
@Component({
selector: 'my-cmp',
template: `
<div class="parent"
[@parent]="exp1"
(@parent.start)="cb('parent-start',$event)"
(@parent.done)="cb('parent-done', $event)">
<div class="child"
[@child]="exp2"
(@child.start)="cb('child-start',$event)"
(@child.done)="cb('child-done', $event)"></div>
</div>
`,
animations: [
trigger(
'parent',
[
transition(
'* => go',
[
style({width: '0px'}),
animate(1000, style({width: '100px'})),
query(
'.child',
[
animateChild({duration: '1s'}),
]),
animate(1000, style({width: '0px'})),
]),
]),
trigger(
'child',
[
transition(
'* => go',
[
style({height: '0px'}),
animate(1000, style({height: '100px'})),
]),
])
]
})
class Cmp {
log: string[] = [];
exp1: string;
exp2: string;
cb(name: string, event: AnimationEvent) { this.log.push(name); }
}
TestBed.configureTestingModule({declarations: [Cmp]});
const engine = TestBed.get(ɵAnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
cmp.exp1 = 'go';
cmp.exp2 = 'go';
fixture.detectChanges();
engine.flush();
flushMicrotasks();
expect(cmp.log).toEqual(['parent-start', 'child-start']);
cmp.log = [];
const players = getLog();
expect(players.length).toEqual(3);
const [p1, p2, p3] = players;
p1.finish();
flushMicrotasks();
expect(cmp.log).toEqual([]);
p2.finish();
flushMicrotasks();
expect(cmp.log).toEqual([]);
p3.finish();
flushMicrotasks();
expect(cmp.log).toEqual(['parent-done', 'child-done']);
}));
it('should fire callbacks and collect the correct the totalTime and element details for any queried sub animations',
fakeAsync(
() => {
@Component({
selector: 'my-cmp',
template: `
<div class="parent" [@parent]="exp" (@parent.done)="cb('all','done', $event)">
<div *ngFor="let item of items"
class="item item-{{ item }}"
@child
(@child.start)="cb('c-' + item, 'start', $event)"
(@child.done)="cb('c-' + item, 'done', $event)">
{{ item }}
</div>
</div>
`,
animations: [
trigger('parent', [
transition('* => go', [
style({ opacity: 0 }),
animate('1s', style({ opacity: 1 })),
query('.item', [
style({ opacity: 0 }),
animate(1000, style({ opacity: 1 }))
]),
query('.item', [
animateChild({ duration: '1.8s', delay: '300ms' })
])
])
]),
trigger('child', [
transition(':enter', [
style({ opacity: 0 }),
animate(1500, style({ opactiy: 1 }))
])
])
]
})
class Cmp {
log: string[] = [];
events: {[name: string]: any} = {};
exp: string;
items: any = [0, 1, 2, 3];
cb(name: string, phase: string, event: AnimationEvent) {
this.log.push(name + '-' + phase);
this.events[name] = event;
}
}
TestBed.configureTestingModule({declarations: [Cmp]});
const engine = TestBed.get(ɵAnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
cmp.exp = 'go';
fixture.detectChanges();
engine.flush();
flushMicrotasks();
expect(cmp.log).toEqual(['c-0-start', 'c-1-start', 'c-2-start', 'c-3-start']);
cmp.log = [];
const players = getLog();
// 1 + 4 + 4 = 9 players
expect(players.length).toEqual(9);
const [pA, pq1a, pq1b, pq1c, pq1d, pq2a, pq2b, pq2c, pq2d] = getLog();
pA.finish();
pq1a.finish();
pq1b.finish();
pq1c.finish();
pq1d.finish();
flushMicrotasks();
expect(cmp.log).toEqual([]);
pq2a.finish();
pq2b.finish();
pq2c.finish();
pq2d.finish();
flushMicrotasks();
expect(cmp.log).toEqual(
['all-done', 'c-0-done', 'c-1-done', 'c-2-done', 'c-3-done']);
expect(cmp.events['c-0'].totalTime).toEqual(4100); // 1000 + 1000 + 1800 + 300
expect(cmp.events['c-0'].element.innerText.trim()).toEqual('0');
expect(cmp.events['c-1'].totalTime).toEqual(4100);
expect(cmp.events['c-1'].element.innerText.trim()).toEqual('1');
expect(cmp.events['c-2'].totalTime).toEqual(4100);
expect(cmp.events['c-2'].element.innerText.trim()).toEqual('2');
expect(cmp.events['c-3'].totalTime).toEqual(4100);
expect(cmp.events['c-3'].element.innerText.trim()).toEqual('3');
}));
});
it('should throw neither state() or transition() are used inside of trigger()', () => {
@Component({
selector: 'if-cmp',
template: `
<div [@myAnimation]="exp"></div>
`,
animations: [trigger('myAnimation', [animate(1000, style({width: '100px'}))])]
})
class Cmp {
exp: any = false;
}
TestBed.configureTestingModule({declarations: [Cmp]});
expect(() => { TestBed.createComponent(Cmp); })
.toThrowError(
/only state\(\) and transition\(\) definitions can sit inside of a trigger\(\)/);
});
it('should not throw an error if styles overlap in separate transitions', () => {
@Component({
selector: 'if-cmp',
template: `
<div [@myAnimation]="exp"></div>
`,
animations: [
trigger(
'myAnimation',
[
transition(
'void => *',
[
style({opacity: 0}),
animate('0.5s 1s', style({opacity: 1})),
]),
transition(
'* => void',
[animate(1000, style({height: 0})), animate(1000, style({opacity: 0}))]),
]),
]
})
class Cmp {
exp: any = false;
}
TestBed.configureTestingModule({declarations: [Cmp]});
expect(() => { TestBed.createComponent(Cmp); }).not.toThrowError();
});
describe('errors for not using the animation module', () => {
@ -1128,3 +1656,7 @@ function assertHasParent(element: any, yes: boolean) {
expect(parent).toBeFalsy();
}
}
function buildParams(params: {[name: string]: any}): AnimationOptions {
return {params};
}

File diff suppressed because it is too large Load Diff

View File

@ -26,25 +26,23 @@ export function main() {
});
});
it('should animate a component that captures height during an animation', () => {
it('should compute pre (!) and post (*) animation styles with different dom states', () => {
@Component({
selector: 'if-cmp',
selector: 'ani-cmp',
template: `
<div *ngIf="exp" #element [@myAnimation]="exp">
hello {{ text }}
</div>
`,
<div [@myAnimation]="exp" #parent>
<div *ngFor="let item of items" class="child" style="line-height:20px">
- {{ item }}
</div>
</div>
`,
animations: [trigger(
'myAnimation',
[
transition('* => *', [style({height: '0px'}), animate(1000, style({height: '*'}))]),
])]
[transition('* => *', [style({height: '!'}), animate(1000, style({height: '*'}))])])]
})
class Cmp {
exp: any = false;
text: string;
@ViewChild('element') public element: any;
public exp: number;
public items = [0, 1, 2, 3, 4];
}
TestBed.configureTestingModule({declarations: [Cmp]});
@ -52,33 +50,36 @@ export function main() {
const engine = TestBed.get(ɵAnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
cmp.exp = 1;
cmp.text = '';
fixture.detectChanges();
engine.flush();
const element = cmp.element.nativeElement;
element.style.lineHeight = '20px';
element.style.width = '50px';
expect(engine.players.length).toEqual(1);
let player = engine.players[0];
let webPlayer = player.getRealPlayer() as ɵWebAnimationsPlayer;
expect(webPlayer.keyframes).toEqual([
{height: '0px', offset: 0}, {height: '100px', offset: 1}
]);
// we destroy the player because since it has started and is
// at 0ms duration a height value of `0px` will be extracted
// from the element and passed into the follow-up animation.
player.destroy();
cmp.exp = 2;
cmp.text = '12345';
cmp.items = [0, 1, 2, 6];
fixture.detectChanges();
engine.flush();
let player = engine.activePlayers.pop() as ɵWebAnimationsPlayer;
player.setPosition(1);
expect(engine.players.length).toEqual(1);
player = engine.players[0];
webPlayer = player.getRealPlayer() as ɵWebAnimationsPlayer;
assertStyleBetween(element, 'height', 15, 25);
cmp.exp = 3;
cmp.text = '12345-12345-12345-12345';
fixture.detectChanges();
engine.flush();
player = engine.activePlayers.pop() as ɵWebAnimationsPlayer;
player.setPosition(1);
assertStyleBetween(element, 'height', 35, 45);
expect(webPlayer.keyframes).toEqual([
{height: '100px', offset: 0}, {height: '80px', offset: 1}
]);
});
});
}

View File

@ -0,0 +1,112 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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 {Animation, AnimationBuilder, AnimationMetadata, AnimationOptions, AnimationPlayer, NoopAnimationPlayer, sequence} from '@angular/animations';
import {Injectable, RendererFactory2, RendererType2, ViewEncapsulation} from '@angular/core';
import {AnimationRenderer} from './animation_renderer';
@Injectable()
export class BrowserAnimationBuilder extends AnimationBuilder {
private _nextAnimationId = 0;
private _renderer: AnimationRenderer;
constructor(rootRenderer: RendererFactory2) {
super();
const typeData = {
id: '0',
encapsulation: ViewEncapsulation.None,
styles: [],
data: {animation: []}
} as RendererType2;
this._renderer = rootRenderer.createRenderer(document.body, typeData) as AnimationRenderer;
}
build(animation: AnimationMetadata|AnimationMetadata[]): Animation {
const id = this._nextAnimationId.toString();
this._nextAnimationId++;
const entry = Array.isArray(animation) ? sequence(animation) : animation;
issueAnimationCommand(this._renderer, null, id, 'register', [entry]);
return new BrowserAnimation(id, this._renderer);
}
}
@Injectable()
export class NoopAnimationBuilder extends BrowserAnimationBuilder {
build(animation: AnimationMetadata|AnimationMetadata[]): Animation { return new NoopAnimation(); }
}
export class BrowserAnimation extends Animation {
constructor(private _id: string, private _renderer: AnimationRenderer) { super(); }
create(element: any, options?: AnimationOptions): AnimationPlayer {
return new RendererAnimationPlayer(this._id, element, options || {}, this._renderer);
}
}
export class NoopAnimation extends Animation {
constructor() { super(); }
create(element: any, options?: AnimationOptions): AnimationPlayer {
return new NoopAnimationPlayer();
}
}
export class RendererAnimationPlayer implements AnimationPlayer {
public parentPlayer: AnimationPlayer|null = null;
private _started = false;
constructor(
public id: string, public element: any, options: AnimationOptions,
private _renderer: AnimationRenderer) {
this._command('create', options);
}
private _listen(eventName: string, callback: (event: any) => any): () => void {
return this._renderer.listen(this.element, `@@${this.id}:${eventName}`, callback);
}
private _command(command: string, ...args: any[]) {
return issueAnimationCommand(this._renderer, this.element, this.id, command, args);
}
onDone(fn: () => void): void { this._listen('done', fn); }
onStart(fn: () => void): void { this._listen('start', fn); }
onDestroy(fn: () => void): void { this._listen('destroy', fn); }
init(): void { this._command('init'); }
hasStarted(): boolean { return this._started; }
play(): void {
this._command('play');
this._started = true;
}
pause(): void { this._command('pause'); }
restart(): void { this._command('restart'); }
finish(): void { this._command('finish'); }
destroy(): void { this._command('destroy'); }
reset(): void { this._command('reset'); }
setPosition(p: number): void { this._command('setPosition', p); }
getPosition(): number { return 0; }
public totalTime = 0;
}
function issueAnimationCommand(
renderer: AnimationRenderer, element: any, id: string, command: string, args: any[]): any {
return renderer.setProperty(element, `@@${id}:${command}`, args);
}

View File

@ -11,18 +11,33 @@ import {Injectable, NgZone, Renderer2, RendererFactory2, RendererStyleFlags2, Re
@Injectable()
export class AnimationRendererFactory implements RendererFactory2 {
private _currentId: number = 0;
constructor(
private delegate: RendererFactory2, private _engine: AnimationEngine, private _zone: NgZone) {
_engine.onRemovalComplete = (element: any, delegate: any) => {
// Note: if an component element has a leave animation, and the component
// a host leave animation, the view engine will call `removeChild` for the parent
// component renderer as well as for the child component renderer.
// Therefore, we need to check if we already removed the element.
if (delegate && delegate.parentNode(element)) {
delegate.removeChild(element.parentNode, element);
}
};
}
createRenderer(hostElement: any, type: RendererType2): Renderer2 {
let delegate = this.delegate.createRenderer(hostElement, type);
if (!hostElement || !type || !type.data || !type.data['animation']) return delegate;
const namespaceId = type.id;
const componentId = type.id;
const namespaceId = type.id + '-' + this._currentId;
this._currentId++;
const animationTriggers = type.data['animation'] as AnimationTriggerMetadata[];
animationTriggers.forEach(
trigger => this._engine.registerTrigger(trigger, namespaceify(namespaceId, trigger.name)));
trigger => this._engine.registerTrigger(
componentId, namespaceId, hostElement, trigger.name, trigger));
return new AnimationRenderer(delegate, this._engine, this._zone, namespaceId);
}
@ -31,7 +46,9 @@ export class AnimationRendererFactory implements RendererFactory2 {
this.delegate.begin();
}
}
end() {
this._zone.runOutsideAngular(() => this._engine.flush());
if (this.delegate.end) {
this.delegate.end();
}
@ -40,7 +57,7 @@ export class AnimationRendererFactory implements RendererFactory2 {
export class AnimationRenderer implements Renderer2 {
public destroyNode: ((node: any) => any)|null = null;
private _flushPromise: Promise<any>|null = null;
private _animationCallbacksBuffer: [(e: any) => any, any][] = [];
constructor(
public delegate: Renderer2, private _engine: AnimationEngine, private _zone: NgZone,
@ -50,7 +67,10 @@ export class AnimationRenderer implements Renderer2 {
get data() { return this.delegate.data; }
destroy(): void { this.delegate.destroy(); }
destroy(): void {
this._engine.destroy(this._namespaceId, this.delegate);
this.delegate.destroy();
}
createElement(name: string, namespace?: string): any {
return this.delegate.createElement(name, namespace);
@ -91,32 +111,23 @@ export class AnimationRenderer implements Renderer2 {
setValue(node: any, value: string): void { this.delegate.setValue(node, value); }
appendChild(parent: any, newChild: any): void {
this._engine.onInsert(newChild, () => this.delegate.appendChild(parent, newChild));
this._queueFlush();
this.delegate.appendChild(parent, newChild);
this._engine.onInsert(this._namespaceId, newChild, parent, false);
}
insertBefore(parent: any, newChild: any, refChild: any): void {
this._engine.onInsert(newChild, () => this.delegate.insertBefore(parent, newChild, refChild));
this._queueFlush();
this.delegate.insertBefore(parent, newChild, refChild);
this._engine.onInsert(this._namespaceId, newChild, parent, true);
}
removeChild(parent: any, oldChild: any): void {
this._engine.onRemove(oldChild, () => {
// Note: if an component element has a leave animation, and the component
// a host leave animation, the view engine will call `removeChild` for the parent
// component renderer as well as for the child component renderer.
// Therefore, we need to check if we already removed the element.
if (this.delegate.parentNode(oldChild)) {
this.delegate.removeChild(parent, oldChild);
}
});
this._queueFlush();
this._engine.onRemove(this._namespaceId, oldChild, this.delegate);
}
setProperty(el: any, name: string, value: any): void {
if (name.charAt(0) == '@') {
this._engine.setProperty(el, namespaceify(this._namespaceId, name.substr(1)), value);
this._queueFlush();
name = name.substr(1);
this._engine.setProperty(this._namespaceId, el, name, value);
} else {
this.delegate.setProperty(el, name, value);
}
@ -126,28 +137,32 @@ export class AnimationRenderer implements Renderer2 {
() => void {
if (eventName.charAt(0) == '@') {
const element = resolveElementFromTarget(target);
const [name, phase] = parseTriggerCallbackName(eventName.substr(1));
return this._engine.listen(
element, namespaceify(this._namespaceId, name), phase, (event: any) => {
const e = event as any;
if (e.triggerName) {
e.triggerName = deNamespaceify(this._namespaceId, e.triggerName);
}
this._zone.run(() => callback(event));
});
let name = eventName.substr(1);
let phase = '';
if (name.charAt(0) != '@') { // transition-specific
[name, phase] = parseTriggerCallbackName(name);
}
return this._engine.listen(this._namespaceId, element, name, phase, event => {
this._bufferMicrotaskIntoZone(callback, event);
});
}
return this.delegate.listen(target, eventName, callback);
}
private _queueFlush() {
if (!this._flushPromise) {
this._zone.runOutsideAngular(() => {
this._flushPromise = Promise.resolve(null).then(() => {
this._flushPromise = null !;
this._engine.flush();
private _bufferMicrotaskIntoZone(fn: (e: any) => any, data: any) {
if (this._animationCallbacksBuffer.length == 0) {
Promise.resolve(null).then(() => {
this._zone.run(() => {
this._animationCallbacksBuffer.forEach(tuple => {
const [fn, data] = tuple;
fn(data);
});
this._animationCallbacksBuffer = [];
});
});
})
}
this._animationCallbacksBuffer.push([fn, data]);
}
}
@ -170,11 +185,3 @@ function parseTriggerCallbackName(triggerName: string) {
const phase = triggerName.substr(dotIndex + 1);
return [trigger, phase];
}
function namespaceify(namespaceId: string, value: string): string {
return `${namespaceId}#${value}`;
}
function deNamespaceify(namespaceId: string, value: string): string {
return value.replace(namespaceId + '#', '');
}

View File

@ -12,4 +12,5 @@
* Entry point for all animation APIs of the animation browser package.
*/
export {BrowserAnimationsModule, NoopAnimationsModule} from './module';
export * from './private_export';

View File

@ -5,4 +5,6 @@
* 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
*/
export {NoopAnimation as ɵNoopAnimation, NoopAnimationBuilder as ɵNoopAnimationBuilder} from './animation_builder';
export {BrowserAnimation as ɵBrowserAnimation, BrowserAnimationBuilder as ɵBrowserAnimationBuilder} from './animation_builder';
export {AnimationRenderer as ɵAnimationRenderer, AnimationRendererFactory as ɵAnimationRendererFactory} from './animation_renderer';

View File

@ -6,10 +6,12 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AnimationDriver, ɵAnimationEngine as AnimationEngine, ɵAnimationStyleNormalizer as AnimationStyleNormalizer, ɵDomAnimationEngine as DomAnimationEngine, ɵNoopAnimationDriver as NoopAnimationDriver, ɵNoopAnimationEngine as NoopAnimationEngine, ɵWebAnimationsDriver as WebAnimationsDriver, ɵWebAnimationsStyleNormalizer as WebAnimationsStyleNormalizer, ɵsupportsWebAnimations as supportsWebAnimations} from '@angular/animations/browser';
import {AnimationBuilder} from '@angular/animations';
import {AnimationDriver, ɵAnimationEngine as AnimationEngine, ɵAnimationStyleNormalizer as AnimationStyleNormalizer, ɵDomAnimationEngine as DomAnimationEngine, ɵNoopAnimationDriver as NoopAnimationDriver, ɵNoopAnimationEngine as NoopAnimationEngine, ɵNoopAnimationStyleNormalizer as NoopAnimationStyleNormalizer, ɵWebAnimationsDriver as WebAnimationsDriver, ɵWebAnimationsStyleNormalizer as WebAnimationsStyleNormalizer, ɵsupportsWebAnimations as supportsWebAnimations} from '@angular/animations/browser';
import {Injectable, NgZone, Provider, RendererFactory2} from '@angular/core';
import {ɵDomRendererFactory2 as DomRendererFactory2} from '@angular/platform-browser';
import {BrowserAnimationBuilder, NoopAnimationBuilder} from './animation_builder';
import {AnimationRendererFactory} from './animation_renderer';
@Injectable()
@ -19,6 +21,13 @@ export class InjectableAnimationEngine extends DomAnimationEngine {
}
}
@Injectable()
export class InjectableNoopAnimationEngine extends NoopAnimationEngine {
constructor(driver: AnimationDriver, normalizer: AnimationStyleNormalizer) {
super(driver, normalizer);
}
}
export function instantiateSupportedAnimationDriver() {
if (supportsWebAnimations()) {
return new WebAnimationsDriver();
@ -40,6 +49,7 @@ export function instantiateRendererFactory(
* include them in the BrowserModule.
*/
export const BROWSER_ANIMATIONS_PROVIDERS: Provider[] = [
{provide: AnimationBuilder, useClass: NoopAnimationBuilder},
{provide: AnimationDriver, useFactory: instantiateSupportedAnimationDriver},
{provide: AnimationStyleNormalizer, useFactory: instantiateDefaultStyleNormalizer},
{provide: AnimationEngine, useClass: InjectableAnimationEngine}, {
@ -54,7 +64,14 @@ export const BROWSER_ANIMATIONS_PROVIDERS: Provider[] = [
* include them in the BrowserTestingModule.
*/
export const BROWSER_NOOP_ANIMATIONS_PROVIDERS: Provider[] = [
{provide: AnimationEngine, useClass: NoopAnimationEngine}, {
{provide: AnimationBuilder, useClass: BrowserAnimationBuilder},
{provide: AnimationDriver, useClass: NoopAnimationDriver},
{provide: AnimationStyleNormalizer, useFactory: instantiateDefaultStyleNormalizer}, {
provide: AnimationEngine,
useClass: NoopAnimationEngine,
deps: [AnimationDriver, AnimationStyleNormalizer]
},
{
provide: RendererFactory2,
useFactory: instantiateRendererFactory,
deps: [DomRendererFactory2, AnimationEngine, NgZone]

View File

@ -5,10 +5,19 @@
* 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 {state, style, trigger} from '@angular/animations';
import {AnimationMetadata, AnimationTriggerMetadata, state, style, trigger} from '@angular/animations';
import {ɵNoopAnimationEngine as NoopAnimationEngine} from '@angular/animations/browser';
import {NoopAnimationStyleNormalizer} from '@angular/animations/browser/src/dsl/style_normalization/animation_style_normalizer';
import {MockAnimationDriver} from '@angular/animations/browser/testing';
import {el} from '@angular/platform-browser/testing/src/browser_util';
import {TriggerAst} from '../../../animations/browser/src/dsl/animation_ast';
import {buildAnimationAst} from '../../../animations/browser/src/dsl/animation_ast_builder';
import {buildTrigger} from '../../../animations/browser/src/dsl/animation_trigger';
const DEFAULT_NAMESPACE_ID = 'id';
const DEFAULT_COMPONENT_ID = '1';
export function main() {
describe('NoopAnimationEngine', () => {
let captures: string[] = [];
@ -16,19 +25,37 @@ export function main() {
beforeEach(() => { captures = []; });
function makeEngine() {
const driver = new MockAnimationDriver();
const normalizer = new NoopAnimationStyleNormalizer();
return new NoopAnimationEngine(driver, normalizer);
}
it('should immediately issue DOM removals during remove animations and then fire the animation callbacks after flush',
() => {
const engine = new NoopAnimationEngine();
const engine = makeEngine();
const capture1 = capture('1');
const capture2 = capture('2');
engine.onRemovalComplete = (element: any, context: any) => {
switch (context as string) {
case '1':
capture1();
break;
case '2':
capture2();
break;
}
};
const elm1 = {nodeType: 1};
const elm2 = {nodeType: 1};
engine.onRemove(elm1, capture('1'));
engine.onRemove(elm2, capture('2'));
engine.onRemove(DEFAULT_NAMESPACE_ID, elm1, '1');
engine.onRemove(DEFAULT_NAMESPACE_ID, elm2, '2');
engine.listen(elm1, 'trig', 'start', capture('1-start'));
engine.listen(elm2, 'trig', 'start', capture('2-start'));
engine.listen(elm1, 'trig', 'done', capture('1-done'));
engine.listen(elm2, 'trig', 'done', capture('2-done'));
listen(elm1, engine, 'trig', 'start', capture('1-start'));
listen(elm2, engine, 'trig', 'start', capture('2-start'));
listen(elm1, engine, 'trig', 'done', capture('1-done'));
listen(elm2, engine, 'trig', 'done', capture('2-done'));
expect(captures).toEqual(['1', '2']);
engine.flush();
@ -37,17 +64,17 @@ export function main() {
});
it('should only fire the `start` listener for a trigger that has had a property change', () => {
const engine = new NoopAnimationEngine();
const engine = makeEngine();
const elm1 = {};
const elm2 = {};
const elm3 = {};
engine.listen(elm1, 'trig1', 'start', capture());
engine.setProperty(elm1, 'trig1', 'cool');
engine.setProperty(elm2, 'trig2', 'sweet');
engine.listen(elm2, 'trig2', 'start', capture());
engine.listen(elm3, 'trig3', 'start', capture());
listen(elm1, engine, 'trig1', 'start', capture());
setProperty(elm1, engine, 'trig1', 'cool');
setProperty(elm2, engine, 'trig2', 'sweet');
listen(elm2, engine, 'trig2', 'start', capture());
listen(elm3, engine, 'trig3', 'start', capture());
expect(captures).toEqual([]);
engine.flush();
@ -79,17 +106,17 @@ export function main() {
});
it('should only fire the `done` listener for a trigger that has had a property change', () => {
const engine = new NoopAnimationEngine();
const engine = makeEngine();
const elm1 = {};
const elm2 = {};
const elm3 = {};
engine.listen(elm1, 'trig1', 'done', capture());
engine.setProperty(elm1, 'trig1', 'awesome');
engine.setProperty(elm2, 'trig2', 'amazing');
engine.listen(elm2, 'trig2', 'done', capture());
engine.listen(elm3, 'trig3', 'done', capture());
listen(elm1, engine, 'trig1', 'done', capture());
setProperty(elm1, engine, 'trig1', 'awesome');
setProperty(elm2, engine, 'trig2', 'amazing');
listen(elm2, engine, 'trig2', 'done', capture());
listen(elm3, engine, 'trig3', 'done', capture());
expect(captures).toEqual([]);
engine.flush();
@ -122,48 +149,49 @@ export function main() {
it('should deregister a listener when the return function is called, but only after flush',
() => {
const engine = new NoopAnimationEngine();
const engine = makeEngine();
const elm = {};
const fn1 = engine.listen(elm, 'trig1', 'start', capture('trig1-start'));
const fn2 = engine.listen(elm, 'trig2', 'done', capture('trig2-done'));
const fn1 = listen(elm, engine, 'trig1', 'start', capture('trig1-start'));
const fn2 = listen(elm, engine, 'trig2', 'done', capture('trig2-done'));
engine.setProperty(elm, 'trig1', 'value1');
engine.setProperty(elm, 'trig2', 'value2');
setProperty(elm, engine, 'trig1', 'value1');
setProperty(elm, engine, 'trig2', 'value2');
engine.flush();
expect(captures).toEqual(['trig1-start', 'trig2-done']);
captures = [];
engine.setProperty(elm, 'trig1', 'value3');
engine.setProperty(elm, 'trig2', 'value4');
setProperty(elm, engine, 'trig1', 'value3');
setProperty(elm, engine, 'trig2', 'value4');
fn1();
engine.flush();
expect(captures).toEqual(['trig1-start', 'trig2-done']);
captures = [];
engine.setProperty(elm, 'trig1', 'value5');
engine.setProperty(elm, 'trig2', 'value6');
setProperty(elm, engine, 'trig1', 'value5');
setProperty(elm, engine, 'trig2', 'value6');
fn2();
engine.flush();
expect(captures).toEqual(['trig2-done']);
captures = [];
engine.setProperty(elm, 'trig1', 'value7');
engine.setProperty(elm, 'trig2', 'value8');
setProperty(elm, engine, 'trig1', 'value7');
setProperty(elm, engine, 'trig2', 'value8');
engine.flush();
expect(captures).toEqual([]);
});
it('should fire a removal listener even if the listener is deregistered prior to flush', () => {
const engine = new NoopAnimationEngine();
const engine = makeEngine();
const elm = {nodeType: 1};
engine.onRemovalComplete = (element: any, context: string) => { capture(context)(); };
const fn = engine.listen(elm, 'trig', 'start', capture('removal listener'));
const fn = listen(elm, engine, 'trig', 'start', capture('removal listener'));
fn();
engine.onRemove(elm, capture('dom removal'));
engine.onRemove(DEFAULT_NAMESPACE_ID, elm, 'dom removal');
engine.flush();
expect(captures).toEqual(['dom removal', 'removal listener']);
@ -174,15 +202,15 @@ export function main() {
if (typeof Element == 'undefined') return;
it('should persist the styles on the element when the animation is complete', () => {
const engine = new NoopAnimationEngine();
engine.registerTrigger(trigger('matias', [
state('a', style({width: '100px'})),
]));
const engine = makeEngine();
const element = el('<div></div>');
registerTrigger(element, engine, trigger('matias', [
state('a', style({width: '100px'})),
]));
expect(element.style.width).not.toEqual('100px');
engine.setProperty(element, 'matias', 'a');
setProperty(element, engine, 'matias', 'a');
expect(element.style.width).not.toEqual('100px');
engine.flush();
@ -191,19 +219,19 @@ export function main() {
it('should remove previously persist styles off of the element when a follow-up animation starts',
() => {
const engine = new NoopAnimationEngine();
engine.registerTrigger(trigger('matias', [
state('a', style({width: '100px'})),
state('b', style({height: '100px'})),
]));
const engine = makeEngine();
const element = el('<div></div>');
engine.setProperty(element, 'matias', 'a');
registerTrigger(element, engine, trigger('matias', [
state('a', style({width: '100px'})),
state('b', style({height: '100px'})),
]));
setProperty(element, engine, 'matias', 'a');
engine.flush();
expect(element.style.width).toEqual('100px');
engine.setProperty(element, 'matias', 'b');
setProperty(element, engine, 'matias', 'b');
expect(element.style.width).not.toEqual('100px');
expect(element.style.height).not.toEqual('100px');
@ -212,17 +240,35 @@ export function main() {
});
it('should fall back to `*` styles incase the target state styles are not found', () => {
const engine = new NoopAnimationEngine();
engine.registerTrigger(trigger('matias', [
state('*', style({opacity: '0.5'})),
]));
const engine = makeEngine();
const element = el('<div></div>');
engine.setProperty(element, 'matias', 'xyz');
registerTrigger(element, engine, trigger('matias', [
state('*', style({opacity: '0.5'})),
]));
setProperty(element, engine, 'matias', 'xyz');
engine.flush();
expect(element.style.opacity).toEqual('0.5');
});
});
});
}
function registerTrigger(
element: any, engine: NoopAnimationEngine, metadata: AnimationTriggerMetadata,
namespaceId: string = DEFAULT_NAMESPACE_ID, componentId: string = DEFAULT_COMPONENT_ID) {
engine.registerTrigger(componentId, namespaceId, element, name, metadata)
}
function setProperty(
element: any, engine: NoopAnimationEngine, property: string, value: any,
id: string = DEFAULT_NAMESPACE_ID) {
engine.setProperty(id, element, property, value);
}
function listen(
element: any, engine: NoopAnimationEngine, eventName: string, phaseName: string,
callback: (event: any) => any, id: string = DEFAULT_NAMESPACE_ID) {
return engine.listen(id, element, eventName, phaseName, callback);
}

View File

@ -6,22 +6,23 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AnimationPlayer, AnimationTriggerMetadata, animate, state, style, transition, trigger} from '@angular/animations';
import {ɵAnimationEngine} from '@angular/animations/browser';
import {Component, Injectable, RendererFactory2, RendererType2, ViewChild} from '@angular/core';
import {ɵAnimationEngine as AnimationEngine} from '@angular/animations/browser';
import {Component, Injectable, NgZone, RendererFactory2, RendererType2, ViewChild} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {BrowserAnimationsModule, ɵAnimationRendererFactory} from '@angular/platform-browser/animations';
import {BrowserAnimationsModule, ɵAnimationRendererFactory as AnimationRendererFactory} from '@angular/platform-browser/animations';
import {DomRendererFactory2} from '@angular/platform-browser/src/dom/dom_renderer';
import {InjectableAnimationEngine} from '../../animations/src/providers';
import {el} from '../../testing/src/browser_util';
export function main() {
describe('ɵAnimationRenderer', () => {
describe('AnimationRenderer', () => {
let element: any;
beforeEach(() => {
element = el('<div></div>');
TestBed.configureTestingModule({
providers: [{provide: ɵAnimationEngine, useClass: MockAnimationEngine}],
providers: [{provide: AnimationEngine, useClass: MockAnimationEngine}],
imports: [BrowserAnimationsModule]
});
});
@ -33,20 +34,13 @@ export function main() {
styles: [],
data: {'animation': animationTriggers}
};
return (TestBed.get(RendererFactory2) as ɵAnimationRendererFactory)
return (TestBed.get(RendererFactory2) as AnimationRendererFactory)
.createRenderer(element, type);
}
it('should register the provided triggers with the view engine when created', () => {
const renderer = makeRenderer([trigger('trig1', []), trigger('trig2', [])]);
const engine = TestBed.get(ɵAnimationEngine) as MockAnimationEngine;
expect(engine.triggers.map(t => t.name)).toEqual(['trig1', 'trig2']);
});
it('should hook into the engine\'s insert operations when appending children', () => {
const renderer = makeRenderer();
const engine = TestBed.get(ɵAnimationEngine) as MockAnimationEngine;
const engine = TestBed.get(AnimationEngine) as MockAnimationEngine;
const container = el('<div></div>');
renderer.appendChild(container, element);
@ -56,7 +50,7 @@ export function main() {
it('should hook into the engine\'s insert operations when inserting a child before another',
() => {
const renderer = makeRenderer();
const engine = TestBed.get(ɵAnimationEngine) as MockAnimationEngine;
const engine = TestBed.get(AnimationEngine) as MockAnimationEngine;
const container = el('<div></div>');
const element2 = el('<div></div>');
container.appendChild(element2);
@ -67,7 +61,7 @@ export function main() {
it('should hook into the engine\'s insert operations when removing children', () => {
const renderer = makeRenderer();
const engine = TestBed.get(ɵAnimationEngine) as MockAnimationEngine;
const engine = TestBed.get(AnimationEngine) as MockAnimationEngine;
const container = el('<div></div>');
renderer.removeChild(container, element);
@ -76,19 +70,19 @@ export function main() {
it('should hook into the engine\'s setProperty call if the property begins with `@`', () => {
const renderer = makeRenderer();
const engine = TestBed.get(ɵAnimationEngine) as MockAnimationEngine;
const engine = TestBed.get(AnimationEngine) as MockAnimationEngine;
renderer.setProperty(element, 'prop', 'value');
expect(engine.captures['setProperty']).toBeFalsy();
renderer.setProperty(element, '@prop', 'value');
expect(engine.captures['setProperty'].pop()).toEqual([element, 'id#prop', 'value']);
expect(engine.captures['setProperty'].pop()).toEqual([element, 'prop', 'value']);
});
describe('listen', () => {
it('should hook into the engine\'s listen call if the property begins with `@`', () => {
const renderer = makeRenderer();
const engine = TestBed.get(ɵAnimationEngine) as MockAnimationEngine;
const engine = TestBed.get(AnimationEngine) as MockAnimationEngine;
const cb = (event: any): boolean => { return true; };
@ -96,13 +90,13 @@ export function main() {
expect(engine.captures['listen']).toBeFalsy();
renderer.listen(element, '@event.phase', cb);
expect(engine.captures['listen'].pop()).toEqual([element, 'id#event', 'phase']);
expect(engine.captures['listen'].pop()).toEqual([element, 'event', 'phase']);
});
it('should resolve the body|document|window nodes given their values as strings as input',
() => {
const renderer = makeRenderer();
const engine = TestBed.get(ɵAnimationEngine) as MockAnimationEngine;
const engine = TestBed.get(AnimationEngine) as MockAnimationEngine;
const cb = (event: any): boolean => { return true; };
@ -117,6 +111,10 @@ export function main() {
});
});
describe('registering animations', () => {
it('should only create a trigger definition once even if the registered multiple times');
});
describe('flushing animations', () => {
// these tests are only mean't to be run within the DOM
if (typeof Element == 'undefined') return;
@ -138,11 +136,11 @@ export function main() {
}
TestBed.configureTestingModule({
providers: [{provide: ɵAnimationEngine, useClass: InjectableAnimationEngine}],
providers: [{provide: AnimationEngine, useClass: InjectableAnimationEngine}],
declarations: [Cmp]
});
const engine = TestBed.get(ɵAnimationEngine);
const engine = TestBed.get(AnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
cmp.exp = 'state';
@ -174,7 +172,7 @@ export function main() {
}
TestBed.configureTestingModule({
providers: [{provide: ɵAnimationEngine, useClass: InjectableAnimationEngine}],
providers: [{provide: AnimationEngine, useClass: InjectableAnimationEngine}],
declarations: [Cmp]
});
@ -223,11 +221,11 @@ export function main() {
}
TestBed.configureTestingModule({
providers: [{provide: ɵAnimationEngine, useClass: InjectableAnimationEngine}],
providers: [{provide: AnimationEngine, useClass: InjectableAnimationEngine}],
declarations: [Cmp]
});
const engine = TestBed.get(ɵAnimationEngine);
const engine = TestBed.get(AnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
@ -239,7 +237,7 @@ export function main() {
assertHasParent(elm2);
assertHasParent(elm3);
engine.flush();
finishPlayers(engine.activePlayers);
finishPlayers(engine.players);
cmp.exp1 = false;
fixture.detectChanges();
@ -247,7 +245,7 @@ export function main() {
assertHasParent(elm2);
assertHasParent(elm3);
engine.flush();
expect(engine.activePlayers.length).toEqual(0);
expect(engine.players.length).toEqual(0);
cmp.exp2 = false;
fixture.detectChanges();
@ -255,7 +253,7 @@ export function main() {
assertHasParent(elm2, false);
assertHasParent(elm3);
engine.flush();
expect(engine.activePlayers.length).toEqual(0);
expect(engine.players.length).toEqual(0);
cmp.exp3 = false;
fixture.detectChanges();
@ -263,14 +261,57 @@ export function main() {
assertHasParent(elm2, false);
assertHasParent(elm3);
engine.flush();
expect(engine.activePlayers.length).toEqual(1);
expect(engine.players.length).toEqual(1);
});
});
});
describe('AnimationRendererFactory', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [{
provide: RendererFactory2,
useClass: ExtendedAnimationRendererFactory,
deps: [DomRendererFactory2, AnimationEngine, NgZone]
}],
imports: [BrowserAnimationsModule]
});
});
it('should provide hooks at the start and end of change detection', () => {
@Component({
selector: 'my-cmp',
template: `
<div [@myAnimation]="exp"></div>
`,
animations: [trigger('myAnimation', [])]
})
class Cmp {
public exp: any;
}
TestBed.configureTestingModule({
providers: [{provide: AnimationEngine, useClass: InjectableAnimationEngine}],
declarations: [Cmp]
});
const renderer = TestBed.get(RendererFactory2) as ExtendedAnimationRendererFactory;
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
renderer.log = [];
fixture.detectChanges();
expect(renderer.log).toEqual(['begin', 'end']);
renderer.log = [];
fixture.detectChanges();
expect(renderer.log).toEqual(['begin', 'end']);
});
});
}
@Injectable()
class MockAnimationEngine extends ɵAnimationEngine {
class MockAnimationEngine extends AnimationEngine {
captures: {[method: string]: any[]} = {};
triggers: AnimationTriggerMetadata[] = [];
@ -279,24 +320,46 @@ class MockAnimationEngine extends ɵAnimationEngine {
data.push(args);
}
registerTrigger(trigger: AnimationTriggerMetadata) { this.triggers.push(trigger); }
registerTrigger(componentId: string, namespaceId: string, trigger: AnimationTriggerMetadata) {
this.triggers.push(trigger);
}
onInsert(element: any, domFn: () => any): void { this._capture('onInsert', [element]); }
onInsert(namespaceId: string, element: any): void { this._capture('onInsert', [element]); }
onRemove(element: any, domFn: () => any): void { this._capture('onRemove', [element]); }
onRemove(namespaceId: string, element: any, domFn: () => any): void {
this._capture('onRemove', [element]);
}
setProperty(element: any, property: string, value: any): void {
setProperty(namespaceId: string, element: any, property: string, value: any): void {
this._capture('setProperty', [element, property, value]);
}
listen(element: any, eventName: string, eventPhase: string, callback: (event: any) => any):
() => void {
listen(
namespaceId: string, element: any, eventName: string, eventPhase: string,
callback: (event: any) => any): () => void {
// we don't capture the callback here since the renderer wraps it in a zone
this._capture('listen', [element, eventName, eventPhase]);
return () => {};
}
flush() {}
destroy(namespaceId: string) {}
}
@Injectable()
class ExtendedAnimationRendererFactory extends AnimationRendererFactory {
public log: string[] = [];
begin() {
super.begin();
this.log.push('begin');
}
end() {
super.end();
this.log.push('end');
}
}

View File

@ -0,0 +1,108 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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 {AnimationBuilder, animate, style} from '@angular/animations';
import {AnimationDriver} from '@angular/animations/browser';
import {MockAnimationDriver} from '@angular/animations/browser/testing';
import {Component, ViewChild} from '@angular/core';
import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
import {BrowserAnimationBuilder} from '../../animations/src/animation_builder';
import {el} from '../../testing/src/browser_util';
export function main() {
describe('BrowserAnimationBuilder', () => {
let element: any;
beforeEach(() => {
element = el('<div></div>');
TestBed.configureTestingModule({
imports: [NoopAnimationsModule],
providers: [{provide: AnimationDriver, useClass: MockAnimationDriver}]
});
});
it('should inject AnimationBuilder into a component', () => {
@Component({
selector: 'ani-cmp',
template: '...',
})
class Cmp {
constructor(public builder: AnimationBuilder) {}
}
TestBed.configureTestingModule({declarations: [Cmp]});
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
fixture.detectChanges();
expect(cmp.builder instanceof BrowserAnimationBuilder).toBeTruthy();
});
it('should listen on start and done on the animation builder\'s player', fakeAsync(() => {
@Component({
selector: 'ani-cmp',
template: '...',
})
class Cmp {
@ViewChild('target') public target: any;
constructor(public builder: AnimationBuilder) {}
build() {
const definition =
this.builder.build([style({opacity: 0}), animate(1000, style({opacity: 1}))]);
return definition.create(this.target);
}
}
TestBed.configureTestingModule({declarations: [Cmp]});
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
fixture.detectChanges();
const player = cmp.build();
let started = false;
player.onStart(() => started = true);
let finished = false;
player.onDone(() => finished = true);
let destroyed = false;
player.onDestroy(() => destroyed = true);
player.init();
flushMicrotasks();
expect(started).toBeFalsy();
expect(finished).toBeFalsy();
expect(destroyed).toBeFalsy();
player.play();
flushMicrotasks();
expect(started).toBeTruthy();
expect(finished).toBeFalsy();
expect(destroyed).toBeFalsy();
player.finish();
flushMicrotasks();
expect(started).toBeTruthy();
expect(finished).toBeTruthy();
expect(destroyed).toBeFalsy();
player.destroy();
flushMicrotasks();
expect(started).toBeTruthy();
expect(finished).toBeTruthy();
expect(destroyed).toBeTruthy();
}));
});
}

View File

@ -1,6 +1,9 @@
/** @experimental */
export declare function animate(timings: string | number, styles?: AnimationStyleMetadata | AnimationKeyframesSequenceMetadata | null): AnimationAnimateMetadata;
/** @experimental */
export declare function animateChild(options?: AnimationOptions | null): AnimationAnimateChildMetadata;
/** @experimental */
export declare type AnimateTimings = {
duration: number;
@ -8,12 +11,36 @@ export declare type AnimateTimings = {
easing: string | null;
};
/** @experimental */
export declare function animation(steps: AnimationMetadata | AnimationMetadata[], options?: AnimationOptions | null): AnimationReferenceMetadata;
/** @experimental */
export declare abstract class Animation {
abstract create(element: any, options?: AnimationOptions): AnimationPlayer;
}
/** @experimental */
export interface AnimationAnimateChildMetadata extends AnimationMetadata {
options: AnimationOptions | null;
}
/** @experimental */
export interface AnimationAnimateMetadata extends AnimationMetadata {
styles: AnimationStyleMetadata | AnimationKeyframesSequenceMetadata | null;
timings: string | number | AnimateTimings;
}
/** @experimental */
export interface AnimationAnimateRefMetadata extends AnimationMetadata {
animation: AnimationReferenceMetadata;
options: AnimationOptions | null;
}
/** @experimental */
export declare abstract class AnimationBuilder {
abstract build(animation: AnimationMetadata | AnimationMetadata[]): Animation;
}
/** @experimental */
export interface AnimationEvent {
element: any;
@ -26,6 +53,7 @@ export interface AnimationEvent {
/** @experimental */
export interface AnimationGroupMetadata extends AnimationMetadata {
options: AnimationOptions | null;
steps: AnimationMetadata[];
}
@ -46,13 +74,30 @@ export declare const enum AnimationMetadataType {
Sequence = 2,
Group = 3,
Animate = 4,
KeyframeSequence = 5,
Keyframes = 5,
Style = 6,
Trigger = 7,
Reference = 8,
AnimateChild = 9,
AnimateRef = 10,
Query = 11,
Stagger = 12,
}
/** @experimental */
export interface AnimationOptions {
delay?: number | string;
duration?: number | string;
params?: {
[name: string]: any;
};
}
/** @experimental */
export declare abstract class AnimationPlayer {
beforeDestroy?: () => any;
parentPlayer: AnimationPlayer | null;
readonly totalTime: number;
abstract destroy(): void;
abstract finish(): void;
abstract getPosition(): number;
@ -68,11 +113,37 @@ export declare abstract class AnimationPlayer {
abstract setPosition(p: any): void;
}
/** @experimental */
export interface AnimationQueryMetadata extends AnimationMetadata {
animation: AnimationMetadata | AnimationMetadata[];
options: AnimationQueryOptions | null;
selector: string;
}
/** @experimental */
export interface AnimationQueryOptions extends AnimationOptions {
limit?: number;
optional?: boolean;
}
/** @experimental */
export interface AnimationReferenceMetadata extends AnimationMetadata {
animation: AnimationMetadata | AnimationMetadata[];
options: AnimationOptions | null;
}
/** @experimental */
export interface AnimationSequenceMetadata extends AnimationMetadata {
options: AnimationOptions | null;
steps: AnimationMetadata[];
}
/** @experimental */
export interface AnimationStaggerMetadata extends AnimationMetadata {
animation: AnimationMetadata | AnimationMetadata[];
timings: string | number;
}
/** @experimental */
export interface AnimationStateMetadata extends AnimationMetadata {
name: string;
@ -81,31 +152,37 @@ export interface AnimationStateMetadata extends AnimationMetadata {
/** @experimental */
export interface AnimationStyleMetadata extends AnimationMetadata {
offset?: number;
styles: {
offset: number | null;
styles: '*' | {
[key: string]: string | number;
} | {
} | Array<{
[key: string]: string | number;
}[];
} | '*'>;
}
/** @experimental */
export interface AnimationTransitionMetadata extends AnimationMetadata {
animation: AnimationMetadata | AnimationMetadata[];
expr: string | ((fromState: string, toState: string) => boolean);
expr: string;
options: AnimationOptions | null;
}
/** @experimental */
export interface AnimationTriggerMetadata {
export interface AnimationTriggerMetadata extends AnimationMetadata {
definitions: AnimationMetadata[];
name: string;
options: {
params?: {
[name: string]: any;
};
} | null;
}
/** @experimental */
export declare const AUTO_STYLE = "*";
/** @experimental */
export declare function group(steps: AnimationMetadata[]): AnimationGroupMetadata;
export declare function group(steps: AnimationMetadata[], options?: AnimationOptions | null): AnimationGroupMetadata;
/** @experimental */
export declare function keyframes(steps: AnimationStyleMetadata[]): AnimationKeyframesSequenceMetadata;
@ -113,6 +190,7 @@ export declare function keyframes(steps: AnimationStyleMetadata[]): AnimationKey
/** @experimental */
export declare class NoopAnimationPlayer implements AnimationPlayer {
parentPlayer: AnimationPlayer | null;
totalTime: number;
constructor();
destroy(): void;
finish(): void;
@ -130,20 +208,29 @@ export declare class NoopAnimationPlayer implements AnimationPlayer {
}
/** @experimental */
export declare function sequence(steps: AnimationMetadata[]): AnimationSequenceMetadata;
export declare function query(selector: string, animation: AnimationMetadata | AnimationMetadata[], options?: AnimationQueryOptions | null): AnimationQueryMetadata;
/** @experimental */
export declare function sequence(steps: AnimationMetadata[], options?: AnimationOptions | null): AnimationSequenceMetadata;
/** @experimental */
export declare function stagger(timings: string | number, animation: AnimationMetadata | AnimationMetadata[]): AnimationStaggerMetadata;
/** @experimental */
export declare function state(name: string, styles: AnimationStyleMetadata): AnimationStateMetadata;
/** @experimental */
export declare function style(tokens: {
export declare function style(tokens: '*' | {
[key: string]: string | number;
} | Array<{
} | Array<'*' | {
[key: string]: string | number;
}>): AnimationStyleMetadata;
/** @experimental */
export declare function transition(stateChangeExpr: string | ((fromState: string, toState: string) => boolean), steps: AnimationMetadata | AnimationMetadata[]): AnimationTransitionMetadata;
export declare function transition(stateChangeExpr: string, steps: AnimationMetadata | AnimationMetadata[], options?: AnimationOptions | null): AnimationTransitionMetadata;
/** @experimental */
export declare function trigger(name: string, definitions: AnimationMetadata[]): AnimationTriggerMetadata;
/** @experimental */
export declare function useAnimation(animation: AnimationReferenceMetadata, options?: AnimationOptions | null): AnimationAnimateRefMetadata;

View File

@ -3,5 +3,6 @@ export declare abstract class AnimationDriver {
abstract animate(element: any, keyframes: {
[key: string]: string | number;
}[], duration: number, delay: number, easing?: string | null, previousPlayers?: any[]): any;
abstract computeStyle(element: any, prop: string, defaultValue?: string): string;
static NOOP: AnimationDriver;
}

View File

@ -3,11 +3,13 @@ export declare class MockAnimationDriver implements AnimationDriver {
animate(element: any, keyframes: {
[key: string]: string | number;
}[], duration: number, delay: number, easing: string, previousPlayers?: any[]): MockAnimationPlayer;
computeStyle(element: any, prop: string, defaultValue?: string): string;
static log: AnimationPlayer[];
}
/** @experimental */
export declare class MockAnimationPlayer extends NoopAnimationPlayer {
currentSnapshot: ɵStyleData;
delay: number;
duration: number;
easing: string;
@ -22,6 +24,9 @@ export declare class MockAnimationPlayer extends NoopAnimationPlayer {
constructor(element: any, keyframes: {
[key: string]: string | number;
}[], duration: number, delay: number, easing: string, previousPlayers: any[]);
beforeDestroy(): void;
destroy(): void;
finish(): void;
hasStarted(): boolean;
play(): void;
}

View File

@ -70,12 +70,12 @@ export declare type AnimationStateTransitionMetadata = any;
/** @deprecated */
export interface AnimationStyleMetadata extends AnimationMetadata {
offset?: number;
styles: {
offset: number | null;
styles: '*' | {
[key: string]: string | number;
} | {
} | Array<{
[key: string]: string | number;
}[];
} | '*'>;
}
/** @deprecated */
@ -1007,7 +1007,7 @@ export interface TrackByFunction<T> {
}
/** @deprecated */
export declare function transition(stateChangeExpr: string | ((fromState: string, toState: string) => boolean), steps: AnimationMetadata | AnimationMetadata[]): AnimationTransitionMetadata;
export declare function transition(stateChangeExpr: string, steps: AnimationMetadata | AnimationMetadata[]): AnimationTransitionMetadata;
/** @experimental */
export declare const TRANSLATIONS: InjectionToken<string>;

View File

@ -1,27 +1,7 @@
/** @experimental */
export declare class MockAnimationDriver implements AnimationDriver {
animate(element: any, keyframes: {
[key: string]: string | number;
}[], duration: number, delay: number, easing: string, previousPlayers?: any[]): MockAnimationPlayer;
static log: AnimationPlayer[];
export declare class BrowserAnimationsTestingModule {
}
/** @experimental */
export declare class MockAnimationPlayer extends NoopAnimationPlayer {
delay: number;
duration: number;
easing: string;
element: any;
keyframes: {
[key: string]: string | number;
}[];
previousPlayers: any[];
previousStyles: {
[key: string]: string | number;
};
constructor(element: any, keyframes: {
[key: string]: string | number;
}[], duration: number, delay: number, easing: string, previousPlayers: any[]);
destroy(): void;
finish(): void;
export declare class NoopAnimationsTestingModule {
}