feat(core): introduce support for animations

Closes #8734
This commit is contained in:
Matias Niemelä
2016-05-25 12:46:22 -07:00
parent 6c6b316bd9
commit 5e0f8cf3f0
83 changed files with 5294 additions and 756 deletions

View File

@ -40,7 +40,11 @@ export {
wtfEndTimeRange,
WtfScopeFn
} from './src/profile/profile';
export {Type, enableProdMode} from "./src/facade/lang";
export {EventEmitter} from "./src/facade/async";
export {ExceptionHandler, WrappedException, BaseException} from "./src/facade/exceptions";
export * from './private_export';
export * from './src/animation/metadata';
export {AnimationPlayer} from './src/animation/animation_player';

View File

@ -21,6 +21,27 @@ import * as provider_util from './src/di/provider_util';
import * as console from './src/console';
import {Provider} from './index';
import {
NoOpAnimationPlayer as NoOpAnimationPlayer_,
AnimationPlayer as AnimationPlayer_
} from './src/animation/animation_player';
import {
NoOpAnimationDriver as NoOpAnimationDriver_,
AnimationDriver as AnimationDriver_
} from './src/animation/animation_driver';
import {AnimationSequencePlayer as AnimationSequencePlayer_} from './src/animation/animation_sequence_player';
import {AnimationGroupPlayer as AnimationGroupPlayer_} from './src/animation/animation_group_player';
import {AnimationKeyframe as AnimationKeyframe_} from './src/animation/animation_keyframe';
import {AnimationStyleUtil as AnimationStyleUtil_} from './src/animation/animation_style_util';
import {AnimationStyles as AnimationStyles_} from './src/animation/animation_styles';
import {
ANY_STATE as ANY_STATE_,
EMPTY_STATE as EMPTY_STATE_,
FILL_STYLE_FLAG as FILL_STYLE_FLAG_
} from './src/animation/animation_constants';
import {MockAnimationPlayer as MockAnimationPlayer_} from './testing/animation/mock_animation_player';
import {MockAnimationDriver as MockAnimationDriver_} from './testing/animation/mock_animation_driver';
export declare namespace __core_private_types__ {
export var isDefaultChangeDetectionStrategy: typeof constants.isDefaultChangeDetectionStrategy;
export type ChangeDetectorState = constants.ChangeDetectorState;
@ -82,6 +103,31 @@ export declare namespace __core_private_types__ {
export var castByValue: typeof view_utils.castByValue;
export type Console = console.Console;
export var Console: typeof console.Console;
export type NoOpAnimationPlayer = NoOpAnimationPlayer_;
export var NoOpAnimationPlayer: typeof NoOpAnimationPlayer_;
export type AnimationPlayer = AnimationPlayer_;
export var AnimationPlayer: typeof AnimationPlayer_;
export type NoOpAnimationDriver = NoOpAnimationDriver_;
export var NoOpAnimationDriver: typeof NoOpAnimationDriver_;
export type AnimationDriver = AnimationDriver_;
export var AnimationDriver: typeof AnimationDriver_;
export type AnimationSequencePlayer = AnimationSequencePlayer_;
export var AnimationSequencePlayer: typeof AnimationSequencePlayer_;
export type AnimationGroupPlayer = AnimationGroupPlayer_;
export var AnimationGroupPlayer: typeof AnimationGroupPlayer_;
export type AnimationKeyframe = AnimationKeyframe_;
export var AnimationKeyframe: typeof AnimationKeyframe_;
export type AnimationStyleUtil = AnimationStyleUtil_;
export var AnimationStyleUtil: typeof AnimationStyleUtil_;
export type AnimationStyles = AnimationStyles_;
export var AnimationStyles: typeof AnimationStyles_;
export var ANY_STATE: typeof ANY_STATE_;
export var EMPTY_STATE: typeof EMPTY_STATE_;
export var FILL_STYLE_FLAG: typeof FILL_STYLE_FLAG_;
export type MockAnimationPlayer = MockAnimationPlayer_;
export var MockAnimationPlayer: typeof MockAnimationPlayer_;
export type MockAnimationDriver = MockAnimationDriver_;
export var MockAnimationDriver: typeof MockAnimationDriver_;
}
export var __core_private__ = {
@ -132,4 +178,18 @@ export var __core_private__ = {
pureProxy10: view_utils.pureProxy10,
castByValue: view_utils.castByValue,
Console: console.Console,
NoOpAnimationPlayer: NoOpAnimationPlayer_,
AnimationPlayer: AnimationPlayer_,
NoOpAnimationDriver: NoOpAnimationDriver_,
AnimationDriver: AnimationDriver_,
AnimationSequencePlayer: AnimationSequencePlayer_,
AnimationGroupPlayer: AnimationGroupPlayer_,
AnimationKeyframe: AnimationKeyframe_,
AnimationStyleUtil: AnimationStyleUtil_,
AnimationStyles: AnimationStyles_,
MockAnimationPlayer: MockAnimationPlayer_,
MockAnimationDriver: MockAnimationDriver_,
ANY_STATE: ANY_STATE_,
EMPTY_STATE: EMPTY_STATE_,
FILL_STYLE_FLAG: FILL_STYLE_FLAG_
};

View File

@ -0,0 +1,57 @@
import {AnimationPlayer} from './animation_player';
import {isPresent} from '../facade/lang';
import {ListWrapper, StringMapWrapper, Map} from '../facade/collection';
export class ActiveAnimationPlayersMap {
private _map = new Map<any, {[key: string]: AnimationPlayer}>();
private _allPlayers: AnimationPlayer[] = [];
get length(): number {
return this.getAllPlayers().length;
}
find(element: any, animationName: string): AnimationPlayer {
var playersByAnimation = this._map.get(element);
if (isPresent(playersByAnimation)) {
return playersByAnimation[animationName];
}
}
findAllPlayersByElement(element: any): AnimationPlayer[] {
var players = [];
StringMapWrapper.forEach(this._map.get(element), player => players.push(player));
return players;
}
set(element: any, animationName: string, player: AnimationPlayer): void {
var playersByAnimation = this._map.get(element);
if (!isPresent(playersByAnimation)) {
playersByAnimation = {};
}
var existingEntry = playersByAnimation[animationName];
if (isPresent(existingEntry)) {
this.remove(element, animationName);
}
playersByAnimation[animationName] = player;
this._allPlayers.push(player);
this._map.set(element, playersByAnimation);
}
getAllPlayers(): AnimationPlayer[] {
return this._allPlayers;
}
remove(element: any, animationName: string): void {
var playersByAnimation = this._map.get(element);
if (isPresent(playersByAnimation)) {
var player = playersByAnimation[animationName];
delete playersByAnimation[animationName];
var index = this._allPlayers.indexOf(player);
ListWrapper.removeAt(this._allPlayers, index);
if (StringMapWrapper.isEmpty(playersByAnimation)) {
this._map.delete(element);
}
}
}
}

View File

@ -0,0 +1,3 @@
export const FILL_STYLE_FLAG = 'true'; // TODO (matsko): change to boolean
export const ANY_STATE = '*';
export const EMPTY_STATE = 'void';

View File

@ -0,0 +1,15 @@
import {NoOpAnimationPlayer, AnimationPlayer} from './animation_player';
import {AnimationKeyframe} from './animation_keyframe';
import {AnimationStyles} from './animation_styles';
export abstract class AnimationDriver {
abstract animate(element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], duration: number, delay: number,
easing: string): AnimationPlayer;
}
export class NoOpAnimationDriver extends AnimationDriver {
animate(element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], duration: number, delay: number,
easing: string): AnimationPlayer {
return new NoOpAnimationPlayer();
}
}

View File

@ -0,0 +1,72 @@
import {AnimationPlayer} from './animation_player';
import {isPresent, scheduleMicroTask} from '../facade/lang';
import {Math} from '../facade/math';
export class AnimationGroupPlayer implements AnimationPlayer {
private _subscriptions: Function[] = [];
private _finished = false;
public parentPlayer: AnimationPlayer = null;
constructor(private _players: AnimationPlayer[]) {
var count = 0;
var total = this._players.length;
if (total == 0) {
scheduleMicroTask(() => this._onFinish());
} else {
this._players.forEach(player => {
player.parentPlayer = this;
player.onDone(() => {
if (++count >= total) {
this._onFinish();
}
});
});
}
}
private _onFinish() {
if (!this._finished) {
this._finished = true;
if (!isPresent(this.parentPlayer)) {
this.destroy();
}
this._subscriptions.forEach(subscription => subscription());
this._subscriptions = [];
}
}
onDone(fn: Function): void { this._subscriptions.push(fn); }
play() { this._players.forEach(player => player.play()); }
pause(): void { this._players.forEach(player => player.pause()); }
restart(): void { this._players.forEach(player => player.restart()); }
finish(): void {
this._onFinish();
this._players.forEach(player => player.finish());
}
destroy(): void {
this._onFinish();
this._players.forEach(player => player.destroy());
}
reset(): void { this._players.forEach(player => player.reset()); }
setPosition(p): void {
this._players.forEach(player => {
player.setPosition(p);
});
}
getPosition(): number {
var min = 0;
this._players.forEach(player => {
var p = player.getPosition();
min = Math.min(p, min);
});
return min;
}
}

View File

@ -0,0 +1,5 @@
import {AnimationStyles} from './animation_styles';
export class AnimationKeyframe {
constructor(public offset: number, public styles: AnimationStyles) {}
}

View File

@ -0,0 +1,39 @@
import {scheduleMicroTask} from '../facade/lang';
import {BaseException} from '../facade/exceptions';
export abstract class AnimationPlayer {
abstract onDone(fn: Function): void;
abstract play(): void;
abstract pause(): void;
abstract restart(): void;
abstract finish(): void;
abstract destroy(): void;
abstract reset(): void;
abstract setPosition(p): void;
abstract getPosition(): number;
get parentPlayer(): AnimationPlayer { throw new BaseException('NOT IMPLEMENTED: Base Class'); }
set parentPlayer(player: AnimationPlayer) { throw new BaseException('NOT IMPLEMENTED: Base Class'); }
}
export class NoOpAnimationPlayer implements AnimationPlayer {
private _subscriptions = [];
public parentPlayer: AnimationPlayer = null;
constructor() {
scheduleMicroTask(() => this._onFinish());
}
_onFinish() {
this._subscriptions.forEach(entry => { entry(); });
this._subscriptions = [];
}
onDone(fn: Function): void { this._subscriptions.push(fn); }
play(): void {}
pause(): void {}
restart(): void {}
finish(): void {
this._onFinish();
}
destroy(): void {}
reset(): void {}
setPosition(p): void {}
getPosition(): number { return 0; }
}

View File

@ -0,0 +1,83 @@
import {isPresent} from '../facade/lang';
import {NoOpAnimationPlayer, AnimationPlayer} from './animation_player';
import {scheduleMicroTask} from '../facade/lang';
export class AnimationSequencePlayer implements AnimationPlayer {
private _currentIndex: number = 0;
private _activePlayer: AnimationPlayer;
private _subscriptions: Function[] = [];
private _finished = false;
public parentPlayer: AnimationPlayer = null;
constructor(private _players: AnimationPlayer[]) {
this._players.forEach(player => {
player.parentPlayer = this;
});
this._onNext(false);
}
private _onNext(start: boolean) {
if (this._finished) return;
if (this._players.length == 0) {
this._activePlayer = new NoOpAnimationPlayer();
scheduleMicroTask(() => this._onFinish());
} else if (this._currentIndex >= this._players.length) {
this._activePlayer = new NoOpAnimationPlayer();
this._onFinish();
} else {
var player = this._players[this._currentIndex++];
player.onDone(() => this._onNext(true));
this._activePlayer = player;
if (start) {
player.play();
}
}
}
private _onFinish() {
if (!this._finished) {
this._finished = true;
if (!isPresent(this.parentPlayer)) {
this.destroy();
}
this._subscriptions.forEach(subscription => subscription());
this._subscriptions = [];
}
}
onDone(fn: Function): void { this._subscriptions.push(fn); }
play(): void { this._activePlayer.play(); }
pause(): void { this._activePlayer.pause(); }
restart(): void {
if (this._players.length > 0) {
this.reset();
this._players[0].restart();
}
}
reset(): void { this._players.forEach(player => player.reset()); }
finish(): void {
this._onFinish();
this._players.forEach(player => player.finish());
}
destroy(): void {
this._onFinish();
this._players.forEach(player => player.destroy());
}
setPosition(p): void {
this._players[0].setPosition(p);
}
getPosition(): number {
return this._players[0].getPosition();
}
}

View File

@ -0,0 +1,113 @@
import {isPresent, isArray} from '../facade/lang';
import {ListWrapper, StringMapWrapper} from '../facade/collection';
import {AUTO_STYLE} from './metadata';
import {FILL_STYLE_FLAG} from './animation_constants';
export class AnimationStyleUtil {
static balanceStyles(previousStyles: {[key: string]: string|number},
newStyles: {[key: string]: string|number},
nullValue = null): {[key: string]: string|number} {
var finalStyles: {[key: string]: string|number} = {};
StringMapWrapper.forEach(newStyles, (value, prop) => {
finalStyles[prop] = value;
});
StringMapWrapper.forEach(previousStyles, (value, prop) => {
if (!isPresent(finalStyles[prop])) {
finalStyles[prop] = nullValue;
}
});
return finalStyles;
}
static balanceKeyframes(collectedStyles: {[key: string]: string|number},
finalStateStyles: {[key: string]: string|number},
keyframes: any[]): any[] {
var limit = keyframes.length - 1;
var firstKeyframe = keyframes[0];
// phase 1: copy all the styles from the first keyframe into the lookup map
var flatenedFirstKeyframeStyles = AnimationStyleUtil.flattenStyles(firstKeyframe.styles.styles);
var extraFirstKeyframeStyles = {};
var hasExtraFirstStyles = false;
StringMapWrapper.forEach(collectedStyles, (value, prop) => {
// if the style is already defined in the first keyframe then
// we do not replace it.
if (!flatenedFirstKeyframeStyles[prop]) {
flatenedFirstKeyframeStyles[prop] = value;
extraFirstKeyframeStyles[prop] = value;
hasExtraFirstStyles = true;
}
});
var keyframeCollectedStyles = StringMapWrapper.merge({}, flatenedFirstKeyframeStyles);
// phase 2: normalize the final keyframe
var finalKeyframe = keyframes[limit];
ListWrapper.insert(finalKeyframe.styles.styles, 0, finalStateStyles);
var flatenedFinalKeyframeStyles = AnimationStyleUtil.flattenStyles(finalKeyframe.styles.styles);
var extraFinalKeyframeStyles = {};
var hasExtraFinalStyles = false;
StringMapWrapper.forEach(keyframeCollectedStyles, (value, prop) => {
if (!isPresent(flatenedFinalKeyframeStyles[prop])) {
extraFinalKeyframeStyles[prop] = AUTO_STYLE;
hasExtraFinalStyles = true;
}
});
if (hasExtraFinalStyles) {
finalKeyframe.styles.styles.push(extraFinalKeyframeStyles);
}
StringMapWrapper.forEach(flatenedFinalKeyframeStyles, (value, prop) => {
if (!isPresent(flatenedFirstKeyframeStyles[prop])) {
extraFirstKeyframeStyles[prop] = AUTO_STYLE;
hasExtraFirstStyles = true;
}
});
if (hasExtraFirstStyles) {
firstKeyframe.styles.styles.push(extraFirstKeyframeStyles);
}
return keyframes;
}
static clearStyles(styles: {[key: string]: string|number}): {[key: string]: string|number} {
var finalStyles: {[key: string]: string|number} = {};
StringMapWrapper.keys(styles).forEach(key => {
finalStyles[key] = null;
});
return finalStyles;
}
static collectAndResolveStyles(collection: {[key: string]: string|number}, styles: {[key: string]: string|number}[]) {
return styles.map(entry => {
var stylesObj = {};
StringMapWrapper.forEach(entry, (value, prop) => {
if (value == FILL_STYLE_FLAG) {
value = collection[prop];
if (!isPresent(value)) {
value = AUTO_STYLE;
}
}
collection[prop] = value;
stylesObj[prop] = value;
});
return stylesObj;
});
}
static flattenStyles(styles: {[key: string]: string|number}[]) {
var finalStyles = {};
styles.forEach(entry => {
StringMapWrapper.forEach(entry, (value, prop) => {
finalStyles[prop] = value;
});
});
return finalStyles;
}
}

View File

@ -0,0 +1,3 @@
export class AnimationStyles {
constructor(public styles: {[key: string]: string | number}[]) {}
}

View File

@ -0,0 +1,116 @@
import {isPresent, isArray, isString, isStringMap, NumberWrapper} from '../facade/lang';
import {BaseException} from '../facade/exceptions';
export const AUTO_STYLE = "*";
export class AnimationEntryMetadata {
constructor(public name: string, public definitions: AnimationStateMetadata[]) {}
}
export abstract class AnimationStateMetadata {}
export class AnimationStateDeclarationMetadata extends AnimationStateMetadata {
constructor(public stateNameExpr: string, public styles: AnimationStyleMetadata) { super(); }
}
export class AnimationStateTransitionMetadata extends AnimationStateMetadata {
constructor(public stateChangeExpr: string, public animation: AnimationMetadata) { super(); }
}
export abstract class AnimationMetadata {}
export class AnimationKeyframesSequenceMetadata extends AnimationMetadata {
constructor(public steps: AnimationStyleMetadata[]) {
super();
}
}
export class AnimationStyleMetadata extends AnimationMetadata {
constructor(public styles: Array<string|{[key: string]: string | number}>, public offset: number = null) { super(); }
}
export class AnimationAnimateMetadata extends AnimationMetadata {
constructor(public timings: string | number,
public styles: AnimationStyleMetadata|AnimationKeyframesSequenceMetadata) {
super();
}
}
export abstract class AnimationWithStepsMetadata extends AnimationMetadata {
constructor() { super(); }
get steps(): AnimationMetadata[] { throw new BaseException('NOT IMPLEMENTED: Base Class'); }
}
export class AnimationSequenceMetadata extends AnimationWithStepsMetadata {
constructor(private _steps: AnimationMetadata[]) { super(); }
get steps(): AnimationMetadata[] { return this._steps; }
}
export class AnimationGroupMetadata extends AnimationWithStepsMetadata {
constructor(private _steps: AnimationMetadata[]) { super(); }
get steps(): AnimationMetadata[] { return this._steps; }
}
export function animate(timing: string | number,
styles: AnimationStyleMetadata|AnimationKeyframesSequenceMetadata = null): AnimationAnimateMetadata {
var stylesEntry = styles;
if (!isPresent(stylesEntry)) {
var EMPTY_STYLE: {[key: string]: string|number} = {};
stylesEntry = new AnimationStyleMetadata([EMPTY_STYLE], 1);
}
return new AnimationAnimateMetadata(timing, stylesEntry);
}
export function group(steps: AnimationMetadata[]): AnimationGroupMetadata {
return new AnimationGroupMetadata(steps);
}
export function sequence(steps: AnimationMetadata[]): AnimationSequenceMetadata {
return new AnimationSequenceMetadata(steps);
}
export function style(tokens: string|{[key: string]: string | number}|Array<string|{[key: string]: string | number}>): AnimationStyleMetadata {
var input: Array<{[key: string]: string | number}|string>;
var offset: number = null;
if (isString(tokens)) {
input = [<string>tokens];
} else {
if (isArray(tokens)) {
input = <Array<{[key: string]: string | number}>>tokens;
} else {
input = [<{[key: string]: string | number}>tokens];
}
input.forEach(entry => {
var entryOffset = entry['offset'];
if (isPresent(entryOffset)) {
offset = offset == null ? NumberWrapper.parseFloat(entryOffset) : offset;
}
});
}
return new AnimationStyleMetadata(input, offset);
}
export function state(stateNameExpr: string, styles: AnimationStyleMetadata): AnimationStateDeclarationMetadata {
return new AnimationStateDeclarationMetadata(stateNameExpr, styles);
}
export function keyframes(steps: AnimationStyleMetadata|AnimationStyleMetadata[]): AnimationKeyframesSequenceMetadata {
var stepData = isArray(steps)
? <AnimationStyleMetadata[]>steps
: [<AnimationStyleMetadata>steps];
return new AnimationKeyframesSequenceMetadata(stepData);
}
export function transition(stateChangeExpr: string, animationData: AnimationMetadata|AnimationMetadata[]): AnimationStateTransitionMetadata {
var animation = isArray(animationData)
? new AnimationSequenceMetadata(<AnimationMetadata[]>animationData)
: <AnimationMetadata>animationData;
return new AnimationStateTransitionMetadata(stateChangeExpr, animation);
}
export function trigger(name: string, animation: AnimationMetadata|AnimationMetadata[]): AnimationEntryMetadata {
var entry = isArray(animation)
? <AnimationMetadata[]>animation
: [<AnimationMetadata>animation];
return new AnimationEntryMetadata(name, entry);
}

View File

@ -9,6 +9,10 @@ import {
removeDebugNodeFromIndex
} from './debug_node';
import {AnimationKeyframe} from '../animation/animation_keyframe';
import {AnimationStyles} from '../animation/animation_styles';
import {AnimationPlayer} from '../animation/animation_player';
export class DebugDomRootRenderer implements RootRenderer {
constructor(private _delegate: RootRenderer) {}
@ -137,4 +141,8 @@ export class DebugDomRenderer implements Renderer {
}
setText(renderNode: any, text: string) { this._delegate.setText(renderNode, text); }
animate(element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], duration: number, delay: number, easing: string): AnimationPlayer {
return this._delegate.animate(element, startingStyles, keyframes, duration, delay, easing);
}
}

View File

@ -81,8 +81,7 @@ export class AppElement {
if (view.type === ViewType.COMPONENT) {
throw new BaseException(`Component views can't be moved!`);
}
view.renderer.detachView(view.flatRootNodes);
view.detach();
view.removeFromContentChildren(this);
return view;

View File

@ -1,7 +1,9 @@
import {
ListWrapper,
StringMapWrapper,
} from '../../src/facade/collection';
Map,
MapWrapper
} from '../facade/collection';
import {AppElement} from './element';
import {
@ -14,9 +16,9 @@ import {
stringify,
isPrimitive,
isString
} from '../../src/facade/lang';
} from '../facade/lang';
import {ObservableWrapper} from '../../src/facade/async';
import {ObservableWrapper} from '../facade/async';
import {Renderer, RootRenderer, RenderComponentType, RenderDebugInfo} from '../render/api';
import {ViewRef_} from './view_ref';
@ -43,6 +45,14 @@ import {StaticNodeDebugInfo, DebugContext} from './debug_context';
import {ElementInjector} from './element_injector';
import {Injector} from '../di/injector';
import {AUTO_STYLE} from '../animation/metadata';
import {AnimationPlayer} from '../animation/animation_player';
import {AnimationGroupPlayer} from '../animation/animation_group_player';
import {AnimationKeyframe} from '../animation/animation_keyframe';
import {AnimationStyles} from '../animation/animation_styles';
import {AnimationDriver} from '../animation/animation_driver';
import {ActiveAnimationPlayersMap} from '../animation/active_animation_players_map';
var _scope_check: WtfScopeFn = wtfCreateScope(`AppView#check(ascii id)`);
/**
@ -71,6 +81,8 @@ export abstract class AppView<T> {
private _hasExternalHostElement: boolean;
public activeAnimationPlayers = new ActiveAnimationPlayersMap();
public context: T;
constructor(public clazz: any, public componentType: RenderComponentType, public type: ViewType,
@ -84,6 +96,25 @@ export abstract class AppView<T> {
}
}
cancelActiveAnimation(element: any, animationName: string, removeAllAnimations: boolean = false) {
if (removeAllAnimations) {
this.activeAnimationPlayers.findAllPlayersByElement(element).forEach(player => player.destroy());
} else {
var player = this.activeAnimationPlayers.find(element, animationName);
if (isPresent(player)) {
player.destroy();
}
}
}
registerAndStartAnimation(element: any, animationName: string, player: AnimationPlayer): void {
this.activeAnimationPlayers.set(element, animationName, player);
player.onDone(() => {
this.activeAnimationPlayers.remove(element, animationName);
});
player.play();
}
create(context: T, givenProjectableNodes: Array<any | any[]>,
rootSelectorOrNode: string | any): AppElement {
this.context = context;
@ -193,7 +224,15 @@ export abstract class AppView<T> {
}
this.destroyInternal();
this.dirtyParentQueriesInternal();
this.renderer.destroyView(hostElement, this.allNodes);
if (this.activeAnimationPlayers.length == 0) {
this.renderer.destroyView(hostElement, this.allNodes);
} else {
var player = new AnimationGroupPlayer(this.activeAnimationPlayers.getAllPlayers());
player.onDone(() => {
this.renderer.destroyView(hostElement, this.allNodes);
});
}
}
/**
@ -201,6 +240,23 @@ export abstract class AppView<T> {
*/
destroyInternal(): void {}
/**
* Overwritten by implementations
*/
detachInternal(): void {}
detach(): void {
this.detachInternal();
if (this.activeAnimationPlayers.length == 0) {
this.renderer.detachView(this.flatRootNodes);
} else {
var player = new AnimationGroupPlayer(this.activeAnimationPlayers.getAllPlayers());
player.onDone(() => {
this.renderer.detachView(this.flatRootNodes);
});
}
}
get changeDetectorRef(): ChangeDetectorRef { return this.ref; }
get parent(): AppView<any> {
@ -319,6 +375,16 @@ export class DebugAppView<T> extends AppView<T> {
}
}
detach(): void {
this._resetDebug();
try {
super.detach();
} catch (e) {
this._rethrowWithContext(e, e.stack);
throw e;
}
}
destroyLocal() {
this._resetDebug();
try {

View File

@ -5,6 +5,7 @@ import 'package:angular2/src/core/change_detection/change_detection.dart';
import './metadata/di.dart';
import './metadata/directives.dart';
import './metadata/view.dart';
import './metadata/animations.dart' show AnimationEntryMetadata;
export './metadata/di.dart';
export './metadata/directives.dart';
@ -72,7 +73,8 @@ class Component extends ComponentMetadata {
dynamic pipes,
ViewEncapsulation encapsulation,
List<String> styles,
List<String> styleUrls})
List<String> styleUrls,
List<AnimationEntryMetadata> animations})
: super(
selector: selector,
inputs: inputs,
@ -92,7 +94,8 @@ class Component extends ComponentMetadata {
pipes: pipes,
encapsulation: encapsulation,
styles: styles,
styleUrls: styleUrls);
styleUrls: styleUrls,
animations: animations);
}
/**
@ -106,7 +109,8 @@ class View extends ViewMetadata {
dynamic pipes,
ViewEncapsulation encapsulation,
List<String> styles,
List<String> styleUrls})
List<String> styleUrls,
List<AnimationEntryMetadata> animations})
: super(
templateUrl: templateUrl,
template: template,
@ -114,7 +118,8 @@ class View extends ViewMetadata {
pipes: pipes,
encapsulation: encapsulation,
styles: styles,
styleUrls: styleUrls);
styleUrls: styleUrls,
animations: animations);
}
/**

View File

@ -57,7 +57,8 @@ import {
} from './metadata/directives';
import {ViewMetadata, ViewEncapsulation} from './metadata/view';
import {ChangeDetectionStrategy} from './change_detection/change_detection';
import {AnimationEntryMetadata} from './animation/metadata';
import {ChangeDetectionStrategy} from '../src/change_detection/change_detection';
import {
makeDecorator,
@ -91,6 +92,7 @@ export interface ComponentDecorator extends TypeDecorator {
renderer?: string,
styles?: string[],
styleUrls?: string[],
animations?: AnimationEntryMetadata[]
}): ViewDecorator;
}
@ -111,6 +113,7 @@ export interface ViewDecorator extends TypeDecorator {
renderer?: string,
styles?: string[],
styleUrls?: string[],
animations?: AnimationEntryMetadata[]
}): ViewDecorator;
}
@ -219,6 +222,7 @@ export interface ComponentMetadataFactory {
template?: string,
styleUrls?: string[],
styles?: string[],
animations?: AnimationEntryMetadata[],
directives?: Array<Type | any[]>,
pipes?: Array<Type | any[]>,
encapsulation?: ViewEncapsulation
@ -240,6 +244,7 @@ export interface ComponentMetadataFactory {
template?: string,
styleUrls?: string[],
styles?: string[],
animations?: AnimationEntryMetadata[],
directives?: Array<Type | any[]>,
pipes?: Array<Type | any[]>,
encapsulation?: ViewEncapsulation
@ -297,6 +302,7 @@ export interface ViewMetadataFactory {
encapsulation?: ViewEncapsulation,
styles?: string[],
styleUrls?: string[],
animations?: AnimationEntryMetadata[]
}): ViewDecorator;
new (obj: {
templateUrl?: string,
@ -306,6 +312,7 @@ export interface ViewMetadataFactory {
encapsulation?: ViewEncapsulation,
styles?: string[],
styleUrls?: string[],
animations?: AnimationEntryMetadata[]
}): ViewMetadata;
}

View File

@ -2,6 +2,7 @@ import {isPresent, Type} from '../../src/facade/lang';
import {InjectableMetadata} from '../di/metadata';
import {ViewEncapsulation} from './view';
import {ChangeDetectionStrategy} from '../change_detection/constants';
import {AnimationEntryMetadata} from '../animation/metadata';
/**
* Directives allow you to attach behavior to elements in the DOM.
@ -872,6 +873,8 @@ export class ComponentMetadata extends DirectiveMetadata {
styles: string[];
animations: AnimationEntryMetadata[];
directives: Array<Type | any[]>;
pipes: Array<Type | any[]>;
@ -881,7 +884,7 @@ export class ComponentMetadata extends DirectiveMetadata {
constructor({selector, inputs, outputs, properties, events, host, exportAs, moduleId,
providers, viewProviders,
changeDetection = ChangeDetectionStrategy.Default, queries, templateUrl, template,
styleUrls, styles, directives, pipes, encapsulation}: {
styleUrls, styles, animations, directives, pipes, encapsulation}: {
selector?: string,
inputs?: string[],
outputs?: string[],
@ -898,6 +901,7 @@ export class ComponentMetadata extends DirectiveMetadata {
template?: string,
styleUrls?: string[],
styles?: string[],
animations?: AnimationEntryMetadata[],
directives?: Array<Type | any[]>,
pipes?: Array<Type | any[]>,
encapsulation?: ViewEncapsulation
@ -924,6 +928,7 @@ export class ComponentMetadata extends DirectiveMetadata {
this.pipes = pipes;
this.encapsulation = encapsulation;
this.moduleId = moduleId;
this.animations = animations;
}
}

View File

@ -1,4 +1,5 @@
import {Type} from '../../src/facade/lang';
import {AnimationEntryMetadata} from '../animation/metadata';
/**
* Defines template and style encapsulation options available for Component's {@link View}.
@ -123,7 +124,10 @@ export class ViewMetadata {
*/
encapsulation: ViewEncapsulation;
constructor({templateUrl, template, directives, pipes, encapsulation, styles, styleUrls}: {
animations: AnimationEntryMetadata[];
constructor({templateUrl, template, directives, pipes, encapsulation, styles, styleUrls,
animations}: {
templateUrl?: string,
template?: string,
directives?: Array<Type | any[]>,
@ -131,6 +135,7 @@ export class ViewMetadata {
encapsulation?: ViewEncapsulation,
styles?: string[],
styleUrls?: string[],
animations?: AnimationEntryMetadata[]
} = {}) {
this.templateUrl = templateUrl;
this.template = template;
@ -139,5 +144,6 @@ export class ViewMetadata {
this.directives = directives;
this.pipes = pipes;
this.encapsulation = encapsulation;
this.animations = animations;
}
}

View File

@ -1,6 +1,9 @@
import {unimplemented} from '../../src/facade/exceptions';
import {ViewEncapsulation} from '../metadata/view';
import {Injector} from '../di/injector';
import {AnimationKeyframe} from '../../src/animation/animation_keyframe';
import {AnimationPlayer} from '../../src/animation/animation_player';
import {AnimationStyles} from '../../src/animation/animation_styles';
export class RenderComponentType {
constructor(public id: string, public templateUrl: string, public slotCount: number,
@ -59,6 +62,8 @@ export abstract class Renderer {
abstract invokeElementMethod(renderElement: any, methodName: string, args: any[]);
abstract setText(renderNode: any, text: string);
abstract animate(element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], duration: number, delay: number, easing: string): AnimationPlayer;
}
/**

View File

@ -0,0 +1,85 @@
import {
AsyncTestCompleter,
beforeEach,
ddescribe,
xdescribe,
describe,
expect,
iit,
inject,
it,
xit
} from '../../testing/testing_internal';
import {
fakeAsync,
flushMicrotasks
} from '../../testing';
import {el} from '@angular/platform-browser/testing';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {isPresent} from '../../src/facade/lang';
import {MockAnimationPlayer} from '../../testing/animation/mock_animation_player';
import {ActiveAnimationPlayersMap} from '../../src/animation/active_animation_players_map';
export function main() {
describe('ActiveAnimationsPlayersMap', function() {
var playersMap;
var elementNode;
var animationName = 'animationName';
beforeEach(() => {
playersMap = new ActiveAnimationPlayersMap();
elementNode = el('<div></div>');
});
afterEach(() => {
getDOM().remove(elementNode);
elementNode = null;
});
it('should register a player an allow it to be accessed', () => {
var player = new MockAnimationPlayer();
playersMap.set(elementNode, animationName, player);
expect(playersMap.find(elementNode, animationName)).toBe(player);
expect(playersMap.findAllPlayersByElement(elementNode)).toEqual([player]);
expect(playersMap.getAllPlayers()).toEqual([player]);
expect(playersMap.length).toEqual(1);
});
it('should remove a registered player when remove() is called', () => {
var player = new MockAnimationPlayer();
playersMap.set(elementNode, animationName, player);
expect(playersMap.find(elementNode, animationName)).toBe(player);
expect(playersMap.length).toEqual(1);
playersMap.remove(elementNode, animationName);
expect(playersMap.find(elementNode, animationName)).not.toBe(player);
expect(playersMap.length).toEqual(0);
});
it('should allow multiple players to be registered on the same element', () => {
var player1 = new MockAnimationPlayer();
var player2 = new MockAnimationPlayer();
playersMap.set(elementNode, 'myAnimation1', player1);
playersMap.set(elementNode, 'myAnimation2', player2);
expect(playersMap.length).toEqual(2);
expect(playersMap.findAllPlayersByElement(elementNode)).toEqual([
player1,
player2
]);
});
it('should only allow one player to be set for a given element/animationName pair', () => {
var player1 = new MockAnimationPlayer();
var player2 = new MockAnimationPlayer();
playersMap.set(elementNode, animationName, player1);
expect(playersMap.find(elementNode, animationName)).toBe(player1);
expect(playersMap.length).toEqual(1);
playersMap.set(elementNode, animationName, player2);
expect(playersMap.find(elementNode, animationName)).toBe(player2);
expect(playersMap.length).toEqual(1);
});
});
}

View File

@ -0,0 +1,194 @@
import {
AsyncTestCompleter,
beforeEach,
ddescribe,
xdescribe,
describe,
expect,
iit,
inject,
it,
xit
} from '../../testing/testing_internal';
import {
fakeAsync,
flushMicrotasks
} from '../../testing';
import {isPresent} from '../../src/facade/lang';
import {AnimationGroupPlayer} from '../../src/animation/animation_group_player';
import {MockAnimationPlayer} from '../../testing/animation/mock_animation_player';
export function main() {
describe('AnimationGroupPlayer', function() {
var players;
beforeEach(() => {
players = [
new MockAnimationPlayer(),
new MockAnimationPlayer(),
new MockAnimationPlayer(),
];
});
var assertLastStatus =
(player: MockAnimationPlayer, status: string, match: boolean, iOffset: number = 0) => {
var index = player.log.length - 1 + iOffset;
var actual = player.log.length > 0 ? player.log[index] : null;
if (match) {
expect(actual).toEqual(status);
} else {
expect(actual).not.toEqual(status);
}
}
var assertPlaying = (player: MockAnimationPlayer, isPlaying: boolean) => {
assertLastStatus(player, 'play', isPlaying);
};
it('should play and pause all players in parallel', () => {
var group = new AnimationGroupPlayer(players);
assertPlaying(players[0], false);
assertPlaying(players[1], false);
assertPlaying(players[2], false);
group.play();
assertPlaying(players[0], true);
assertPlaying(players[1], true);
assertPlaying(players[2], true);
group.pause();
assertPlaying(players[0], false);
assertPlaying(players[1], false);
assertPlaying(players[2], false);
});
it('should finish when all players have finished', () => {
var group = new AnimationGroupPlayer(players);
var completed = false;
group.onDone(() => completed = true);
group.play();
expect(completed).toBeFalsy();
players[0].finish();
expect(completed).toBeFalsy();
players[1].finish();
expect(completed).toBeFalsy();
players[2].finish();
expect(completed).toBeTruthy();
});
it('should restart all the players', () => {
var group = new AnimationGroupPlayer(players);
group.play();
assertLastStatus(players[0], 'restart', false);
assertLastStatus(players[1], 'restart', false);
assertLastStatus(players[2], 'restart', false);
group.restart();
assertLastStatus(players[0], 'restart', true);
assertLastStatus(players[1], 'restart', true);
assertLastStatus(players[2], 'restart', true);
});
it('should finish all the players', () => {
var group = new AnimationGroupPlayer(players);
var completed = false;
group.onDone(() => completed = true);
expect(completed).toBeFalsy();
group.play();
assertLastStatus(players[0], 'finish', false);
assertLastStatus(players[1], 'finish', false);
assertLastStatus(players[2], 'finish', false);
expect(completed).toBeFalsy();
group.finish();
assertLastStatus(players[0], 'finish', true, -1);
assertLastStatus(players[1], 'finish', true, -1);
assertLastStatus(players[2], 'finish', true, -1);
assertLastStatus(players[0], 'destroy', true);
assertLastStatus(players[1], 'destroy', true);
assertLastStatus(players[2], 'destroy', true);
expect(completed).toBeTruthy();
});
it('should call destroy automatically when finished if no parent player is present', () => {
var group = new AnimationGroupPlayer(players);
group.play();
assertLastStatus(players[0], 'destroy', false);
assertLastStatus(players[1], 'destroy', false);
assertLastStatus(players[2], 'destroy', false);
group.finish();
assertLastStatus(players[0], 'destroy', true);
assertLastStatus(players[1], 'destroy', true);
assertLastStatus(players[2], 'destroy', true);
});
it('should not call destroy automatically when finished if a parent player is present', () => {
var group = new AnimationGroupPlayer(players);
var parent = new AnimationGroupPlayer([group, new MockAnimationPlayer()]);
group.play();
assertLastStatus(players[0], 'destroy', false);
assertLastStatus(players[1], 'destroy', false);
assertLastStatus(players[2], 'destroy', false);
group.finish();
assertLastStatus(players[0], 'destroy', false);
assertLastStatus(players[1], 'destroy', false);
assertLastStatus(players[2], 'destroy', false);
parent.finish();
assertLastStatus(players[0], 'destroy', true);
assertLastStatus(players[1], 'destroy', true);
assertLastStatus(players[2], 'destroy', true);
});
it('should function without any players', () => {
var group = new AnimationGroupPlayer([]);
group.onDone(() => {});
group.pause();
group.play();
group.finish();
group.restart();
group.destroy();
});
it('should call onDone after the next microtask if no players are provided', fakeAsync(() => {
var group = new AnimationGroupPlayer([]);
var completed = false;
group.onDone(() => completed = true);
expect(completed).toEqual(false);
flushMicrotasks();
expect(completed).toEqual(true);
}));
});
}

View File

@ -0,0 +1,899 @@
import {
AsyncTestCompleter,
beforeEach,
ddescribe,
xdescribe,
describe,
expect,
iit,
inject,
it,
xit,
beforeEachProviders
} from '../../testing/testing_internal';
import {TestComponentBuilder} from '@angular/compiler/testing';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {
fakeAsync,
flushMicrotasks,
tick
} from '../../testing';
import {isPresent, isArray, IS_DART} from '../../src/facade/lang';
import {provide, Component} from '../../index';
import {NgIf, NgFor, AsyncPipe} from '@angular/common';
import {CompilerConfig} from '@angular/compiler';
import {AnimationDriver} from '../../src/animation/animation_driver';
import {MockAnimationDriver} from '../../testing/animation/mock_animation_driver';
import {trigger, state, transition, keyframes, style, animate, group, sequence, AnimationEntryMetadata} from '../../src/animation/metadata';
import {AnimationStyleUtil} from '../../src/animation/animation_style_util';
import {AUTO_STYLE} from '../../src/animation/metadata';
export function main() {
if (IS_DART) {
declareTests();
} else {
describe('jit', () => {
beforeEachProviders(
() => [provide(CompilerConfig, {useValue: new CompilerConfig(true, false, true)})]);
declareTests();
});
describe('no jit', () => {
beforeEachProviders(
() => [provide(CompilerConfig, {useValue: new CompilerConfig(true, false, false)})]);
declareTests();
});
}
}
function declareTests() {
describe('animation tests', function() {
beforeEachProviders(() => [provide(AnimationDriver, {useClass: MockAnimationDriver})]);
var makeAnimationCmp = (tcb: TestComponentBuilder, tpl: string, animationEntry: AnimationEntryMetadata|AnimationEntryMetadata[], callback = null) => {
var entries = isArray(animationEntry)
? <AnimationEntryMetadata[]>animationEntry
: [<AnimationEntryMetadata>animationEntry];
tcb = tcb.overrideTemplate(DummyIfCmp, tpl);
tcb = tcb.overrideAnimations(DummyIfCmp, entries);
tcb.createAsync(DummyIfCmp).then((root) => { callback(root); });
tick();
};
describe('animation triggers', () => {
it('should trigger a state change animation from void => state',
inject([TestComponentBuilder, AnimationDriver],
fakeAsync(
(tcb: TestComponentBuilder, driver: MockAnimationDriver) => {
makeAnimationCmp(tcb, '<div *ngIf="exp" @myAnimation="exp"></div>',
trigger('myAnimation', [
transition('void => *', [
style({'opacity': 0}),
animate(500,
style({'opacity': 1}))
])
]),
(fixture) => {
var cmp = fixture.debugElement.componentInstance;
cmp.exp = true;
fixture.detectChanges();
flushMicrotasks();
expect(driver.log.length).toEqual(1);
var keyframes2 = driver.log[0]['keyframeLookup'];
expect(keyframes2.length).toEqual(2);
expect(keyframes2[0]).toEqual([0, {'opacity': 0}]);
expect(keyframes2[1]).toEqual([1, {'opacity': 1}]);
});
})));
it('should trigger a state change animation from state => void',
inject([TestComponentBuilder, AnimationDriver],
fakeAsync(
(tcb: TestComponentBuilder, driver: MockAnimationDriver) => {
makeAnimationCmp(tcb, '<div *ngIf="exp" @myAnimation="exp"></div>',
trigger('myAnimation', [
transition('* => void', [
style({'opacity': 1}),
animate(500,
style({'opacity': 0}))
])
]),
(fixture) => {
var cmp = fixture.debugElement.componentInstance;
cmp.exp = true;
fixture.detectChanges();
flushMicrotasks();
cmp.exp = false;
fixture.detectChanges();
flushMicrotasks();
expect(driver.log.length).toEqual(1);
var keyframes2 = driver.log[0]['keyframeLookup'];
expect(keyframes2.length).toEqual(2);
expect(keyframes2[0]).toEqual([0, {'opacity': 1}]);
expect(keyframes2[1]).toEqual([1, {'opacity': 0}]);
});
})));
it('should animate the element when the expression changes between states',
inject([TestComponentBuilder, AnimationDriver],
fakeAsync(
(tcb: TestComponentBuilder, driver: MockAnimationDriver) => {
tcb.overrideAnimations(DummyIfCmp, [
trigger("myAnimation", [
transition("* => state1", [
style({'background': 'red'}),
animate('0.5s 1s ease-out',
style({'background': 'blue'}))
])
])
]).createAsync(DummyIfCmp)
.then((fixture) => {
tick();
var cmp = fixture.debugElement.componentInstance;
cmp.exp = 'state1';
fixture.detectChanges();
flushMicrotasks();
expect(driver.log.length).toEqual(1);
var animation1 = driver.log[0];
expect(animation1['duration']).toEqual(500);
expect(animation1['delay']).toEqual(1000);
expect(animation1['easing']).toEqual('ease-out');
var startingStyles = animation1['startingStyles'];
expect(startingStyles).toEqual({'background': 'red'});
var keyframes = animation1['keyframeLookup'];
expect(keyframes[0]).toEqual([0, {'background': 'red'}]);
expect(keyframes[1]).toEqual([1, {'background': 'blue'}]);
});
})));
it('should combine repeated style steps into a single step',
inject(
[TestComponentBuilder, AnimationDriver],
fakeAsync((tcb: TestComponentBuilder, driver: MockAnimationDriver) => {
tcb.overrideAnimations(DummyIfCmp, [
trigger("myAnimation", [
transition("void => *", [
style({'background': 'red'}),
style({'width': '100px'}),
style({'background': 'gold'}),
style({'height': 111}),
animate('999ms', style({'width': '200px', 'background': 'blue'})),
style({'opacity': '1'}),
style({'border-width': '100px'}),
animate('999ms', style({'opacity': '0', 'height': '200px', 'border-width': '10px'}))
])
])
])
.createAsync(DummyIfCmp)
.then((fixture) => {
tick();
var cmp = fixture.debugElement.componentInstance;
cmp.exp = true;
fixture.detectChanges();
flushMicrotasks();
expect(driver.log.length).toEqual(2);
var animation1 = driver.log[0];
expect(animation1['duration']).toEqual(999);
expect(animation1['delay']).toEqual(0);
expect(animation1['easing']).toEqual(null);
expect(animation1['startingStyles'])
.toEqual({'background': 'gold', 'width': '100px', 'height': 111});
var keyframes1 = animation1['keyframeLookup'];
expect(keyframes1[0]).toEqual([0, {'background': 'gold', 'width': '100px'}]);
expect(keyframes1[1]).toEqual([1, {'background': 'blue', 'width': '200px'}]);
var animation2 = driver.log[1];
expect(animation2['duration']).toEqual(999);
expect(animation2['delay']).toEqual(0);
expect(animation2['easing']).toEqual(null);
expect(animation2['startingStyles'])
.toEqual({'opacity': '1', 'border-width': '100px'});
var keyframes2 = animation2['keyframeLookup'];
expect(keyframes2[0])
.toEqual([0, {'opacity': '1', 'height': 111, 'border-width': '100px'}]);
expect(keyframes2[1])
.toEqual([1, {'opacity': '0', 'height': '200px', 'border-width': '10px'}]);
});
})));
describe('groups/sequences', () => {
var assertPlaying =
(player: MockAnimationDriver, isPlaying) => {
var method = 'play';
var lastEntry = player.log.length > 0 ? player.log[player.log.length - 1] : null;
if (isPresent(lastEntry)) {
if (isPlaying) {
expect(lastEntry).toEqual(method);
} else {
expect(lastEntry).not.toEqual(method);
}
}
}
it('should run animations in sequence one by one if a top-level array is used',
inject([TestComponentBuilder, AnimationDriver],
fakeAsync((tcb: TestComponentBuilder, driver: MockAnimationDriver) => {
tcb.overrideAnimations(DummyIfCmp, [
trigger("myAnimation", [
transition("void => *", [
style({"opacity": '0'}),
animate(1000, style({'opacity': '0.5'})),
animate('1000ms', style({'opacity': '0.8'})),
animate('1s', style({'opacity': '1'})),
])
])
])
.createAsync(DummyIfCmp)
.then((fixture) => {
tick();
var cmp = fixture.debugElement.componentInstance;
cmp.exp = true;
fixture.detectChanges();
flushMicrotasks();
expect(driver.log.length).toEqual(3);
var player1 = driver.log[0]['player'];
var player2 = driver.log[1]['player'];
var player3 = driver.log[2]['player'];
assertPlaying(player1, true);
assertPlaying(player2, false);
assertPlaying(player3, false);
player1.finish();
assertPlaying(player1, false);
assertPlaying(player2, true);
assertPlaying(player3, false);
player2.finish();
assertPlaying(player1, false);
assertPlaying(player2, false);
assertPlaying(player3, true);
player3.finish();
assertPlaying(player1, false);
assertPlaying(player2, false);
assertPlaying(player3, false);
});
})));
it('should run animations in parallel if a group is used',
inject(
[TestComponentBuilder, AnimationDriver],
fakeAsync((tcb: TestComponentBuilder, driver: MockAnimationDriver) => {
tcb.overrideAnimations(DummyIfCmp, [
trigger("myAnimation", [
transition("void => *", [
style({'width': 0, 'height': 0}),
group([animate(1000, style({'width': 100})), animate(5000, style({'height': 500}))]),
group([animate(1000, style({'width': 0})), animate(5000, style({'height': 0}))])
])
])
])
.createAsync(DummyIfCmp)
.then((fixture) => {
tick();
var cmp = fixture.debugElement.componentInstance;
cmp.exp = true;
fixture.detectChanges();
flushMicrotasks();
expect(driver.log.length).toEqual(5);
var player1 = driver.log[0]['player'];
var player2 = driver.log[1]['player'];
var player3 = driver.log[2]['player'];
var player4 = driver.log[3]['player'];
var player5 = driver.log[4]['player'];
assertPlaying(player1, true);
assertPlaying(player2, false);
assertPlaying(player3, false);
assertPlaying(player4, false);
assertPlaying(player5, false);
player1.finish();
assertPlaying(player1, false);
assertPlaying(player2, true);
assertPlaying(player3, true);
assertPlaying(player4, false);
assertPlaying(player5, false);
player2.finish();
assertPlaying(player1, false);
assertPlaying(player2, false);
assertPlaying(player3, true);
assertPlaying(player4, false);
assertPlaying(player5, false);
player3.finish();
assertPlaying(player1, false);
assertPlaying(player2, false);
assertPlaying(player3, false);
assertPlaying(player4, true);
assertPlaying(player5, true);
});
})));
});
describe('keyframes', () => {
it('should create an animation step with multiple keyframes',
inject(
[TestComponentBuilder, AnimationDriver],
fakeAsync((tcb: TestComponentBuilder, driver: MockAnimationDriver) => {
tcb.overrideAnimations(DummyIfCmp, [
trigger("myAnimation", [
transition("void => *", [
animate(1000, keyframes([
style([{ "width": 0, offset: 0 }]),
style([{ "width": 100, offset: 0.25 }]),
style([{ "width": 200, offset: 0.75 }]),
style([{ "width": 300, offset: 1 }])
]))
])
])
])
.createAsync(DummyIfCmp)
.then((fixture) => {
tick();
var cmp = fixture.debugElement.componentInstance;
cmp.exp = true;
fixture.detectChanges();
flushMicrotasks();
var keyframes = driver.log[0]['keyframeLookup'];
expect(keyframes.length).toEqual(4);
expect(keyframes[0]).toEqual([0, {'width': 0}]);
expect(keyframes[1]).toEqual([0.25, {'width': 100}]);
expect(keyframes[2]).toEqual([0.75, {'width': 200}]);
expect(keyframes[3]).toEqual([1, {'width': 300}]);
});
})));
it('should fetch any keyframe styles that are not defined in the first keyframe from the previous entries or getCompuedStyle',
inject(
[TestComponentBuilder, AnimationDriver],
fakeAsync((tcb: TestComponentBuilder, driver: MockAnimationDriver) => {
tcb.overrideAnimations(DummyIfCmp, [
trigger("myAnimation", [
transition("void => *", [
style({ "color": "white" }),
animate(1000, style({ "color": "silver" })),
animate(1000, keyframes([
style([{ "color": "gold", offset: 0.25 }]),
style([{ "color": "bronze", "background-color":"teal", offset: 0.50 }]),
style([{ "color": "platinum", offset: 0.75 }]),
style([{ "color": "diamond", offset: 1 }])
]))
])
])
])
.createAsync(DummyIfCmp)
.then((fixture) => {
tick();
var cmp = fixture.debugElement.componentInstance;
cmp.exp = true;
fixture.detectChanges();
flushMicrotasks();
var keyframes = driver.log[1]['keyframeLookup'];
expect(keyframes.length).toEqual(5);
expect(keyframes[0]).toEqual([0, {"color": "silver", "background-color":AUTO_STYLE }]);
expect(keyframes[1]).toEqual([0.25, {"color": "gold"}]);
expect(keyframes[2]).toEqual([0.50, {"color": "bronze", "background-color":"teal"}]);
expect(keyframes[3]).toEqual([0.75, {"color": "platinum"}]);
expect(keyframes[4]).toEqual([1, {"color": "diamond", "background-color":"teal"}]);
});
})));
});
it('should cancel the previously running animation active with the same element/animationName pair',
inject([TestComponentBuilder, AnimationDriver],
fakeAsync(
(tcb: TestComponentBuilder, driver: MockAnimationDriver) => {
tcb.overrideAnimations(DummyIfCmp, [
trigger("myAnimation", [
transition("* => *", [
style({ "opacity":0 }),
animate(500, style({ "opacity":1 }))
])
])
])
.createAsync(DummyIfCmp)
.then((fixture) => {
tick();
var cmp = fixture.debugElement.componentInstance;
cmp.exp = "state1";
fixture.detectChanges();
flushMicrotasks();
var enterCompleted = false;
var enterPlayer = driver.log[0]['player'];
enterPlayer.onDone(() => enterCompleted = true);
expect(enterCompleted).toEqual(false);
cmp.exp = "state2";
fixture.detectChanges();
flushMicrotasks();
expect(enterCompleted).toEqual(true);
});
})));
it('should destroy all animation players once the animation is complete',
inject([TestComponentBuilder, AnimationDriver],
fakeAsync(
(tcb: TestComponentBuilder, driver: MockAnimationDriver) => {
tcb.overrideAnimations(DummyIfCmp, [
trigger("myAnimation", [
transition("void => *", [
style({'background': 'red', 'opacity': 0.5}),
animate(500, style({'background': 'black'})),
group([
animate(500, style({'background': 'black'})),
animate(1000, style({'opacity': '0.2'})),
]),
sequence([
animate(500, style({'opacity': '1'})),
animate(1000, style({'background': 'white'}))
])
])
])
]).createAsync(DummyIfCmp)
.then((fixture) => {
tick();
var cmp = fixture.debugElement.componentInstance;
cmp.exp = true;
fixture.detectChanges();
flushMicrotasks();
expect(driver.log.length).toEqual(5);
driver.log.forEach(entry => entry['player'].finish());
driver.log.forEach(entry => {
var player = <MockAnimationDriver>entry['player'];
expect(player.log[player.log.length - 2]).toEqual('finish');
expect(player.log[player.log.length - 1]).toEqual('destroy');
});
});
})));
it('should use first matched animation when multiple animations are registered',
inject(
[TestComponentBuilder, AnimationDriver],
fakeAsync((tcb: TestComponentBuilder, driver: MockAnimationDriver) => {
tcb = tcb.overrideTemplate(DummyIfCmp, `
<div @rotate="exp"></div>
<div @rotate="exp2"></div>
`);
tcb.overrideAnimations(DummyIfCmp, [
trigger("rotate", [
transition("start => *", [
style({'color': 'white'}),
animate(500,
style({'color': 'red'}))
]),
transition("start => end", [
style({'color': 'white'}),
animate(500,
style({'color': 'pink'}))
])
]),
]).createAsync(DummyIfCmp)
.then((fixture) => {
tick();
var cmp = fixture.debugElement.componentInstance;
cmp.exp = 'start';
cmp.exp2 = 'start';
fixture.detectChanges();
flushMicrotasks();
expect(driver.log.length).toEqual(0);
cmp.exp = 'something';
fixture.detectChanges();
flushMicrotasks();
expect(driver.log.length).toEqual(1);
var animation1 = driver.log[0];
var keyframes1 = animation1['keyframeLookup'];
var toStyles1 = keyframes1[1][1];
expect(toStyles1['color']).toEqual('red');
cmp.exp2 = 'end';
fixture.detectChanges();
flushMicrotasks();
expect(driver.log.length).toEqual(2);
var animation2 = driver.log[1];
var keyframes2 = animation2['keyframeLookup'];
var toStyles2 = keyframes2[1][1];
expect(toStyles2['color']).toEqual('red');
});
})));
it('should not remove the element until the void transition animation is complete',
inject([TestComponentBuilder, AnimationDriver], fakeAsync((tcb: TestComponentBuilder, driver: MockAnimationDriver) => {
makeAnimationCmp(tcb, '<div class="my-if" *ngIf="exp" @myAnimation></div>',
trigger('myAnimation', [
transition('* => void', [
animate(1000, style({'opacity': 0}))
])
]), (fixture) => {
tick();
var cmp = fixture.debugElement.componentInstance;
cmp.exp = true;
fixture.detectChanges();
flushMicrotasks();
cmp.exp = false;
fixture.detectChanges();
flushMicrotasks();
var player = driver.log[0]['player'];
var container = fixture.debugElement.nativeElement;
var ifElm = getDOM().querySelector(container, '.my-if');
expect(ifElm).toBeTruthy();
player.finish();
ifElm = getDOM().querySelector(container,'.my-if');
expect(ifElm).toBeFalsy();
});
})));
it('should fill an animation with the missing style values if not defined within an earlier style step',
inject([TestComponentBuilder, AnimationDriver], fakeAsync((tcb: TestComponentBuilder, driver: MockAnimationDriver) => {
makeAnimationCmp(tcb, '<div @myAnimation="exp"></div>',
trigger('myAnimation', [
transition('* => *', [
animate(1000, style({'opacity': 0})),
animate(1000, style({'opacity': 1}))
])
]), (fixture) => {
tick();
var cmp = fixture.debugElement.componentInstance;
cmp.exp = 'state1';
fixture.detectChanges();
flushMicrotasks();
var animation1 = driver.log[0];
var keyframes1 = animation1['keyframeLookup'];
expect(keyframes1[0]).toEqual([0, {'opacity': AUTO_STYLE}]);
expect(keyframes1[1]).toEqual([1, {'opacity': 0}]);
var animation2 = driver.log[1];
var keyframes2 = animation2['keyframeLookup'];
expect(keyframes2[0]).toEqual([0, {'opacity': 0}]);
expect(keyframes2[1]).toEqual([1, {'opacity': 1}]);
});
})));
it('should perform two transitions in parallel if defined in different state triggers',
inject([TestComponentBuilder, AnimationDriver], fakeAsync((tcb: TestComponentBuilder, driver: MockAnimationDriver) => {
makeAnimationCmp(tcb, '<div @one="exp" @two="exp2"></div>', [
trigger('one', [
transition('state1 => state2', [
style({'opacity': 0}),
animate(1000, style({'opacity': 1}))
])
]),
trigger('two', [
transition('state1 => state2', [
style({'width': 100}),
animate(1000, style({'width': 1000}))
])
])
], (fixture) => {
tick();
var cmp = fixture.debugElement.componentInstance;
cmp.exp = 'state1';
cmp.exp2 = 'state1';
fixture.detectChanges();
flushMicrotasks();
cmp.exp = 'state2';
fixture.detectChanges();
flushMicrotasks();
expect(driver.log.length).toEqual(1);
var count = 0;
var animation1 = driver.log[0];
var player1 = animation1['player'];
player1.onDone(() => count++);
expect(count).toEqual(0);
cmp.exp2 = 'state2';
fixture.detectChanges();
flushMicrotasks();
expect(driver.log.length).toEqual(2);
expect(count).toEqual(0);
var animation2 = driver.log[1];
var player2 = animation2['player'];
player2.onDone(() => count++);
expect(count).toEqual(0);
player1.finish();
expect(count).toEqual(1);
player2.finish();
expect(count).toEqual(2);
});
})));
});
describe('animation states', () => {
it('should retain the destination animation state styles once the animation is complete',
inject([TestComponentBuilder, AnimationDriver], fakeAsync((tcb: TestComponentBuilder, driver: MockAnimationDriver) => {
makeAnimationCmp(tcb, '<div class="target" @status="exp"></div>', [
trigger('status', [
state('final', style({ "top": '100px' })),
transition('* => final', [ animate(1000) ])
])
], (fixture) => {
tick();
var cmp = fixture.debugElement.componentInstance;
var node = getDOM().querySelector(fixture.debugElement.nativeElement, '.target');
cmp.exp = 'final';
fixture.detectChanges();
flushMicrotasks();
var animation = driver.log[0];
var player = animation['player'];
player.finish();
expect(getDOM().getStyle(node, 'top')).toEqual('100px');
});
})));
it('should seed in the origin animation state styles into the first animation step',
inject([TestComponentBuilder, AnimationDriver], fakeAsync((tcb: TestComponentBuilder, driver: MockAnimationDriver) => {
makeAnimationCmp(tcb, '<div class="target" @status="exp"></div>', [
trigger('status', [
state('void', style({ "height": '100px' })),
transition('* => *', [ animate(1000) ])
])
], (fixture) => {
tick();
var cmp = fixture.debugElement.componentInstance;
var node = getDOM().querySelector(fixture.debugElement.nativeElement, '.target');
cmp.exp = 'final';
fixture.detectChanges();
flushMicrotasks();
var animation = driver.log[0];
expect(animation['startingStyles']).toEqual({
"height": "100px"
});
});
})));
it('should perform a state change even if there is no transition that is found',
inject([TestComponentBuilder, AnimationDriver], fakeAsync((tcb: TestComponentBuilder, driver: MockAnimationDriver) => {
makeAnimationCmp(tcb, '<div class="target" @status="exp"></div>', [
trigger('status', [
state('void', style({ "width": '0px' })),
state('final', style({ "width": '100px' })),
])
], (fixture) => {
tick();
var cmp = fixture.debugElement.componentInstance;
var node = getDOM().querySelector(fixture.debugElement.nativeElement, '.target');
cmp.exp = 'final';
fixture.detectChanges();
flushMicrotasks();
expect(driver.log.length).toEqual(0);
flushMicrotasks();
expect(getDOM().getStyle(node, 'width')).toEqual('100px');
});
})));
it('should allow multiple states to be defined with the same styles',
inject([TestComponentBuilder, AnimationDriver], fakeAsync((tcb: TestComponentBuilder, driver: MockAnimationDriver) => {
makeAnimationCmp(tcb, '<div class="target" @status="exp"></div>', [
trigger('status', [
state('a, c', style({ "height": '100px' })),
state('b, d', style({ "width": '100px' })),
])
], (fixture) => {
tick();
var cmp = fixture.debugElement.componentInstance;
var node = getDOM().querySelector(fixture.debugElement.nativeElement, '.target');
cmp.exp = 'a';
fixture.detectChanges();
flushMicrotasks();
flushMicrotasks();
expect(getDOM().getStyle(node, 'height')).toEqual('100px');
expect(getDOM().getStyle(node, 'width')).not.toEqual('100px');
cmp.exp = 'b';
fixture.detectChanges();
flushMicrotasks();
flushMicrotasks();
expect(getDOM().getStyle(node, 'height')).not.toEqual('100px');
expect(getDOM().getStyle(node, 'width')).toEqual('100px');
cmp.exp = 'c';
fixture.detectChanges();
flushMicrotasks();
flushMicrotasks();
expect(getDOM().getStyle(node, 'height')).toEqual('100px');
expect(getDOM().getStyle(node, 'width')).not.toEqual('100px');
cmp.exp = 'd';
fixture.detectChanges();
flushMicrotasks();
flushMicrotasks();
expect(getDOM().getStyle(node, 'height')).not.toEqual('100px');
expect(getDOM().getStyle(node, 'width')).toEqual('100px');
cmp.exp = 'e';
fixture.detectChanges();
flushMicrotasks();
flushMicrotasks();
expect(getDOM().getStyle(node, 'height')).not.toEqual('100px');
expect(getDOM().getStyle(node, 'width')).not.toEqual('100px');
});
})));
it('should allow multiple transitions to be defined with the same sequence',
inject([TestComponentBuilder, AnimationDriver], fakeAsync((tcb: TestComponentBuilder, driver: MockAnimationDriver) => {
makeAnimationCmp(tcb, '<div class="target" @status="exp"></div>', [
trigger('status', [
transition('a => b, b => c', [
animate(1000)
]),
transition('* => *', [
animate(300)
])
])
], (fixture) => {
tick();
var cmp = fixture.debugElement.componentInstance;
var node = getDOM().querySelector(fixture.debugElement.nativeElement, '.target');
cmp.exp = 'a';
fixture.detectChanges();
flushMicrotasks();
expect(driver.log.pop()['duration']).toEqual(300);
cmp.exp = 'b';
fixture.detectChanges();
flushMicrotasks();
expect(driver.log.pop()['duration']).toEqual(1000);
cmp.exp = 'c';
fixture.detectChanges();
flushMicrotasks();
expect(driver.log.pop()['duration']).toEqual(1000);
cmp.exp = 'd';
fixture.detectChanges();
flushMicrotasks();
expect(driver.log.pop()['duration']).toEqual(300);
});
})));
it('should balance the animation with the origin/destination styles as keyframe animation properties',
inject([TestComponentBuilder, AnimationDriver], fakeAsync((tcb: TestComponentBuilder, driver: MockAnimationDriver) => {
makeAnimationCmp(tcb, '<div class="target" @status="exp"></div>', [
trigger('status', [
state('void', style({ "height": "100px", "opacity":0 })),
state('final', style({ "height": "333px", "width":"200px" })),
transition('void => final', [
animate(1000)
])
])
], (fixture) => {
tick();
var cmp = fixture.debugElement.componentInstance;
var node = getDOM().querySelector(fixture.debugElement.nativeElement, '.target');
cmp.exp = 'final';
fixture.detectChanges();
flushMicrotasks();
var animation = driver.log.pop();
var keyframes = animation['keyframeLookup'];
expect(keyframes[0]).toEqual(
[0, { "height": "100px",
"opacity": 0,
"width": AUTO_STYLE }]);
expect(keyframes[1]).toEqual(
[1, { "height": "333px",
"opacity": AUTO_STYLE,
"width": "200px" }]);
});
})));
});
});
}
@Component({
selector: 'if-cmp',
directives: [NgIf],
template: `
<div *ngIf="exp" @myAnimation="exp"></div>
`
})
class DummyIfCmp {
exp = false;
exp2 = false;
}

View File

@ -0,0 +1,41 @@
import {
AsyncTestCompleter,
beforeEach,
ddescribe,
xdescribe,
describe,
expect,
iit,
inject,
it,
xit
} from '../../testing/testing_internal';
import {
fakeAsync,
flushMicrotasks
} from '../../testing';
import {NoOpAnimationPlayer, AnimationPlayer} from '../../src/animation/animation_player';
export function main() {
describe('NoOpAnimationPlayer', function() {
it('should call onDone after the next microtask when constructed', fakeAsync(() => {
var player = new NoOpAnimationPlayer();
var completed = false;
player.onDone(() => completed = true);
expect(completed).toEqual(false);
flushMicrotasks();
expect(completed).toEqual(true);
}));
it('should be able to run each of the player methods', fakeAsync(() => {
var player = new NoOpAnimationPlayer();
player.pause();
player.play();
player.finish();
player.restart();
player.destroy();
}));
});
}

View File

@ -0,0 +1,216 @@
import {
AsyncTestCompleter,
beforeEach,
ddescribe,
xdescribe,
describe,
expect,
iit,
inject,
it,
xit
} from '../../testing/testing_internal';
import {
fakeAsync,
flushMicrotasks
} from '../../testing';
import {isPresent} from '../../src/facade/lang';
import {AnimationSequencePlayer} from '../../src/animation/animation_sequence_player';
import {MockAnimationPlayer} from '../../testing/animation/mock_animation_player';
export function main() {
describe('AnimationSequencePlayer', function() {
var players;
beforeEach(() => {
players = [
new MockAnimationPlayer(),
new MockAnimationPlayer(),
new MockAnimationPlayer(),
];
});
var assertLastStatus =
(player: MockAnimationPlayer, status: string, match: boolean, iOffset: number = 0) => {
var index = player.log.length - 1 + iOffset;
var actual = player.log.length > 0 ? player.log[index] : null;
if (match) {
expect(actual).toEqual(status);
} else {
expect(actual).not.toEqual(status);
}
}
var assertPlaying = (player: MockAnimationPlayer, isPlaying: boolean) => {
assertLastStatus(player, 'play', isPlaying);
};
it('should pause/play the active player', () => {
var sequence = new AnimationSequencePlayer(players);
assertPlaying(players[0], false);
assertPlaying(players[1], false);
assertPlaying(players[2], false);
sequence.play();
assertPlaying(players[0], true);
assertPlaying(players[1], false);
assertPlaying(players[2], false);
sequence.pause();
assertPlaying(players[0], false);
assertPlaying(players[1], false);
assertPlaying(players[2], false);
sequence.play();
players[0].finish();
assertPlaying(players[0], false);
assertPlaying(players[1], true);
assertPlaying(players[2], false);
players[1].finish();
assertPlaying(players[0], false);
assertPlaying(players[1], false);
assertPlaying(players[2], true);
players[2].finish();
sequence.pause();
assertPlaying(players[0], false);
assertPlaying(players[1], false);
assertPlaying(players[2], false);
});
it('should finish when all players have finished', () => {
var sequence = new AnimationSequencePlayer(players);
var completed = false;
sequence.onDone(() => completed = true);
sequence.play();
expect(completed).toBeFalsy();
players[0].finish();
expect(completed).toBeFalsy();
players[1].finish();
expect(completed).toBeFalsy();
players[2].finish();
expect(completed).toBeTruthy();
});
it('should restart all the players', () => {
var sequence = new AnimationSequencePlayer(players);
sequence.play();
assertPlaying(players[0], true);
assertPlaying(players[1], false);
assertPlaying(players[2], false);
players[0].finish();
assertPlaying(players[0], false);
assertPlaying(players[1], true);
assertPlaying(players[2], false);
sequence.restart();
assertLastStatus(players[0], 'restart', true);
assertLastStatus(players[1], 'reset', true);
assertLastStatus(players[2], 'reset', true);
});
it('should finish all the players', () => {
var sequence = new AnimationSequencePlayer(players);
var completed = false;
sequence.onDone(() => completed = true);
sequence.play();
assertLastStatus(players[0], 'finish', false);
assertLastStatus(players[1], 'finish', false);
assertLastStatus(players[2], 'finish', false);
sequence.finish();
assertLastStatus(players[0], 'finish', true, -1);
assertLastStatus(players[1], 'finish', true, -1);
assertLastStatus(players[2], 'finish', true, -1);
assertLastStatus(players[0], 'destroy', true);
assertLastStatus(players[1], 'destroy', true);
assertLastStatus(players[2], 'destroy', true);
expect(completed).toBeTruthy();
});
it('should call destroy automatically when finished if no parent player is present', () => {
var sequence = new AnimationSequencePlayer(players);
sequence.play();
assertLastStatus(players[0], 'destroy', false);
assertLastStatus(players[1], 'destroy', false);
assertLastStatus(players[2], 'destroy', false);
sequence.finish();
assertLastStatus(players[0], 'destroy', true);
assertLastStatus(players[1], 'destroy', true);
assertLastStatus(players[2], 'destroy', true);
});
it('should not call destroy automatically when finished if a parent player is present', () => {
var sequence = new AnimationSequencePlayer(players);
var parent = new AnimationSequencePlayer([sequence, new MockAnimationPlayer()]);
sequence.play();
assertLastStatus(players[0], 'destroy', false);
assertLastStatus(players[1], 'destroy', false);
assertLastStatus(players[2], 'destroy', false);
sequence.finish();
assertLastStatus(players[0], 'destroy', false);
assertLastStatus(players[1], 'destroy', false);
assertLastStatus(players[2], 'destroy', false);
parent.finish();
assertLastStatus(players[0], 'destroy', true);
assertLastStatus(players[1], 'destroy', true);
assertLastStatus(players[2], 'destroy', true);
});
it('should function without any players', () => {
var sequence = new AnimationSequencePlayer([]);
sequence.onDone(() => {});
sequence.pause();
sequence.play();
sequence.finish();
sequence.restart();
sequence.destroy();
});
it('should call onDone after the next microtask if no players are provided', fakeAsync(() => {
var sequence = new AnimationSequencePlayer([]);
var completed = false;
sequence.onDone(() => completed = true);
expect(completed).toEqual(false);
flushMicrotasks();
expect(completed).toEqual(true);
}));
});
}

View File

@ -0,0 +1,195 @@
import {
AsyncTestCompleter,
beforeEach,
ddescribe,
xdescribe,
describe,
expect,
iit,
inject,
it,
xit
} from '../../testing/testing_internal';
import {
fakeAsync,
flushMicrotasks
} from '../../testing';
import {el} from '@angular/platform-browser/testing';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {isPresent} from '../../src/facade/lang';
import {MockAnimationPlayer} from '../../testing/animation/mock_animation_player';
import {AnimationStyleUtil} from '../../src/animation/animation_style_util';
import {AnimationKeyframe} from '../../src/animation/animation_keyframe';
import {AnimationStyles} from '../../src/animation/animation_styles';
import {FILL_STYLE_FLAG} from '../../src/animation/animation_constants';
import {AUTO_STYLE} from '../../src/animation/metadata';
export function main() {
describe('AnimationStyleUtil', function() {
describe('balanceStyles', () => {
it('should set all non-shared styles to the provided null value between the two sets of styles', () => {
var styles = { opacity: 0, color: 'red' };
var newStyles = { background: 'red' };
var flag = '*';
var result = AnimationStyleUtil.balanceStyles(styles, newStyles, flag);
expect(result).toEqual({
opacity:flag,
color:flag,
background:'red'
})
});
it('should handle an empty set of styles', () => {
var value = '*';
expect(AnimationStyleUtil.balanceStyles({}, { opacity: 0 }, value)).toEqual({
opacity: 0
});
expect(AnimationStyleUtil.balanceStyles({ opacity: 0 }, {}, value)).toEqual({
opacity: value
});
});
});
describe('balanceKeyframes', () => {
it('should balance both the starting and final keyframes with thep provided styles', () => {
var collectedStyles = {
width: 100,
height: 200
};
var finalStyles = {
background: 'red',
border: '1px solid black'
};
var keyframes = [
new AnimationKeyframe(0, new AnimationStyles([{ height: 100, opacity: 1 }])),
new AnimationKeyframe(1, new AnimationStyles([{ background: 'blue', left: '100px', top: '100px' }]))
];
var result = AnimationStyleUtil.balanceKeyframes(collectedStyles, finalStyles, keyframes);
expect(AnimationStyleUtil.flattenStyles(result[0].styles.styles)).toEqual({
"width": 100,
"height": 100,
"opacity": 1,
"background": '*',
"border": '*',
"left": '*',
"top": '*'
});
expect(AnimationStyleUtil.flattenStyles(result[1].styles.styles)).toEqual({
"width": '*',
"height": '*',
"opacity": '*',
"background": 'blue',
"border": '1px solid black',
"left": '100px',
"top": '100px'
});
});
it('should perform balancing when no collected and final styles are provided', () => {
var keyframes = [
new AnimationKeyframe(0, new AnimationStyles([{ height: 100, opacity: 1 }])),
new AnimationKeyframe(1, new AnimationStyles([{ width: 100 }]))
];
var result = AnimationStyleUtil.balanceKeyframes({}, {}, keyframes);
expect(AnimationStyleUtil.flattenStyles(result[0].styles.styles)).toEqual({
"height": 100,
"opacity": 1,
"width": "*"
});
expect(AnimationStyleUtil.flattenStyles(result[1].styles.styles)).toEqual({
"width": 100,
"height": "*",
"opacity": "*"
});
});
});
describe('clearStyles', () => {
it('should set all the style values to "null"', () => {
var styles = {
"opacity": 0,
"width": 100,
"color": "red"
};
var expectedResult = {
"opacity": null,
"width": null,
"color": null
};
expect(AnimationStyleUtil.clearStyles(styles)).toEqual(expectedResult);
});
it('should handle an empty set of styles', () => {
expect(AnimationStyleUtil.clearStyles({})).toEqual({});
});
});
describe('collectAndResolveStyles', () => {
it('should keep a record of the styles as they are called', () => {
var styles1 = [{
"opacity": 0,
"width": 100
}];
var styles2 = [{
"height": 999,
"opacity": 1
}];
var collection: {[key: string]: string|number} = {};
expect(AnimationStyleUtil.collectAndResolveStyles(collection, styles1)).toEqual(styles1);
expect(collection).toEqual({
"opacity": 0,
"width": 100
});
expect(AnimationStyleUtil.collectAndResolveStyles(collection, styles2)).toEqual(styles2);
expect(collection).toEqual({
"opacity": 1,
"width": 100,
"height": 999
});
});
it('should resolve styles if they contain a FILL_STYLE_FLAG value', () => {
var styles1 = [{
"opacity": 0,
"width": FILL_STYLE_FLAG
}];
var styles2 = [{
"height": 999,
"opacity": FILL_STYLE_FLAG
}];
var collection = {};
expect(AnimationStyleUtil.collectAndResolveStyles(collection, styles1)).toEqual([{
"opacity": 0,
"width": AUTO_STYLE
}]);
expect(AnimationStyleUtil.collectAndResolveStyles(collection, styles2)).toEqual([{
"opacity": 0,
"height": 999
}]);
});
});
});
}

View File

@ -0,0 +1,35 @@
import {AnimationDriver} from '../../src/animation/animation_driver';
import {AnimationKeyframe} from '../../src/animation/animation_keyframe';
import {AnimationPlayer} from '../../src/animation/animation_player';
import {StringMapWrapper} from '../../src/facade/collection';
import {AnimationStyles} from '../../src/animation/animation_styles';
import {MockAnimationPlayer} from '../../testing/animation/mock_animation_player';
export class MockAnimationDriver extends AnimationDriver {
log = [];
animate(element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], duration: number, delay: number,
easing: string): AnimationPlayer {
var player = new MockAnimationPlayer();
this.log.push({
'element': element,
'startingStyles': _serializeStyles(startingStyles),
'keyframes': keyframes,
'keyframeLookup': _serializeKeyframes(keyframes),
'duration': duration,
'delay': delay,
'easing': easing,
'player': player
});
return player;
}
}
function _serializeKeyframes(keyframes: AnimationKeyframe[]): any[] {
return keyframes.map(keyframe => [keyframe.offset, _serializeStyles(keyframe.styles)]);
}
function _serializeStyles(styles: AnimationStyles): {[key: string]: any} {
var flatStyles = {};
styles.styles.forEach(entry => StringMapWrapper.forEach(entry, (val, prop) => { flatStyles[prop] = val; }));
return flatStyles;
}

View File

@ -0,0 +1,47 @@
import {isPresent} from '../../src/facade/lang';
import {AnimationPlayer} from '../../src/animation/animation_player';
export class MockAnimationPlayer implements AnimationPlayer {
private _subscriptions = [];
private _finished = false;
private _destroyed = false;
public parentPlayer: AnimationPlayer = null;
public log = [];
private _onfinish(): void {
if (!this._finished) {
this._finished = true;
this.log.push('finish');
this._subscriptions.forEach((entry) => { entry(); });
this._subscriptions = [];
if (!isPresent(this.parentPlayer)) {
this.destroy();
}
}
}
onDone(fn: Function): void { this._subscriptions.push(fn); }
play(): void { this.log.push('play'); }
pause(): void { this.log.push('pause'); }
restart(): void { this.log.push('restart'); }
finish(): void { this._onfinish(); }
reset(): void { this.log.push('reset'); }
destroy(): void {
if (!this._destroyed) {
this._destroyed = true;
this.finish();
this.log.push('destroy');
}
}
setPosition(p): void {}
getPosition(): number { return 0; }
}