refactor(animations): single animation engine code pass

This commit is contained in:
Matias Niemelä 2017-05-02 15:45:48 -07:00 committed by Jason Aden
parent 16c8167886
commit 8a6eb1ac78
30 changed files with 395 additions and 818 deletions

View File

@ -1,26 +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 {AnimationPlayer, AnimationTriggerMetadata} from '@angular/animations';
export abstract class AnimationEngine {
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(
namespaceId: string, element: any, eventName: string, eventPhase: string,
callback: (event: any) => any): () => any;
abstract flush(): void;
abstract destroy(namespaceId: string, context: any): void;
onRemovalComplete: (delegate: any, element: any) => void;
public players: AnimationPlayer[];
}

View File

@ -6,7 +6,10 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {AnimationMetadata, AnimationOptions, ɵStyleData} from '@angular/animations'; import {AnimationMetadata, AnimationOptions, ɵStyleData} from '@angular/animations';
import {AnimationDriver} from '../render/animation_driver';
import {normalizeStyles} from '../util'; import {normalizeStyles} from '../util';
import {Ast} from './animation_ast'; import {Ast} from './animation_ast';
import {buildAnimationAst} from './animation_ast_builder'; import {buildAnimationAst} from './animation_ast_builder';
import {buildAnimationTimelines} from './animation_timeline_builder'; import {buildAnimationTimelines} from './animation_timeline_builder';
@ -15,7 +18,7 @@ import {ElementInstructionMap} from './element_instruction_map';
export class Animation { export class Animation {
private _animationAst: Ast; private _animationAst: Ast;
constructor(input: AnimationMetadata|AnimationMetadata[]) { constructor(private _driver: AnimationDriver, input: AnimationMetadata|AnimationMetadata[]) {
const errors: any[] = []; const errors: any[] = [];
const ast = buildAnimationAst(input, errors); const ast = buildAnimationAst(input, errors);
if (errors.length) { if (errors.length) {
@ -36,7 +39,7 @@ export class Animation {
const errors: any = []; const errors: any = [];
subInstructions = subInstructions || new ElementInstructionMap(); subInstructions = subInstructions || new ElementInstructionMap();
const result = buildAnimationTimelines( const result = buildAnimationTimelines(
element, this._animationAst, start, dest, options, subInstructions, errors); this._driver, element, this._animationAst, start, dest, options, subInstructions, errors);
if (errors.length) { if (errors.length) {
const errorMessage = `animation building failed:\n${errors.join("\n")}`; const errorMessage = `animation building failed:\n${errors.join("\n")}`;
throw new Error(errorMessage); throw new Error(errorMessage);

View File

@ -5,8 +5,9 @@
* Use of this source code is governed by an MIT-style license that can be * Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {AUTO_STYLE, AnimateTimings, AnimationOptions, AnimationQueryOptions, ɵPRE_STYLE as PRE_STYLE, ɵStyleData} from '@angular/animations'; import {AUTO_STYLE, AnimateChildOptions, AnimateTimings, AnimationOptions, AnimationQueryOptions, ɵPRE_STYLE as PRE_STYLE, ɵStyleData} from '@angular/animations';
import {AnimationDriver} from '../render/animation_driver';
import {copyObj, copyStyles, interpolateParams, iteratorToArray, resolveTiming, resolveTimingValue} from '../util'; 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 {AnimateAst, AnimateChildAst, AnimateRefAst, Ast, AstVisitor, DynamicTimingAst, GroupAst, KeyframesAst, QueryAst, ReferenceAst, SequenceAst, StaggerAst, StateAst, StyleAst, TimingAst, TransitionAst, TriggerAst} from './animation_ast';
@ -100,137 +101,20 @@ const ONE_FRAME_IN_MILLISECONDS = 1;
* the `AnimationValidatorVisitor` code. * the `AnimationValidatorVisitor` code.
*/ */
export function buildAnimationTimelines( export function buildAnimationTimelines(
rootElement: any, ast: Ast, startingStyles: ɵStyleData = {}, finalStyles: ɵStyleData = {}, driver: AnimationDriver, rootElement: any, ast: Ast, startingStyles: ɵStyleData = {},
options: AnimationOptions, subInstructions?: ElementInstructionMap, finalStyles: ɵStyleData = {}, options: AnimationOptions,
errors: any[] = []): AnimationTimelineInstruction[] { subInstructions?: ElementInstructionMap, errors: any[] = []): AnimationTimelineInstruction[] {
return new AnimationTimelineBuilderVisitor().buildKeyframes( return new AnimationTimelineBuilderVisitor().buildKeyframes(
rootElement, ast, startingStyles, finalStyles, options, subInstructions, errors); driver, 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 { export class AnimationTimelineBuilderVisitor implements AstVisitor {
buildKeyframes( buildKeyframes(
rootElement: any, ast: Ast, startingStyles: ɵStyleData, finalStyles: ɵStyleData, driver: AnimationDriver, rootElement: any, ast: Ast, startingStyles: ɵStyleData,
options: AnimationOptions, subInstructions?: ElementInstructionMap, finalStyles: ɵStyleData, options: AnimationOptions, subInstructions?: ElementInstructionMap,
errors: any[] = []): AnimationTimelineInstruction[] { errors: any[] = []): AnimationTimelineInstruction[] {
subInstructions = subInstructions || new ElementInstructionMap(); subInstructions = subInstructions || new ElementInstructionMap();
const context = new AnimationTimelineContext(rootElement, subInstructions, errors, []); const context = new AnimationTimelineContext(driver, rootElement, subInstructions, errors, []);
context.options = options; context.options = options;
context.currentTimeline.setStyles([startingStyles], null, context.errors, options); context.currentTimeline.setStyles([startingStyles], null, context.errors, options);
@ -266,7 +150,8 @@ export class AnimationTimelineBuilderVisitor implements AstVisitor {
if (elementInstructions) { if (elementInstructions) {
const innerContext = context.createSubContext(ast.options); const innerContext = context.createSubContext(ast.options);
const startTime = context.currentTimeline.currentTime; const startTime = context.currentTimeline.currentTime;
const endTime = this._visitSubInstructions(elementInstructions, innerContext); const endTime = this._visitSubInstructions(
elementInstructions, innerContext, innerContext.options as AnimateChildOptions);
if (startTime != endTime) { if (startTime != endTime) {
// we do this on the upper context because we created a sub context for // we do this on the upper context because we created a sub context for
// the sub child animations // the sub child animations
@ -285,8 +170,8 @@ export class AnimationTimelineBuilderVisitor implements AstVisitor {
} }
private _visitSubInstructions( private _visitSubInstructions(
instructions: AnimationTimelineInstruction[], context: AnimationTimelineContext): number { instructions: AnimationTimelineInstruction[], context: AnimationTimelineContext,
const options = context.options; options: AnimateChildOptions): number {
const startTime = context.currentTimeline.currentTime; const startTime = context.currentTimeline.currentTime;
let furthestTime = startTime; let furthestTime = startTime;
@ -464,8 +349,8 @@ export class AnimationTimelineBuilderVisitor implements AstVisitor {
} }
let furthestTime = startTime; let furthestTime = startTime;
const elms = invokeQuery( const elms = context.invokeQuery(
context.element, ast.selector, ast.originalSelector, ast.limit, ast.includeSelf, ast.selector, ast.originalSelector, ast.limit, ast.includeSelf,
options.optional ? true : false, context.errors); options.optional ? true : false, context.errors);
context.currentQueryTotal = elms.length; context.currentQueryTotal = elms.length;
@ -540,6 +425,146 @@ export class AnimationTimelineBuilderVisitor implements AstVisitor {
} }
} }
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(
private _driver: AnimationDriver, 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(options: AnimationOptions|null, skipIfExists?: boolean) {
if (!options) return;
// NOTE: this will get patched up when other animation methods support duration overrides
const newOptions = options as any;
if (newOptions.duration != null) {
(this.options as any).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(
this._driver, 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);
}
}
invokeQuery(
selector: string, originalSelector: string, limit: number, includeSelf: boolean,
optional: boolean, errors: any[]): any[] {
let results: any[] = [];
if (includeSelf) {
results.push(this.element);
}
if (selector.length > 0) { // if :self is only used then the selector is empty
const multi = limit != 1;
results.push(...this._driver.query(this.element, selector, multi));
}
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;
}
}
export class TimelineBuilder { export class TimelineBuilder {
public duration: number = 0; public duration: number = 0;
public easing: string|null; public easing: string|null;
@ -824,35 +849,6 @@ class SubTimelineBuilder extends TimelineBuilder {
} }
} }
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 { function roundOffset(offset: number, decimalPoints = 3): number {
const mult = Math.pow(10, decimalPoints - 1); const mult = Math.pow(10, decimalPoints - 1);
return Math.round(offset * mult) / mult; return Math.round(offset * mult) / mult;

View File

@ -7,6 +7,7 @@
*/ */
import {AnimationOptions, ɵStyleData} from '@angular/animations'; import {AnimationOptions, ɵStyleData} from '@angular/animations';
import {AnimationDriver} from '../render/animation_driver';
import {getOrSetAsInMap} from '../render/shared'; import {getOrSetAsInMap} from '../render/shared';
import {iteratorToArray, mergeAnimationOptions} from '../util'; import {iteratorToArray, mergeAnimationOptions} from '../util';
@ -26,7 +27,8 @@ export class AnimationTransitionFactory {
} }
build( build(
element: any, currentState: any, nextState: any, options?: AnimationOptions, driver: AnimationDriver, element: any, currentState: any, nextState: any,
options?: AnimationOptions,
subInstructions?: ElementInstructionMap): AnimationTransitionInstruction|undefined { subInstructions?: ElementInstructionMap): AnimationTransitionInstruction|undefined {
const animationOptions = mergeAnimationOptions(this.ast.options || {}, options || {}); const animationOptions = mergeAnimationOptions(this.ast.options || {}, options || {});
@ -36,7 +38,7 @@ export class AnimationTransitionFactory {
const errors: any[] = []; const errors: any[] = [];
const timelines = buildAnimationTimelines( const timelines = buildAnimationTimelines(
element, this.ast.animation, currentStateStyles, nextStateStyles, animationOptions, driver, element, this.ast.animation, currentStateStyles, nextStateStyles, animationOptions,
subInstructions, errors); subInstructions, errors);
if (errors.length) { if (errors.length) {

View File

@ -5,12 +5,10 @@
* Use of this source code is governed by an MIT-style license that can be * Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
export {AnimationEngine as ɵAnimationEngine} from './animation_engine';
export {Animation as ɵAnimation} from './dsl/animation'; export {Animation as ɵAnimation} from './dsl/animation';
export {AnimationStyleNormalizer as ɵAnimationStyleNormalizer, NoopAnimationStyleNormalizer as ɵNoopAnimationStyleNormalizer} from './dsl/style_normalization/animation_style_normalizer'; 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 {WebAnimationsStyleNormalizer as ɵWebAnimationsStyleNormalizer} from './dsl/style_normalization/web_animations_style_normalizer';
export {NoopAnimationDriver as ɵNoopAnimationDriver} from './render/animation_driver'; export {NoopAnimationDriver as ɵNoopAnimationDriver} from './render/animation_driver';
export {DomAnimationEngine as ɵDomAnimationEngine} from './render/dom_animation_engine_next'; export {AnimationEngine as ɵAnimationEngine} from './render/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 {WebAnimationsDriver as ɵWebAnimationsDriver, supportsWebAnimations as ɵsupportsWebAnimations} from './render/web_animations/web_animations_driver';
export {WebAnimationsPlayer as ɵWebAnimationsPlayer} from './render/web_animations/web_animations_player'; export {WebAnimationsPlayer as ɵWebAnimationsPlayer} from './render/web_animations/web_animations_player';

View File

@ -5,16 +5,25 @@
* Use of this source code is governed by an MIT-style license that can be * Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {ɵStyleData} from '@angular/animations';
import {AnimationPlayer, NoopAnimationPlayer} from '@angular/animations'; import {AnimationPlayer, NoopAnimationPlayer} from '@angular/animations';
import {containsElement, invokeQuery, matchesElement} from './shared';
/** /**
* @experimental * @experimental
*/ */
export class NoopAnimationDriver implements AnimationDriver { export class NoopAnimationDriver implements AnimationDriver {
matchesElement(element: any, selector: string): boolean {
return matchesElement(element, selector);
}
containsElement(elm1: any, elm2: any): boolean { return containsElement(elm1, elm2); }
query(element: any, selector: string, multi: boolean): any[] {
return invokeQuery(element, selector, multi);
}
computeStyle(element: any, prop: string, defaultValue?: string): string { computeStyle(element: any, prop: string, defaultValue?: string): string {
return defaultValue || ''; return defaultValue || '';
} }
@ -32,6 +41,12 @@ export class NoopAnimationDriver implements AnimationDriver {
export abstract class AnimationDriver { export abstract class AnimationDriver {
static NOOP: AnimationDriver = new NoopAnimationDriver(); static NOOP: AnimationDriver = new NoopAnimationDriver();
abstract matchesElement(element: any, selector: string): boolean;
abstract containsElement(elm1: any, elm2: any): boolean;
abstract query(element: any, selector: string, multi: boolean): any[];
abstract computeStyle(element: any, prop: string, defaultValue?: string): string; abstract computeStyle(element: any, prop: string, defaultValue?: string): string;
abstract animate( abstract animate(

View File

@ -6,8 +6,6 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {AnimationMetadata, AnimationPlayer, AnimationTriggerMetadata} from '@angular/animations'; import {AnimationMetadata, AnimationPlayer, AnimationTriggerMetadata} from '@angular/animations';
import {AnimationEngine} from '../animation_engine';
import {TriggerAst} from '../dsl/animation_ast'; import {TriggerAst} from '../dsl/animation_ast';
import {buildAnimationAst} from '../dsl/animation_ast_builder'; import {buildAnimationAst} from '../dsl/animation_ast_builder';
import {AnimationTrigger, buildTrigger} from '../dsl/animation_trigger'; import {AnimationTrigger, buildTrigger} from '../dsl/animation_trigger';
@ -18,7 +16,7 @@ import {parseTimelineCommand} from './shared';
import {TimelineAnimationEngine} from './timeline_animation_engine'; import {TimelineAnimationEngine} from './timeline_animation_engine';
import {TransitionAnimationEngine} from './transition_animation_engine'; import {TransitionAnimationEngine} from './transition_animation_engine';
export class DomAnimationEngine implements AnimationEngine { export class AnimationEngine {
private _transitionEngine: TransitionAnimationEngine; private _transitionEngine: TransitionAnimationEngine;
private _timelineEngine: TimelineAnimationEngine; private _timelineEngine: TimelineAnimationEngine;

View File

@ -1,214 +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, ɵ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;
}
const DEFAULT_STATE_VALUE = 'void';
const DEFAULT_STATE_STYLES = '*';
export class NoopAnimationEngine extends AnimationEngine {
private _listeners = new Map<any, ListenerTuple[]>();
private _changes: ChangeTuple[] = [];
private _flaggedRemovals = new Set<any>();
private _onDoneFns: (() => any)[] = [];
private _triggerStyles: {[triggerName: string]: {[stateName: string]: ɵStyleData}} =
Object.create(null);
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 errors: any[] = [];
const ast = buildAnimationAst(metadata, errors) as TriggerAst;
const trigger = buildTrigger(name, ast);
this._triggerStyles[name] = trigger.states;
}
onInsert(namespaceId: string, element: any, parent: any, insertBefore: boolean): void {}
onRemove(namespaceId: string, element: any, context: any): void {
this.onRemovalComplete(element, context);
if (element['nodeType'] == 1) {
this._flaggedRemovals.add(element);
}
}
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 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) {
eraseStyles(element, fromStateStyles);
}
element[storageProp] = value;
this._onDoneFns.push(() => {
const toStateStyles = triggerStateStyles[value] || triggerStateStyles[DEFAULT_STATE_STYLES];
if (toStateStyles) {
setStyles(element, toStateStyles);
}
});
return true;
}
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>{
namespacedName: namespaceId + '#' + eventName,
triggerName: eventName, eventPhase, callback
};
listeners.push(tuple);
return () => tuple.doRemove = true;
}
flush(): void {
const onStartCallbacks: (() => any)[] = [];
const onDoneCallbacks: (() => any)[] = [];
function handleListener(listener: ListenerTuple, data: ChangeTuple) {
const phase = listener.eventPhase;
const event = makeAnimationEvent(
data.element, data.triggerName, data.oldValue, data.newValue, phase, 0);
if (phase == 'start') {
onStartCallbacks.push(() => listener.callback(event));
} else if (phase == 'done') {
onDoneCallbacks.push(() => listener.callback(event));
}
}
this._changes.forEach(change => {
const element = change.element;
const listeners = this._listeners.get(element);
if (listeners) {
listeners.forEach(listener => {
if (listener.namespacedName == change.namespacedName) {
handleListener(listener, change);
}
});
}
});
// upon removal ALL the animation triggers need to get fired
this._flaggedRemovals.forEach(element => {
const listeners = this._listeners.get(element);
if (listeners) {
listeners.forEach(listener => {
const triggerName = listener.triggerName;
const namespacedName = listener.namespacedName;
const storageProp = makeStorageProp(namespacedName);
handleListener(listener, <ChangeTuple>{
element,
triggerName,
namespacedName: listener.namespacedName,
oldValue: element[storageProp] || DEFAULT_STATE_VALUE,
newValue: DEFAULT_STATE_VALUE
});
});
}
});
// remove all the listeners after everything is complete
Array.from(this._listeners.keys()).forEach(element => {
const listenersToKeep = this._listeners.get(element) !.filter(l => !l.doRemove);
if (listenersToKeep.length) {
this._listeners.set(element, listenersToKeep);
} else {
this._listeners.delete(element);
}
});
onStartCallbacks.forEach(fn => fn());
onDoneCallbacks.forEach(fn => fn());
this._flaggedRemovals.clear();
this._changes = [];
this._onDoneFns.forEach(doneFn => doneFn());
this._onDoneFns = [];
}
get players(): AnimationPlayer[] { return []; }
destroy(namespaceId: string) {}
}
function makeAnimationEvent(
element: any, triggerName: string, fromState: string, toState: string, phaseName: string,
totalTime: number): AnimationEvent {
return <AnimationEvent>{element, triggerName, fromState, toState, phaseName, totalTime};
}
function makeStorageProp(property: string): string {
return '_@_' + property;
}

View File

@ -114,3 +114,44 @@ export function parseTimelineCommand(command: string): [string, string] {
const action = command.substr(separatorPos + 1); const action = command.substr(separatorPos + 1);
return [id, action]; return [id, action];
} }
let _contains: (elm1: any, elm2: any) => boolean = (elm1: any, elm2: any) => false;
let _matches: (element: any, selector: string) => boolean = (element: any, selector: string) =>
false;
let _query: (element: any, selector: string, multi: boolean) => any[] =
(element: any, selector: string, multi: boolean) => {
return [];
};
if (typeof Element != 'undefined') {
// this is well supported in all browsers
_contains = (elm1: any, elm2: any) => { return elm1.contains(elm2) as boolean; };
if (Element.prototype.matches) {
_matches = (element: any, selector: string) => element.matches(selector);
} else {
const proto = Element.prototype as any;
const fn = proto.matchesSelector || proto.mozMatchesSelector || proto.msMatchesSelector ||
proto.oMatchesSelector || proto.webkitMatchesSelector;
if (fn) {
_matches = (element: any, selector: string) => fn.apply(element, [selector]);
}
}
_query = (element: any, selector: string, multi: boolean): any[] => {
let results: any[] = [];
if (multi) {
results.push(...element.querySelectorAll(selector));
} else {
const elm = element.querySelector(selector);
if (elm) {
results.push(elm);
}
}
return results;
};
}
export const matchesElement = _matches;
export const containsElement = _contains;
export const invokeQuery = _query;

View File

@ -54,8 +54,8 @@ export class TimelineAnimationEngine {
const autoStylesMap = new Map<any, ɵStyleData>(); const autoStylesMap = new Map<any, ɵStyleData>();
if (ast) { if (ast) {
instructions = instructions = buildAnimationTimelines(
buildAnimationTimelines(element, ast, {}, {}, options, EMPTY_INSTRUCTION_MAP, errors); this._driver, element, ast, {}, {}, options, EMPTY_INSTRUCTION_MAP, errors);
instructions.forEach(inst => { instructions.forEach(inst => {
const styles = getOrSetAsInMap(autoStylesMap, inst.element, {}); const styles = getOrSetAsInMap(autoStylesMap, inst.element, {});
inst.postStyleProps.forEach(prop => styles[prop] = null); inst.postStyleProps.forEach(prop => styles[prop] = null);

View File

@ -89,7 +89,7 @@ export class AnimationTransitionNamespace {
constructor( constructor(
public id: string, public hostElement: any, private _engine: TransitionAnimationEngine) { public id: string, public hostElement: any, private _engine: TransitionAnimationEngine) {
this._hostClassName = 'ng-tns-' + id; this._hostClassName = 'ng-tns-' + id;
hostElement.classList.add(this._hostClassName); addClass(hostElement, this._hostClassName);
} }
listen(element: any, name: string, phase: string, callback: (event: any) => boolean): () => any { listen(element: any, name: string, phase: string, callback: (event: any) => boolean): () => any {
@ -114,8 +114,8 @@ export class AnimationTransitionNamespace {
const triggersWithStates = getOrSetAsInMap(this._engine.statesByElement, element, {}); const triggersWithStates = getOrSetAsInMap(this._engine.statesByElement, element, {});
if (!triggersWithStates.hasOwnProperty(name)) { if (!triggersWithStates.hasOwnProperty(name)) {
element.classList.add(NG_TRIGGER_CLASSNAME); addClass(element, NG_TRIGGER_CLASSNAME);
element.classList.add(NG_TRIGGER_CLASSNAME + '-' + name); addClass(element, NG_TRIGGER_CLASSNAME + '-' + name);
triggersWithStates[name] = null; triggersWithStates[name] = null;
} }
@ -161,8 +161,8 @@ export class AnimationTransitionNamespace {
let triggersWithStates = this._engine.statesByElement.get(element); let triggersWithStates = this._engine.statesByElement.get(element);
if (!triggersWithStates) { if (!triggersWithStates) {
element.classList.add(NG_TRIGGER_CLASSNAME); addClass(element, NG_TRIGGER_CLASSNAME);
element.classList.add(NG_TRIGGER_CLASSNAME + '-' + triggerName); addClass(element, NG_TRIGGER_CLASSNAME + '-' + triggerName);
this._engine.statesByElement.set(element, triggersWithStates = {}); this._engine.statesByElement.set(element, triggersWithStates = {});
} }
@ -207,11 +207,11 @@ export class AnimationTransitionNamespace {
{element, triggerName, transition, fromState, toState, player, isFallbackTransition}); {element, triggerName, transition, fromState, toState, player, isFallbackTransition});
if (!isFallbackTransition) { if (!isFallbackTransition) {
element.classList.add(NG_ANIMATING_CLASSNAME); addClass(element, NG_ANIMATING_CLASSNAME);
} }
player.onDone(() => { player.onDone(() => {
element.classList.remove(NG_ANIMATING_CLASSNAME); removeClass(element, NG_ANIMATING_CLASSNAME);
let index = this.players.indexOf(player); let index = this.players.indexOf(player);
if (index >= 0) { if (index >= 0) {
@ -255,8 +255,8 @@ export class AnimationTransitionNamespace {
} }
private _destroyInnerNodes(rootElement: any, context: any, animate: boolean = false) { private _destroyInnerNodes(rootElement: any, context: any, animate: boolean = false) {
listToArray(rootElement.querySelectorAll(NG_TRIGGER_SELECTOR)).forEach(elm => { listToArray(this._engine.driver.query(rootElement, NG_TRIGGER_SELECTOR, true)).forEach(elm => {
if (animate && elm.classList.contains(this._hostClassName)) { if (animate && containsClass(elm, this._hostClassName)) {
const innerNs = this._engine.namespacesByHostElement.get(elm); const innerNs = this._engine.namespacesByHostElement.get(elm);
// special case for a host element with animations on the same element // special case for a host element with animations on the same element
@ -274,8 +274,8 @@ export class AnimationTransitionNamespace {
removeNode(element: any, context: any, doNotRecurse?: boolean): void { removeNode(element: any, context: any, doNotRecurse?: boolean): void {
const engine = this._engine; const engine = this._engine;
element.classList.add(LEAVE_CLASSNAME); addClass(element, LEAVE_CLASSNAME);
engine.afterFlush(() => element.classList.remove(LEAVE_CLASSNAME)); engine.afterFlush(() => removeClass(element, LEAVE_CLASSNAME));
if (!doNotRecurse && element.childElementCount) { if (!doNotRecurse && element.childElementCount) {
this._destroyInnerNodes(element, context, true); this._destroyInnerNodes(element, context, true);
@ -380,7 +380,7 @@ export class AnimationTransitionNamespace {
} }
} }
insertNode(element: any, parent: any): void { element.classList.add(this._hostClassName); } insertNode(element: any, parent: any): void { addClass(element, this._hostClassName); }
drainQueuedTransitions(): QueueInstruction[] { drainQueuedTransitions(): QueueInstruction[] {
const instructions: QueueInstruction[] = []; const instructions: QueueInstruction[] = [];
@ -415,13 +415,13 @@ export class AnimationTransitionNamespace {
return instructions.sort((a, b) => { return instructions.sort((a, b) => {
// if depCount == 0 them move to front // if depCount == 0 them move to front
// otherwise if a.contains(b) then move back // otherwise if a contains b then move back
const d0 = a.transition.ast.depCount; const d0 = a.transition.ast.depCount;
const d1 = b.transition.ast.depCount; const d1 = b.transition.ast.depCount;
if (d0 == 0 || d1 == 0) { if (d0 == 0 || d1 == 0) {
return d0 - d1; return d0 - d1;
} }
return a.element.contains(b.element) ? 1 : -1; return this._engine.driver.containsElement(a.element, b.element) ? 1 : -1;
}); });
} }
@ -468,7 +468,7 @@ export class TransitionAnimationEngine {
_onRemovalComplete(element: any, context: any) { this.onRemovalComplete(element, context); } _onRemovalComplete(element: any, context: any) { this.onRemovalComplete(element, context); }
constructor(private _driver: AnimationDriver, private _normalizer: AnimationStyleNormalizer) {} constructor(public driver: AnimationDriver, private _normalizer: AnimationStyleNormalizer) {}
get queuedPlayers(): TransitionAnimationPlayer[] { get queuedPlayers(): TransitionAnimationPlayer[] {
const players: TransitionAnimationPlayer[] = []; const players: TransitionAnimationPlayer[] = [];
@ -508,7 +508,7 @@ export class TransitionAnimationEngine {
let found = false; let found = false;
for (let i = limit; i >= 0; i--) { for (let i = limit; i >= 0; i--) {
const nextNamespace = this._namespaceList[i]; const nextNamespace = this._namespaceList[i];
if (nextNamespace.hostElement.contains(hostElement)) { if (this.driver.containsElement(nextNamespace.hostElement, hostElement)) {
this._namespaceList.splice(i + 1, 0, ns); this._namespaceList.splice(i + 1, 0, ns);
found = true; found = true;
break; break;
@ -591,12 +591,12 @@ export class TransitionAnimationEngine {
private _buildInstruction(entry: QueueInstruction, subTimelines: ElementInstructionMap) { private _buildInstruction(entry: QueueInstruction, subTimelines: ElementInstructionMap) {
return entry.transition.build( return entry.transition.build(
entry.element, entry.fromState.value, entry.toState.value, entry.toState.options, this.driver, entry.element, entry.fromState.value, entry.toState.value,
subTimelines); entry.toState.options, subTimelines);
} }
destroyInnerAnimations(containerElement: any) { destroyInnerAnimations(containerElement: any) {
listToArray(containerElement.querySelectorAll(NG_TRIGGER_SELECTOR)).forEach(element => { listToArray(this.driver.query(containerElement, NG_TRIGGER_SELECTOR, true)).forEach(element => {
const players = this.playersByElement.get(element); const players = this.playersByElement.get(element);
if (players) { if (players) {
players.forEach(player => { players.forEach(player => {
@ -662,14 +662,15 @@ export class TransitionAnimationEngine {
// the :enter queries match the elements (since the timeline queries // the :enter queries match the elements (since the timeline queries
// are fired during instruction building). // are fired during instruction building).
const allEnterNodes = iteratorToArray(this.newlyInserted.values()); const allEnterNodes = iteratorToArray(this.newlyInserted.values());
const enterNodes: any[] = collectEnterElements(allEnterNodes); const enterNodes: any[] = collectEnterElements(this.driver, allEnterNodes);
const bodyNode = getBodyNode();
for (let i = this._namespaceList.length - 1; i >= 0; i--) { for (let i = this._namespaceList.length - 1; i >= 0; i--) {
const ns = this._namespaceList[i]; const ns = this._namespaceList[i];
ns.drainQueuedTransitions().forEach(entry => { ns.drainQueuedTransitions().forEach(entry => {
const player = entry.player; const player = entry.player;
const element = entry.element; const element = entry.element;
if (!document.body.contains(element)) { if (!bodyNode || !this.driver.containsElement(bodyNode, element)) {
player.destroy(); player.destroy();
return; return;
} }
@ -737,18 +738,18 @@ export class TransitionAnimationEngine {
allPreviousPlayersMap.forEach(players => { players.forEach(player => player.destroy()); }); allPreviousPlayersMap.forEach(players => { players.forEach(player => player.destroy()); });
const leaveNodes: any[] = allPostStyleElements.size ? const leaveNodes: any[] = bodyNode && allPostStyleElements.size ?
listToArray(document.body.querySelectorAll(LEAVE_SELECTOR)) : listToArray(this.driver.query(bodyNode, LEAVE_SELECTOR, true)) :
[]; [];
// PRE STAGE: fill the ! styles // PRE STAGE: fill the ! styles
const preStylesMap = allPreStyleElements.size ? const preStylesMap = allPreStyleElements.size ?
cloakAndComputeStyles(this._driver, enterNodes, allPreStyleElements, PRE_STYLE) : cloakAndComputeStyles(this.driver, enterNodes, allPreStyleElements, PRE_STYLE) :
new Map<any, ɵStyleData>(); new Map<any, ɵStyleData>();
// POST STAGE: fill the * styles // POST STAGE: fill the * styles
const postStylesMap = const postStylesMap =
cloakAndComputeStyles(this._driver, leaveNodes, allPostStyleElements, AUTO_STYLE); cloakAndComputeStyles(this.driver, leaveNodes, allPostStyleElements, AUTO_STYLE);
const rootPlayers: TransitionAnimationPlayer[] = []; const rootPlayers: TransitionAnimationPlayer[] = [];
const subPlayers: TransitionAnimationPlayer[] = []; const subPlayers: TransitionAnimationPlayer[] = [];
@ -766,7 +767,7 @@ export class TransitionAnimationEngine {
for (let i = 0; i < sortedParentElements.length; i++) { for (let i = 0; i < sortedParentElements.length; i++) {
const parent = sortedParentElements[i]; const parent = sortedParentElements[i];
if (parent === element) break; if (parent === element) break;
if (parent.contains(element)) { if (this.driver.containsElement(parent, element)) {
parentHasPriority = parent; parentHasPriority = parent;
break; break;
} }
@ -845,7 +846,7 @@ export class TransitionAnimationEngine {
player.play(); player.play();
}); });
enterNodes.forEach(element => element.classList.remove(ENTER_CLASSNAME)); enterNodes.forEach(element => removeClass(element, ENTER_CLASSNAME));
return rootPlayers; return rootPlayers;
} }
@ -958,7 +959,7 @@ export class TransitionAnimationEngine {
const preStyles = preStylesMap.get(element); const preStyles = preStylesMap.get(element);
const postStyles = postStylesMap.get(element); const postStyles = postStylesMap.get(element);
const keyframes = normalizeKeyframes( const keyframes = normalizeKeyframes(
this._driver, this._normalizer, element, timelineInstruction.keyframes, preStyles, this.driver, this._normalizer, element, timelineInstruction.keyframes, preStyles,
postStyles); postStyles);
const player = this._buildPlayer(timelineInstruction, keyframes, previousPlayers); const player = this._buildPlayer(timelineInstruction, keyframes, previousPlayers);
@ -983,11 +984,11 @@ export class TransitionAnimationEngine {
() => { deleteOrUnsetInMap(this.playersByQueriedElement, player.element, player); }); () => { deleteOrUnsetInMap(this.playersByQueriedElement, player.element, player); });
}); });
allConsumedElements.forEach(element => { element.classList.add(NG_ANIMATING_CLASSNAME); }); allConsumedElements.forEach(element => addClass(element, NG_ANIMATING_CLASSNAME));
const player = optimizeGroupPlayer(allNewPlayers); const player = optimizeGroupPlayer(allNewPlayers);
player.onDone(() => { player.onDone(() => {
allConsumedElements.forEach(element => { element.classList.remove(NG_ANIMATING_CLASSNAME); }); allConsumedElements.forEach(element => removeClass(element, NG_ANIMATING_CLASSNAME));
setStyles(rootElement, instruction.toStyles); setStyles(rootElement, instruction.toStyles);
}); });
@ -1003,7 +1004,7 @@ export class TransitionAnimationEngine {
instruction: AnimationTimelineInstruction, keyframes: ɵStyleData[], instruction: AnimationTimelineInstruction, keyframes: ɵStyleData[],
previousPlayers: AnimationPlayer[]): AnimationPlayer { previousPlayers: AnimationPlayer[]): AnimationPlayer {
if (keyframes.length > 0) { if (keyframes.length > 0) {
return this._driver.animate( return this.driver.animate(
instruction.element, keyframes, instruction.duration, instruction.delay, instruction.element, keyframes, instruction.duration, instruction.delay,
instruction.easing, previousPlayers); instruction.easing, previousPlayers);
} }
@ -1150,31 +1151,21 @@ function cloakElement(element: any, value?: string) {
return oldValue; return oldValue;
} }
let elementMatches: (element: any, selector: string) => boolean = function filterNodeClasses(
(element: any, selector: string) => false; driver: AnimationDriver, rootElement: any | null, selector: string): any[] {
if (typeof Element == 'function') {
if (Element.prototype.matches) {
elementMatches = (element: any, selector: string) => element.matches(selector);
} else {
const proto = Element.prototype as any;
const fn = proto.matchesSelector || proto.mozMatchesSelector || proto.msMatchesSelector ||
proto.oMatchesSelector || proto.webkitMatchesSelector;
elementMatches = (element: any, selector: string) => fn.apply(element, [selector]);
}
}
function filterNodeClasses(rootElement: any, selector: string): any[] {
const rootElements: any[] = []; const rootElements: any[] = [];
if (!rootElement) return rootElements;
let cursor: any = rootElement; let cursor: any = rootElement;
let nextCursor: any = {}; let nextCursor: any = {};
do { do {
nextCursor = cursor.querySelector(selector); nextCursor = driver.query(cursor, selector, false)[0];
if (!nextCursor) { if (!nextCursor) {
cursor = cursor.parentElement; cursor = cursor.parentElement;
if (!cursor) break; if (!cursor) break;
nextCursor = cursor = cursor.nextElementSibling; nextCursor = cursor = cursor.nextElementSibling;
} else { } else {
while (nextCursor && elementMatches(nextCursor, selector)) { while (nextCursor && driver.matchesElement(nextCursor, selector)) {
rootElements.push(nextCursor); rootElements.push(nextCursor);
nextCursor = nextCursor.nextElementSibling; nextCursor = nextCursor.nextElementSibling;
if (nextCursor) { if (nextCursor) {
@ -1221,10 +1212,50 @@ function listToArray(list: any): any[] {
return arr; return arr;
} }
function collectEnterElements(allEnterNodes: any[]) { function collectEnterElements(driver: AnimationDriver, allEnterNodes: any[]) {
allEnterNodes.forEach(element => element.classList.add(POTENTIAL_ENTER_CLASSNAME)); allEnterNodes.forEach(element => addClass(element, POTENTIAL_ENTER_CLASSNAME));
const enterNodes = filterNodeClasses(document.body, POTENTIAL_ENTER_SELECTOR); const enterNodes = filterNodeClasses(driver, getBodyNode(), POTENTIAL_ENTER_SELECTOR);
enterNodes.forEach(element => element.classList.add(ENTER_CLASSNAME)); enterNodes.forEach(element => addClass(element, ENTER_CLASSNAME));
allEnterNodes.forEach(element => element.classList.remove(POTENTIAL_ENTER_CLASSNAME)); allEnterNodes.forEach(element => removeClass(element, POTENTIAL_ENTER_CLASSNAME));
return enterNodes; return enterNodes;
} }
const CLASSES_CACHE_KEY = '$$classes';
function containsClass(element: any, className: string): boolean {
if (element.classList) {
return element.classList.contains(className);
} else {
const classes = element[CLASSES_CACHE_KEY];
return classes && classes[className];
}
}
function addClass(element: any, className: string) {
if (element.classList) {
element.classList.add(className);
} else {
let classes: {[className: string]: boolean} = element[CLASSES_CACHE_KEY];
if (!classes) {
classes = element[CLASSES_CACHE_KEY] = {};
}
classes[className] = true;
}
}
function removeClass(element: any, className: string) {
if (element.classList) {
element.classList.remove(className);
} else {
let classes: {[className: string]: boolean} = element[CLASSES_CACHE_KEY];
if (classes) {
delete classes[className];
}
}
}
function getBodyNode(): any|null {
if (typeof document != 'undefined') {
return document.body;
}
return null;
}

View File

@ -8,10 +8,21 @@
import {AnimationPlayer, ɵStyleData} from '@angular/animations'; import {AnimationPlayer, ɵStyleData} from '@angular/animations';
import {AnimationDriver} from '../animation_driver'; import {AnimationDriver} from '../animation_driver';
import {containsElement, invokeQuery, matchesElement} from '../shared';
import {WebAnimationsPlayer} from './web_animations_player'; import {WebAnimationsPlayer} from './web_animations_player';
export class WebAnimationsDriver implements AnimationDriver { export class WebAnimationsDriver implements AnimationDriver {
matchesElement(element: any, selector: string): boolean {
return matchesElement(element, selector);
}
containsElement(elm1: any, elm2: any): boolean { return containsElement(elm1, elm2); }
query(element: any, selector: string, multi: boolean): any[] {
return invokeQuery(element, selector, multi);
}
computeStyle(element: any, prop: string, defaultValue?: string): string { computeStyle(element: any, prop: string, defaultValue?: string): string {
return (window.getComputedStyle(element) as any)[prop] as string; return (window.getComputedStyle(element) as any)[prop] as string;
} }

View File

@ -12,6 +12,7 @@ import {Animation} from '../../src/dsl/animation';
import {buildAnimationAst} from '../../src/dsl/animation_ast_builder'; import {buildAnimationAst} from '../../src/dsl/animation_ast_builder';
import {AnimationTimelineInstruction} from '../../src/dsl/animation_timeline_instruction'; import {AnimationTimelineInstruction} from '../../src/dsl/animation_timeline_instruction';
import {ElementInstructionMap} from '../../src/dsl/element_instruction_map'; import {ElementInstructionMap} from '../../src/dsl/element_instruction_map';
import {MockAnimationDriver} from '../../testing';
function createDiv() { function createDiv() {
return document.createElement('div'); return document.createElement('div');
@ -868,8 +869,9 @@ function invokeAnimationSequence(
element: any, steps: AnimationMetadata | AnimationMetadata[], locals: {[key: string]: any} = {}, element: any, steps: AnimationMetadata | AnimationMetadata[], locals: {[key: string]: any} = {},
startingStyles: ɵStyleData[] = [], destinationStyles: ɵStyleData[] = [], startingStyles: ɵStyleData[] = [], destinationStyles: ɵStyleData[] = [],
subInstructions?: ElementInstructionMap): AnimationTimelineInstruction[] { subInstructions?: ElementInstructionMap): AnimationTimelineInstruction[] {
return new Animation(steps).buildTimelines( const driver = new MockAnimationDriver();
element, startingStyles, destinationStyles, locals, subInstructions); return new Animation(driver, steps)
.buildTimelines(element, startingStyles, destinationStyles, locals, subInstructions);
} }
function validateAndThrowAnimationSequence(steps: AnimationMetadata | AnimationMetadata[]) { function validateAndThrowAnimationSequence(steps: AnimationMetadata | AnimationMetadata[]) {

View File

@ -10,6 +10,7 @@ import {AnimationOptions, animate, state, style, transition} from '@angular/anim
import {AnimationTransitionInstruction} from '@angular/animations/browser/src/dsl/animation_transition_instruction'; import {AnimationTransitionInstruction} from '@angular/animations/browser/src/dsl/animation_transition_instruction';
import {AnimationTrigger} from '@angular/animations/browser/src/dsl/animation_trigger'; import {AnimationTrigger} from '@angular/animations/browser/src/dsl/animation_trigger';
import {MockAnimationDriver} from '../../testing';
import {makeTrigger} from '../shared'; import {makeTrigger} from '../shared';
export function main() { export function main() {
@ -221,7 +222,8 @@ function buildTransition(
params?: AnimationOptions): AnimationTransitionInstruction|null { params?: AnimationOptions): AnimationTransitionInstruction|null {
const trans = trigger.matchTransition(fromState, toState) !; const trans = trigger.matchTransition(fromState, toState) !;
if (trans) { if (trans) {
return trans.build(element, fromState, toState, params) !; const driver = new MockAnimationDriver();
return trans.build(driver, element, fromState, toState, params) !;
} }
return null; return null;
} }

View File

@ -52,7 +52,6 @@ export function main() {
describe('property setting', () => { describe('property setting', () => {
it('should invoke a transition based on a property change', () => { it('should invoke a transition based on a property change', () => {
const engine = makeEngine(); const engine = makeEngine();
const trig = trigger('myTrigger', [ const trig = trigger('myTrigger', [
transition('* => *', [style({height: '0px'}), animate(1000, style({height: '100px'}))]) transition('* => *', [style({height: '0px'}), animate(1000, style({height: '100px'}))])
]); ]);

View File

@ -8,6 +8,8 @@
import {AUTO_STYLE, AnimationPlayer, NoopAnimationPlayer, ɵStyleData} from '@angular/animations'; import {AUTO_STYLE, AnimationPlayer, NoopAnimationPlayer, ɵStyleData} from '@angular/animations';
import {AnimationDriver} from '../../src/render/animation_driver'; import {AnimationDriver} from '../../src/render/animation_driver';
import {containsElement, invokeQuery, matchesElement} from '../../src/render/shared';
/** /**
* @experimental Animation support is experimental. * @experimental Animation support is experimental.
@ -15,6 +17,16 @@ import {AnimationDriver} from '../../src/render/animation_driver';
export class MockAnimationDriver implements AnimationDriver { export class MockAnimationDriver implements AnimationDriver {
static log: AnimationPlayer[] = []; static log: AnimationPlayer[] = [];
matchesElement(element: any, selector: string): boolean {
return matchesElement(element, selector);
}
containsElement(elm1: any, elm2: any): boolean { return containsElement(elm1, elm2); }
query(element: any, selector: string, multi: boolean): any[] {
return invokeQuery(element, selector, multi);
}
computeStyle(element: any, prop: string, defaultValue?: string): string { computeStyle(element: any, prop: string, defaultValue?: string): string {
return defaultValue || ''; return defaultValue || '';
} }

View File

@ -21,10 +21,14 @@ export declare type AnimateTimings = {
*/ */
export declare interface AnimationOptions { export declare interface AnimationOptions {
delay?: number|string; delay?: number|string;
duration?: number|string;
params?: {[name: string]: any}; params?: {[name: string]: any};
} }
/**
* @experimental Animation support is experimental.
*/
export declare interface AnimateChildOptions extends AnimationOptions { duration?: number|string; }
/** /**
* @experimental Animation support is experimental. * @experimental Animation support is experimental.
*/ */
@ -635,7 +639,7 @@ export function animation(
/** /**
* @experimental Animation support is experimental. * @experimental Animation support is experimental.
*/ */
export function animateChild(options: AnimationOptions | null = null): export function animateChild(options: AnimateChildOptions | null = null):
AnimationAnimateChildMetadata { AnimationAnimateChildMetadata {
return {type: AnimationMetadataType.AnimateChild, options}; return {type: AnimationMetadataType.AnimateChild, options};
} }

View File

@ -13,7 +13,7 @@
*/ */
export {Animation, AnimationBuilder} from './animation_builder'; export {Animation, AnimationBuilder} from './animation_builder';
export {AnimationEvent} from './animation_event'; export {AnimationEvent} from './animation_event';
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 {AUTO_STYLE, AnimateChildOptions, 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 {AnimationPlayer, NoopAnimationPlayer} from './players/animation_player';
export * from './private_export'; export * from './private_export';

View File

@ -7,7 +7,7 @@
*/ */
import {animate, style, transition, trigger} from '@angular/animations'; import {animate, style, transition, trigger} from '@angular/animations';
import {AnimationDriver, ɵAnimationEngine} from '@angular/animations/browser'; import {AnimationDriver, ɵAnimationEngine} from '@angular/animations/browser';
import {ɵDomAnimationEngine, ɵWebAnimationsDriver, ɵWebAnimationsPlayer, ɵsupportsWebAnimations} from '@angular/animations/browser' import {ɵWebAnimationsDriver, ɵWebAnimationsPlayer, ɵsupportsWebAnimations} from '@angular/animations/browser'
import {Component, ViewChild} from '@angular/core'; import {Component, ViewChild} from '@angular/core';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations';

View File

@ -35,11 +35,6 @@ export class BrowserAnimationBuilder extends AnimationBuilder {
} }
} }
@Injectable()
export class NoopAnimationBuilder extends BrowserAnimationBuilder {
build(animation: AnimationMetadata|AnimationMetadata[]): Animation { return new NoopAnimation(); }
}
export class BrowserAnimation extends Animation { export class BrowserAnimation extends Animation {
constructor(private _id: string, private _renderer: AnimationRenderer) { super(); } constructor(private _id: string, private _renderer: AnimationRenderer) { super(); }
@ -48,14 +43,6 @@ export class BrowserAnimation extends Animation {
} }
} }
export class NoopAnimation extends Animation {
constructor() { super(); }
create(element: any, options?: AnimationOptions): AnimationPlayer {
return new NoopAnimationPlayer();
}
}
export class RendererAnimationPlayer implements AnimationPlayer { export class RendererAnimationPlayer implements AnimationPlayer {
public parentPlayer: AnimationPlayer|null = null; public parentPlayer: AnimationPlayer|null = null;
private _started = false; private _started = false;

View File

@ -46,7 +46,7 @@ export class AnimationRendererFactory implements RendererFactory2 {
this.delegate.begin(); this.delegate.begin();
} }
} }
end() { end() {
this._zone.runOutsideAngular(() => this._engine.flush()); this._zone.runOutsideAngular(() => this._engine.flush());
if (this.delegate.end) { if (this.delegate.end) {

View File

@ -5,6 +5,5 @@
* Use of this source code is governed by an MIT-style license that can be * Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
export {NoopAnimation as ɵNoopAnimation, NoopAnimationBuilder as ɵNoopAnimationBuilder} from './animation_builder';
export {BrowserAnimation as ɵBrowserAnimation, BrowserAnimationBuilder as ɵBrowserAnimationBuilder} from './animation_builder'; export {BrowserAnimation as ɵBrowserAnimation, BrowserAnimationBuilder as ɵBrowserAnimationBuilder} from './animation_builder';
export {AnimationRenderer as ɵAnimationRenderer, AnimationRendererFactory as ɵAnimationRendererFactory} from './animation_renderer'; export {AnimationRenderer as ɵAnimationRenderer, AnimationRendererFactory as ɵAnimationRendererFactory} from './animation_renderer';

View File

@ -7,22 +7,15 @@
*/ */
import {AnimationBuilder} from '@angular/animations'; 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 {AnimationDriver, ɵAnimationEngine as AnimationEngine, ɵAnimationStyleNormalizer as AnimationStyleNormalizer, ɵNoopAnimationDriver as NoopAnimationDriver, ɵWebAnimationsDriver as WebAnimationsDriver, ɵWebAnimationsStyleNormalizer as WebAnimationsStyleNormalizer, ɵsupportsWebAnimations as supportsWebAnimations} from '@angular/animations/browser';
import {Injectable, NgZone, Provider, RendererFactory2} from '@angular/core'; import {Injectable, NgZone, Provider, RendererFactory2} from '@angular/core';
import {ɵDomRendererFactory2 as DomRendererFactory2} from '@angular/platform-browser'; import {ɵDomRendererFactory2 as DomRendererFactory2} from '@angular/platform-browser';
import {BrowserAnimationBuilder, NoopAnimationBuilder} from './animation_builder'; import {BrowserAnimationBuilder} from './animation_builder';
import {AnimationRendererFactory} from './animation_renderer'; import {AnimationRendererFactory} from './animation_renderer';
@Injectable() @Injectable()
export class InjectableAnimationEngine extends DomAnimationEngine { export class InjectableAnimationEngine extends AnimationEngine {
constructor(driver: AnimationDriver, normalizer: AnimationStyleNormalizer) {
super(driver, normalizer);
}
}
@Injectable()
export class InjectableNoopAnimationEngine extends NoopAnimationEngine {
constructor(driver: AnimationDriver, normalizer: AnimationStyleNormalizer) { constructor(driver: AnimationDriver, normalizer: AnimationStyleNormalizer) {
super(driver, normalizer); super(driver, normalizer);
} }
@ -44,13 +37,8 @@ export function instantiateRendererFactory(
return new AnimationRendererFactory(renderer, engine, zone); return new AnimationRendererFactory(renderer, engine, zone);
} }
/** const SHARED_ANIMATION_PROVIDERS: Provider[] = [
* Separate providers from the actual module so that we can do a local modification in Google3 to {provide: AnimationBuilder, useClass: BrowserAnimationBuilder},
* include them in the BrowserModule.
*/
export const BROWSER_ANIMATIONS_PROVIDERS: Provider[] = [
{provide: AnimationBuilder, useClass: NoopAnimationBuilder},
{provide: AnimationDriver, useFactory: instantiateSupportedAnimationDriver},
{provide: AnimationStyleNormalizer, useFactory: instantiateDefaultStyleNormalizer}, {provide: AnimationStyleNormalizer, useFactory: instantiateDefaultStyleNormalizer},
{provide: AnimationEngine, useClass: InjectableAnimationEngine}, { {provide: AnimationEngine, useClass: InjectableAnimationEngine}, {
provide: RendererFactory2, provide: RendererFactory2,
@ -59,21 +47,18 @@ export const BROWSER_ANIMATIONS_PROVIDERS: Provider[] = [
} }
]; ];
/**
* Separate providers from the actual module so that we can do a local modification in Google3 to
* include them in the BrowserModule.
*/
export const BROWSER_ANIMATIONS_PROVIDERS: Provider[] = [
{provide: AnimationDriver, useFactory: instantiateSupportedAnimationDriver},
...SHARED_ANIMATION_PROVIDERS
];
/** /**
* Separate providers from the actual module so that we can do a local modification in Google3 to * Separate providers from the actual module so that we can do a local modification in Google3 to
* include them in the BrowserTestingModule. * include them in the BrowserTestingModule.
*/ */
export const BROWSER_NOOP_ANIMATIONS_PROVIDERS: Provider[] = [ export const BROWSER_NOOP_ANIMATIONS_PROVIDERS: Provider[] =
{provide: AnimationBuilder, useClass: BrowserAnimationBuilder}, [{provide: AnimationDriver, useClass: NoopAnimationDriver}, ...SHARED_ANIMATION_PROVIDERS];
{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

@ -1,274 +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 {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[] = [];
function capture(value?: string) { return (v: any = null) => captures.push(value || v); }
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 = 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(DEFAULT_NAMESPACE_ID, elm1, '1');
engine.onRemove(DEFAULT_NAMESPACE_ID, elm2, '2');
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();
expect(captures).toEqual(['1', '2', '1-start', '2-start', '1-done', '2-done']);
});
it('should only fire the `start` listener for a trigger that has had a property change', () => {
const engine = makeEngine();
const elm1 = {};
const elm2 = {};
const elm3 = {};
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();
expect(captures.length).toEqual(2);
const trig1Data = captures.shift();
const trig2Data = captures.shift();
expect(trig1Data).toEqual({
element: elm1,
triggerName: 'trig1',
fromState: 'void',
toState: 'cool',
phaseName: 'start',
totalTime: 0
});
expect(trig2Data).toEqual({
element: elm2,
triggerName: 'trig2',
fromState: 'void',
toState: 'sweet',
phaseName: 'start',
totalTime: 0
});
captures = [];
engine.flush();
expect(captures).toEqual([]);
});
it('should only fire the `done` listener for a trigger that has had a property change', () => {
const engine = makeEngine();
const elm1 = {};
const elm2 = {};
const elm3 = {};
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();
expect(captures.length).toEqual(2);
const trig1Data = captures.shift();
const trig2Data = captures.shift();
expect(trig1Data).toEqual({
element: elm1,
triggerName: 'trig1',
fromState: 'void',
toState: 'awesome',
phaseName: 'done',
totalTime: 0
});
expect(trig2Data).toEqual({
element: elm2,
triggerName: 'trig2',
fromState: 'void',
toState: 'amazing',
phaseName: 'done',
totalTime: 0
});
captures = [];
engine.flush();
expect(captures).toEqual([]);
});
it('should deregister a listener when the return function is called, but only after flush',
() => {
const engine = makeEngine();
const elm = {};
const fn1 = listen(elm, engine, 'trig1', 'start', capture('trig1-start'));
const fn2 = listen(elm, engine, 'trig2', 'done', capture('trig2-done'));
setProperty(elm, engine, 'trig1', 'value1');
setProperty(elm, engine, 'trig2', 'value2');
engine.flush();
expect(captures).toEqual(['trig1-start', 'trig2-done']);
captures = [];
setProperty(elm, engine, 'trig1', 'value3');
setProperty(elm, engine, 'trig2', 'value4');
fn1();
engine.flush();
expect(captures).toEqual(['trig1-start', 'trig2-done']);
captures = [];
setProperty(elm, engine, 'trig1', 'value5');
setProperty(elm, engine, 'trig2', 'value6');
fn2();
engine.flush();
expect(captures).toEqual(['trig2-done']);
captures = [];
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 = makeEngine();
const elm = {nodeType: 1};
engine.onRemovalComplete = (element: any, context: string) => { capture(context)(); };
const fn = listen(elm, engine, 'trig', 'start', capture('removal listener'));
fn();
engine.onRemove(DEFAULT_NAMESPACE_ID, elm, 'dom removal');
engine.flush();
expect(captures).toEqual(['dom removal', 'removal listener']);
});
describe('styling', () => {
// these tests are only mean't to be run within the DOM
if (typeof Element == 'undefined') return;
it('should persist the styles on the element when the animation is complete', () => {
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');
setProperty(element, engine, 'matias', 'a');
expect(element.style.width).not.toEqual('100px');
engine.flush();
expect(element.style.width).toEqual('100px');
});
it('should remove previously persist styles off of the element when a follow-up animation starts',
() => {
const engine = makeEngine();
const element = el('<div></div>');
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');
setProperty(element, engine, 'matias', 'b');
expect(element.style.width).not.toEqual('100px');
expect(element.style.height).not.toEqual('100px');
engine.flush();
expect(element.style.height).toEqual('100px');
});
it('should fall back to `*` styles incase the target state styles are not found', () => {
const engine = makeEngine();
const element = el('<div></div>');
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,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {animate, style, transition, trigger} from '@angular/animations'; import {animate, style, transition, trigger} from '@angular/animations';
import {ɵAnimationEngine, ɵNoopAnimationEngine} from '@angular/animations/browser'; import {ɵAnimationEngine} from '@angular/animations/browser';
import {Component} from '@angular/core'; import {Component} from '@angular/core';
import {TestBed} from '@angular/core/testing'; import {TestBed} from '@angular/core/testing';
@ -16,11 +16,6 @@ export function main() {
describe('NoopAnimationsModule', () => { describe('NoopAnimationsModule', () => {
beforeEach(() => { TestBed.configureTestingModule({imports: [NoopAnimationsModule]}); }); beforeEach(() => { TestBed.configureTestingModule({imports: [NoopAnimationsModule]}); });
it('the engine should be a Noop engine', () => {
const engine = TestBed.get(ɵAnimationEngine);
expect(engine instanceof ɵNoopAnimationEngine).toBeTruthy();
});
it('should flush and fire callbacks when the zone becomes stable', (async) => { it('should flush and fire callbacks when the zone becomes stable', (async) => {
@Component({ @Component({
selector: 'my-cmp', selector: 'my-cmp',
@ -80,20 +75,21 @@ export function main() {
cmp.exp = true; cmp.exp = true;
fixture.detectChanges(); fixture.detectChanges();
cmp.startEvent = null;
cmp.doneEvent = null;
cmp.exp = false;
fixture.detectChanges();
fixture.whenStable().then(() => { fixture.whenStable().then(() => {
expect(cmp.startEvent.triggerName).toEqual('myAnimation'); cmp.startEvent = null;
expect(cmp.startEvent.phaseName).toEqual('start'); cmp.doneEvent = null;
expect(cmp.startEvent.toState).toEqual('void');
expect(cmp.doneEvent.triggerName).toEqual('myAnimation'); cmp.exp = false;
expect(cmp.doneEvent.phaseName).toEqual('done'); fixture.detectChanges();
expect(cmp.doneEvent.toState).toEqual('void'); fixture.whenStable().then(() => {
async(); expect(cmp.startEvent.triggerName).toEqual('myAnimation');
expect(cmp.startEvent.phaseName).toEqual('start');
expect(cmp.startEvent.toState).toEqual('void');
expect(cmp.doneEvent.triggerName).toEqual('myAnimation');
expect(cmp.doneEvent.phaseName).toEqual('done');
expect(cmp.doneEvent.toState).toEqual('void');
async();
});
}); });
}); });
}); });

View File

@ -11,7 +11,6 @@ import {Component, Injectable, NgZone, RendererFactory2, RendererType2, ViewChil
import {TestBed} from '@angular/core/testing'; import {TestBed} from '@angular/core/testing';
import {BrowserAnimationsModule, ɵAnimationRendererFactory as 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 {DomRendererFactory2} from '@angular/platform-browser/src/dom/dom_renderer';
import {InjectableAnimationEngine} from '../../animations/src/providers'; import {InjectableAnimationEngine} from '../../animations/src/providers';
import {el} from '../../testing/src/browser_util'; import {el} from '../../testing/src/browser_util';
@ -311,7 +310,7 @@ export function main() {
} }
@Injectable() @Injectable()
class MockAnimationEngine extends AnimationEngine { class MockAnimationEngine extends InjectableAnimationEngine {
captures: {[method: string]: any[]} = {}; captures: {[method: string]: any[]} = {};
triggers: AnimationTriggerMetadata[] = []; triggers: AnimationTriggerMetadata[] = [];
@ -330,8 +329,9 @@ class MockAnimationEngine extends AnimationEngine {
this._capture('onRemove', [element]); this._capture('onRemove', [element]);
} }
setProperty(namespaceId: string, element: any, property: string, value: any): void { setProperty(namespaceId: string, element: any, property: string, value: any): boolean {
this._capture('setProperty', [element, property, value]); this._capture('setProperty', [element, property, value]);
return true;
} }
listen( listen(

View File

@ -92,7 +92,7 @@ export class DowngradeComponentAdapter {
// for `ngOnChanges()`. This is necessary if we are already in a `$digest`, which means that // for `ngOnChanges()`. This is necessary if we are already in a `$digest`, which means that
// `ngOnChanges()` (which is called by a watcher) will run before the `$observe()` callback. // `ngOnChanges()` (which is called by a watcher) will run before the `$observe()` callback.
let unwatch: any = this.componentScope.$watch(() => { let unwatch: any = this.componentScope.$watch(() => {
unwatch(); unwatch('');
unwatch = null; unwatch = null;
observeFn((attrs as any)[input.attr]); observeFn((attrs as any)[input.attr]);
}); });

View File

@ -2,7 +2,12 @@
export declare function animate(timings: string | number, styles?: AnimationStyleMetadata | AnimationKeyframesSequenceMetadata | null): AnimationAnimateMetadata; export declare function animate(timings: string | number, styles?: AnimationStyleMetadata | AnimationKeyframesSequenceMetadata | null): AnimationAnimateMetadata;
/** @experimental */ /** @experimental */
export declare function animateChild(options?: AnimationOptions | null): AnimationAnimateChildMetadata; export declare function animateChild(options?: AnimateChildOptions | null): AnimationAnimateChildMetadata;
/** @experimental */
export interface AnimateChildOptions extends AnimationOptions {
duration?: number | string;
}
/** @experimental */ /** @experimental */
export declare type AnimateTimings = { export declare type AnimateTimings = {
@ -87,7 +92,6 @@ export declare const enum AnimationMetadataType {
/** @experimental */ /** @experimental */
export interface AnimationOptions { export interface AnimationOptions {
delay?: number | string; delay?: number | string;
duration?: number | string;
params?: { params?: {
[name: string]: any; [name: string]: any;
}; };

View File

@ -4,5 +4,8 @@ export declare abstract class AnimationDriver {
[key: string]: string | number; [key: string]: string | number;
}[], duration: number, delay: number, easing?: string | null, previousPlayers?: any[]): any; }[], duration: number, delay: number, easing?: string | null, previousPlayers?: any[]): any;
abstract computeStyle(element: any, prop: string, defaultValue?: string): string; abstract computeStyle(element: any, prop: string, defaultValue?: string): string;
abstract containsElement(elm1: any, elm2: any): boolean;
abstract matchesElement(element: any, selector: string): boolean;
abstract query(element: any, selector: string, multi: boolean): any[];
static NOOP: AnimationDriver; static NOOP: AnimationDriver;
} }

View File

@ -4,6 +4,9 @@ export declare class MockAnimationDriver implements AnimationDriver {
[key: string]: string | number; [key: string]: string | number;
}[], duration: number, delay: number, easing: string, previousPlayers?: any[]): MockAnimationPlayer; }[], duration: number, delay: number, easing: string, previousPlayers?: any[]): MockAnimationPlayer;
computeStyle(element: any, prop: string, defaultValue?: string): string; computeStyle(element: any, prop: string, defaultValue?: string): string;
containsElement(elm1: any, elm2: any): boolean;
matchesElement(element: any, selector: string): boolean;
query(element: any, selector: string, multi: boolean): any[];
static log: AnimationPlayer[]; static log: AnimationPlayer[];
} }