refactor(animations): support browser animation rendering (#14578)

This commit is contained in:
Matias Niemelä 2017-02-22 15:14:49 -08:00 committed by Igor Minar
parent 88755b0dae
commit 830393d234
88 changed files with 3429 additions and 1225 deletions

View File

@ -7,6 +7,7 @@ cd `dirname $0`
PACKAGES=(core
compiler
common
animations
forms
platform-browser
platform-browser-dynamic
@ -14,7 +15,6 @@ PACKAGES=(core
platform-server
platform-webworker
platform-webworker-dynamic
animation
upgrade
router
compiler-cli
@ -161,6 +161,10 @@ do
JS_STATIC_PATH_ES5=${DEST_MODULE}/${PACKAGE}/static.es5.js
JS_UPGRADE_PATH=${DEST_MODULE}/${PACKAGE}/upgrade.js
JS_UPGRADE_PATH_ES5=${DEST_MODULE}/${PACKAGE}/upgrade.es5.js
JS_ANIMATIONS_PATH=${DEST_MODULE}/${PACKAGE}/animations.js
JS_ANIMATIONS_PATH_ES5=${DEST_MODULE}/${PACKAGE}/animations.es5.js
JS_ANIMATIONS_TESTING_PATH=${DEST_MODULE}/${PACKAGE}/animations/testing.js
JS_ANIMATIONS_TESTING_PATH_ES5=${DEST_MODULE}/${PACKAGE}/animations/testing.es5.js
# UMD/ES5
UMD_ES5_PATH=${DEST_BUNDLES}/${PACKAGE}.umd.js
@ -170,6 +174,9 @@ do
UMD_ES5_MIN_PATH=${DEST_BUNDLES}/${PACKAGE}.umd.min.js
UMD_STATIC_ES5_MIN_PATH=${DEST_BUNDLES}/${PACKAGE}-static.umd.min.js
UMD_UPGRADE_ES5_MIN_PATH=${DEST_BUNDLES}/${PACKAGE}-upgrade.umd.min.js
UMD_ANIMATIONS_ES5_PATH=${DEST_BUNDLES}/${PACKAGE}-animations.umd.js
UMD_ANIMATIONS_ES5_MIN_PATH=${DEST_BUNDLES}/${PACKAGE}-animations.umd.min.js
UMD_ANIMATIONS_TESTING_ES5_PATH=${DEST_BUNDLES}/${PACKAGE}-animations-testing.umd.js
if [[ ${PACKAGE} != router ]]; then
LICENSE_BANNER=${PWD}/modules/@angular/license-banner.txt
@ -218,6 +225,16 @@ do
$TSC -p ${SRCDIR}/tsconfig-testing.json
fi
if [[ -e ${SRCDIR}/tsconfig-animations.json ]]; then
echo "====== [${PACKAGE}]: COMPILING (ANIMATIONS): ${TSC} -p ${SRCDIR}/tsconfig-animations.json"
$TSC -p ${SRCDIR}/tsconfig-animations.json
if [[ -e ${SRCDIR}/tsconfig-animations-testing.json ]]; then
echo "====== [${PACKAGE}]: COMPILING (ANIMATION TESTING): ${TSC} -p ${SRCDIR}/tsconfig-animations-testing.json"
$TSC -p ${SRCDIR}/tsconfig-animations-testing.json
fi
fi
if [[ -e ${SRCDIR}/tsconfig-static.json ]]; then
echo "====== [${PACKAGE}]: COMPILING (STATIC): ${TSC} -p ${SRCDIR}/tsconfig-static.json"
$TSC -p ${SRCDIR}/tsconfig-static.json
@ -357,6 +374,58 @@ do
mv ${UMD_UPGRADE_ES5_PATH}.tmp ${UMD_UPGRADE_ES5_PATH}
$UGLIFYJS -c --screw-ie8 --comments -o ${UMD_UPGRADE_ES5_MIN_PATH} ${UMD_UPGRADE_ES5_PATH}
fi
if [[ -d animations ]]; then
echo "====== Rollup ${PACKAGE} animations"
../../../node_modules/.bin/rollup -i ${DESTDIR}/animations/index.js -o ${DESTDIR}/animations.tmp.js
echo "====== Downleveling ${PACKAGE} ANIMATIONS to ES5/UMD"
[[ -e ${SRCDIR}/.babelrc-animations ]] && cp ${SRCDIR}/.babelrc-animations ${DESTDIR}/.babelrc
$BABELJS ${DESTDIR}/animations.tmp.js -o ${UMD_ANIMATIONS_ES5_PATH}
rm -f ${DESTDIR}/.babelrc
echo "====== Move ${PACKAGE} animations typings"
rsync -a --exclude=*.js --exclude=*.js.map ${DESTDIR}/animations/ ${DESTDIR}/typings/animations
mv ${DESTDIR}/typings/animations/index.d.ts ${DESTDIR}/typings/animations/animations.d.ts
mv ${DESTDIR}/typings/animations/index.metadata.json ${DESTDIR}/typings/animations/animations.metadata.json
echo "====== Rollup ${PACKAGE} animations/testing"
../../../node_modules/.bin/rollup -i ${DESTDIR}/animations/testing/index.js -o ${DESTDIR}/animations-testing.tmp.js
echo "====== Downleveling ${PACKAGE} ANIMATIONS TESTING to ES5/UMD"
[[ -e ${SRCDIR}/.babelrc-animations-testing ]] && cp ${SRCDIR}/.babelrc-animations-testing ${DESTDIR}/.babelrc
$BABELJS ${DESTDIR}/animations-testing.tmp.js -o ${UMD_ANIMATIONS_TESTING_ES5_PATH}
rm -f ${DESTDIR}/.babelrc
echo "====== Move ${PACKAGE} animations testing typings"
rsync -a --exclude=*.js --exclude=*.js.map ${DESTDIR}/animations/testing/ ${DESTDIR}/typings/animations/testing
mv ${DESTDIR}/typings/animations/testing/index.d.ts ${DESTDIR}/typings/animations/testing/testing.d.ts
mv ${DESTDIR}/typings/animations/testing/index.metadata.json ${DESTDIR}/typings/animations/testing/testing.metadata.json
rm -rf ${DESTDIR}/animations
mkdir ${DESTDIR}/animations && [[ -d ${DEST_MODULE}/${PACKAGE} ]] || mkdir ${DEST_MODULE}/${PACKAGE}
mkdir ${DESTDIR}/animations/testing
getPackageContents "${PACKAGE}" "animations" > ${DESTDIR}/animations/package.json
echo '{"typings": "../../typings/animations/testing/testing.d.ts", "main": "../../bundles/platform-browser-animations-testing.umd.js", "module": "../../@angular/platform-browser/animations/testing.es5.js", "es2015": "../../@angular/platform-browser/animations/testing.js"}' > ${DESTDIR}/animations/testing/package.json
mv ${DESTDIR}/animations.tmp.js ${JS_ANIMATIONS_PATH}
$BABELJS ${JS_ANIMATIONS_PATH} -o ${JS_ANIMATIONS_PATH_ES5}
cat ${LICENSE_BANNER} > ${UMD_ANIMATIONS_ES5_PATH}.tmp
cat ${UMD_ANIMATIONS_ES5_PATH} >> ${UMD_ANIMATIONS_ES5_PATH}.tmp
mv ${UMD_ANIMATIONS_ES5_PATH}.tmp ${UMD_ANIMATIONS_ES5_PATH}
$UGLIFYJS -c --screw-ie8 --comments -o ${UMD_ANIMATIONS_ES5_MIN_PATH} ${UMD_ANIMATIONS_ES5_PATH}
mkdir ${DEST_MODULE}/${PACKAGE}/animations
mv ${DESTDIR}/animations-testing.tmp.js ${JS_ANIMATIONS_TESTING_PATH}
$BABELJS ${JS_ANIMATIONS_TESTING_PATH} -o ${JS_ANIMATIONS_TESTING_PATH_ES5}
cat ${LICENSE_BANNER} > ${UMD_ANIMATIONS_TESTING_ES5_PATH}.tmp
cat ${UMD_ANIMATIONS_TESTING_ES5_PATH} >> ${UMD_ANIMATIONS_TESTING_ES5_PATH}.tmp
mv ${UMD_ANIMATIONS_TESTING_ES5_PATH}.tmp ${UMD_ANIMATIONS_TESTING_ES5_PATH}
fi
) 2>&1 | grep -v "as external dependency"
fi

View File

@ -36,4 +36,4 @@ gulp.task('tools:build', loadTask('tools-build'));
gulp.task('check-cycle', loadTask('check-cycle'));
gulp.task('serve', loadTask('serve', 'default'));
gulp.task('serve-examples', loadTask('serve', 'examples'));
gulp.task('changelog', loadTask('changelog'));
gulp.task('changelog', loadTask('changelog'));

View File

@ -1,18 +0,0 @@
{
"name": "@angular/animation",
"version": "0.0.0-PLACEHOLDER",
"description": "Angular - animation integration with web-animations",
"main": "./bundles/animation.umd.js",
"module": "./@angular/animation.es5.js",
"es2015": "./@angular/animation.js",
"typings": "./typings/animation.d.ts",
"author": "angular",
"license": "MIT",
"peerDependencies": {
"@angular/core": "0.0.0-PLACEHOLDER"
},
"repository": {
"type": "git",
"url": "https://github.com/angular/angular.git"
}
}

View File

@ -1,11 +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
*/
export {AnimationModule} from './animation_module';
export {Animation} from './dsl/animation';
export {AUTO_STYLE, AnimationGroupMetadata, AnimationKeyframesSequenceMetadata, AnimationSequenceMetadata, AnimationStateMetadata, AnimationStyleMetadata, AnimationTransitionMetadata, animate, group, keyframes, sequence, state, style, transition} from './dsl/animation_metadata';
export {AnimationTrigger, trigger} from './dsl/animation_trigger';

View File

@ -1,35 +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 {NgModule, ɵTransitionEngine} from '@angular/core';
import {AnimationStyleNormalizer} from './dsl/style_normalization/animation_style_normalizer';
import {WebAnimationsStyleNormalizer} from './dsl/style_normalization/web_animations_style_normalizer';
import {AnimationDriver, NoOpAnimationDriver} from './engine/animation_driver';
import {DomAnimationTransitionEngine} from './engine/dom_animation_transition_engine';
import {WebAnimationsDriver, supportsWebAnimations} from './engine/web_animations/web_animations_driver';
export function resolveDefaultAnimationDriver(): AnimationDriver {
if (supportsWebAnimations()) {
return new WebAnimationsDriver();
}
return new NoOpAnimationDriver();
}
/**
* The module that includes all animation code such as `style()`, `animate()`, `trigger()`, etc...
*
* @experimental
*/
@NgModule({
providers: [
{provide: AnimationDriver, useFactory: resolveDefaultAnimationDriver},
{provide: AnimationStyleNormalizer, useClass: WebAnimationsStyleNormalizer},
{provide: ɵTransitionEngine, useClass: DomAnimationTransitionEngine}
]
})
export class AnimationModule {
}

View File

@ -1,40 +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 * as meta from './animation_metadata';
export interface AnimationDslVisitor {
visitState(ast: meta.AnimationStateMetadata, context: any): any;
visitTransition(ast: meta.AnimationTransitionMetadata, context: any): any;
visitSequence(ast: meta.AnimationSequenceMetadata, context: any): any;
visitGroup(ast: meta.AnimationGroupMetadata, context: any): any;
visitAnimate(ast: meta.AnimationAnimateMetadata, context: any): any;
visitStyle(ast: meta.AnimationStyleMetadata, context: any): any;
visitKeyframeSequence(ast: meta.AnimationKeyframesSequenceMetadata, context: any): any;
}
export function visitAnimationNode(
visitor: AnimationDslVisitor, node: meta.AnimationMetadata, context: any) {
switch (node.type) {
case meta.AnimationMetadataType.State:
return visitor.visitState(<meta.AnimationStateMetadata>node, context);
case meta.AnimationMetadataType.Transition:
return visitor.visitTransition(<meta.AnimationTransitionMetadata>node, context);
case meta.AnimationMetadataType.Sequence:
return visitor.visitSequence(<meta.AnimationSequenceMetadata>node, context);
case meta.AnimationMetadataType.Group:
return visitor.visitGroup(<meta.AnimationGroupMetadata>node, context);
case meta.AnimationMetadataType.Animate:
return visitor.visitAnimate(<meta.AnimationAnimateMetadata>node, context);
case meta.AnimationMetadataType.KeyframeSequence:
return visitor.visitKeyframeSequence(<meta.AnimationKeyframesSequenceMetadata>node, context);
case meta.AnimationMetadataType.Style:
return visitor.visitStyle(<meta.AnimationStyleMetadata>node, context);
default:
throw new Error(`Unable to resolve animation metadata node #${node.type}`);
}
}

View File

@ -1,31 +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, ɵNoOpAnimationPlayer} from '@angular/core';
import {StyleData} from '../common/style_data';
/**
* @experimental
*/
export class NoOpAnimationDriver implements AnimationDriver {
animate(
element: any, keyframes: StyleData[], duration: number, delay: number, easing: string,
previousPlayers: AnimationPlayer[] = []): AnimationPlayer {
return new ɵNoOpAnimationPlayer();
}
}
/**
* @experimental
*/
export abstract class AnimationDriver {
static NOOP: AnimationDriver = new NoOpAnimationDriver();
abstract animate(
element: any, keyframes: StyleData[], duration: number, delay: number, easing: string,
previousPlayers?: AnimationPlayer[]): AnimationPlayer;
}

View File

@ -1,235 +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, Injectable, ɵAnimationGroupPlayer, ɵNoOpAnimationPlayer, ɵTransitionEngine} from '@angular/core';
import {StyleData} from '../common/style_data';
import {AnimationTimelineInstruction} from '../dsl/animation_timeline_instruction';
import {AnimationTransitionInstruction} from '../dsl/animation_transition_instruction';
import {AnimationStyleNormalizer} from '../dsl/style_normalization/animation_style_normalizer';
import {AnimationDriver} from './animation_driver';
import {AnimationEngineInstruction, AnimationTransitionInstructionType} from './animation_engine_instruction';
export declare type AnimationPlayerTuple = {
element: any; player: AnimationPlayer;
};
@Injectable()
export class DomAnimationTransitionEngine extends ɵTransitionEngine {
private _flaggedInserts = new Set<any>();
private _queuedRemovals: any[] = [];
private _queuedAnimations: AnimationPlayerTuple[] = [];
private _activeElementAnimations = new Map<any, AnimationPlayer[]>();
private _activeTransitionAnimations = new Map<any, {[triggerName: string]: AnimationPlayer}>();
constructor(private _driver: AnimationDriver, private _normalizer: AnimationStyleNormalizer) {
super();
}
insertNode(container: any, element: any) {
container.appendChild(element);
this._flaggedInserts.add(element);
}
removeNode(element: any) { this._queuedRemovals.push(element); }
process(element: any, instructions: AnimationEngineInstruction[]): AnimationPlayer {
const players = instructions.map(instruction => {
if (instruction.type == AnimationTransitionInstructionType.TransitionAnimation) {
return this._handleTransitionAnimation(
element, <AnimationTransitionInstruction>instruction);
}
if (instruction.type == AnimationTransitionInstructionType.TimelineAnimation) {
return this._handleTimelineAnimation(
element, <AnimationTimelineInstruction>instruction, []);
}
return new ɵNoOpAnimationPlayer();
});
return optimizeGroupPlayer(players);
}
private _handleTransitionAnimation(element: any, instruction: AnimationTransitionInstruction):
AnimationPlayer {
const triggerName = instruction.triggerName;
const elmTransitionMap = getOrSetAsInMap(this._activeTransitionAnimations, element, {});
let previousPlayers: AnimationPlayer[];
if (instruction.isRemovalTransition) {
// we make a copy of the array because the actual source array is modified
// each time a player is finished/destroyed (the forEach loop would fail otherwise)
previousPlayers = copyArray(this._activeElementAnimations.get(element));
} else {
previousPlayers = [];
const existingPlayer = elmTransitionMap[triggerName];
if (existingPlayer) {
previousPlayers.push(existingPlayer);
}
}
// it's important to do this step before destroying the players
// so that the onDone callback below won't fire before this
eraseStyles(element, instruction.fromStyles);
// we first run this so that the previous animation player
// data can be passed into the successive animation players
const players = instruction.timelines.map(
timelineInstruction => this._buildPlayer(element, timelineInstruction, previousPlayers));
previousPlayers.forEach(previousPlayer => previousPlayer.destroy());
const player = optimizeGroupPlayer(players);
player.onDone(() => {
player.destroy();
const elmTransitionMap = this._activeTransitionAnimations.get(element);
if (elmTransitionMap) {
delete elmTransitionMap[triggerName];
if (Object.keys(elmTransitionMap).length == 0) {
this._activeTransitionAnimations.delete(element);
}
}
deleteFromArrayMap(this._activeElementAnimations, element, player);
setStyles(element, instruction.toStyles);
});
this._queuePlayer(element, player);
elmTransitionMap[triggerName] = player;
return player;
}
private _handleTimelineAnimation(
element: any, instruction: AnimationTimelineInstruction,
previousPlayers: AnimationPlayer[]): AnimationPlayer {
const player = this._buildPlayer(element, instruction, previousPlayers);
player.onDestroy(() => { deleteFromArrayMap(this._activeElementAnimations, element, player); });
this._queuePlayer(element, player);
return player;
}
private _buildPlayer(
element: any, instruction: AnimationTimelineInstruction,
previousPlayers: AnimationPlayer[]): AnimationPlayer {
return this._driver.animate(
element, this._normalizeKeyframes(instruction.keyframes), instruction.duration,
instruction.delay, instruction.easing, previousPlayers);
}
private _normalizeKeyframes(keyframes: StyleData[]): StyleData[] {
const errors: string[] = [];
const normalizedKeyframes: StyleData[] = [];
keyframes.forEach(kf => {
const normalizedKeyframe: StyleData = {};
Object.keys(kf).forEach(prop => {
let normalizedProp = prop;
let normalizedValue = kf[prop];
if (prop != 'offset') {
normalizedProp = this._normalizer.normalizePropertyName(prop, errors);
normalizedValue =
this._normalizer.normalizeStyleValue(prop, normalizedProp, kf[prop], errors);
}
normalizedKeyframe[normalizedProp] = normalizedValue;
});
normalizedKeyframes.push(normalizedKeyframe);
});
if (errors.length) {
const LINE_START = '\n - ';
throw new Error(
`Unable to animate due to the following errors:${LINE_START}${errors.join(LINE_START)}`);
}
return normalizedKeyframes;
}
private _queuePlayer(element: any, player: AnimationPlayer) {
const tuple = <AnimationPlayerTuple>{element, player};
this._queuedAnimations.push(tuple);
player.init();
const elementAnimations = getOrSetAsInMap(this._activeElementAnimations, element, []);
elementAnimations.push(player);
}
triggerAnimations() {
while (this._queuedAnimations.length) {
const {player, element} = this._queuedAnimations.shift();
// in the event that an animation throws an error then we do
// not want to re-run animations on any previous animations
// if they have already been kicked off beforehand
if (!player.hasStarted()) {
player.play();
}
}
this._queuedRemovals.forEach(element => {
if (this._flaggedInserts.has(element)) return;
let parent = element;
let players: AnimationPlayer[];
while (parent = parent.parentNode) {
const match = this._activeElementAnimations.get(parent);
if (match) {
players = match;
break;
}
}
if (players) {
optimizeGroupPlayer(players).onDone(() => remove(element));
} else {
if (element.parentNode) {
remove(element);
}
}
});
this._queuedRemovals = [];
this._flaggedInserts.clear();
}
}
function getOrSetAsInMap(map: Map<any, any>, key: any, defaultValue: any) {
let value = map.get(key);
if (!value) {
map.set(key, value = defaultValue);
}
return value;
}
function deleteFromArrayMap(map: Map<any, any[]>, key: any, value: any) {
let arr = map.get(key);
if (arr) {
const index = arr.indexOf(value);
if (index >= 0) {
arr.splice(index, 1);
if (arr.length == 0) {
map.delete(key);
}
}
}
}
function setStyles(element: any, styles: StyleData) {
Object.keys(styles).forEach(prop => { element.style[prop] = styles[prop]; });
}
function eraseStyles(element: any, styles: StyleData) {
Object.keys(styles).forEach(prop => {
// IE requires '' instead of null
// see https://github.com/angular/angular/issues/7916
element.style[prop] = '';
});
}
function optimizeGroupPlayer(players: AnimationPlayer[]): AnimationPlayer {
return players.length == 1 ? players[0] : new ɵAnimationGroupPlayer(players);
}
function copyArray(source: any[]): any[] {
return source ? source.splice(0) : [];
}
function remove(element: any) {
element.parentNode.removeChild(element);
}

View File

@ -1,470 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ɵNoOpAnimationPlayer} from '@angular/core';
import {el} from '@angular/platform-browser/testing/browser_util';
import {animate, keyframes, state, style, transition} from '../../src/dsl/animation_metadata';
import {buildAnimationKeyframes} from '../../src/dsl/animation_timeline_visitor';
import {trigger} from '../../src/dsl/animation_trigger';
import {AnimationStyleNormalizer, NoOpAnimationStyleNormalizer} from '../../src/dsl/style_normalization/animation_style_normalizer';
import {AnimationEngineInstruction} from '../../src/engine/animation_engine_instruction';
import {DomAnimationTransitionEngine} from '../../src/engine/dom_animation_transition_engine';
import {MockAnimationDriver, MockAnimationPlayer} from '../../testing/mock_animation_driver';
export function main() {
const driver = new MockAnimationDriver();
// these tests are only mean't to be run within the DOM
if (typeof Element == 'undefined') return;
describe('AnimationEngine', () => {
let element: any;
beforeEach(() => {
MockAnimationDriver.log = [];
element = el('<div></div>');
});
function makeEngine(normalizer: AnimationStyleNormalizer = null) {
return new DomAnimationTransitionEngine(
driver, normalizer || new NoOpAnimationStyleNormalizer());
}
describe('instructions', () => {
it('should animate a transition instruction', () => {
const engine = makeEngine();
const trig = trigger('something', [
state('on', style({height: 100})), state('off', style({height: 0})),
transition('on => off', animate(9876))
]);
const instruction = trig.matchTransition('on', 'off');
expect(MockAnimationDriver.log.length).toEqual(0);
engine.process(element, [instruction]);
expect(MockAnimationDriver.log.length).toEqual(1);
});
it('should animate a timeline instruction', () => {
const engine = makeEngine();
const timelines =
buildAnimationKeyframes([style({height: 100}), animate(1000, style({height: 0}))]);
const instruction = timelines[0];
expect(MockAnimationDriver.log.length).toEqual(0);
engine.process(element, [instruction]);
expect(MockAnimationDriver.log.length).toEqual(1);
});
it('should animate an array of animation instructions', () => {
const engine = makeEngine();
const instructions = buildAnimationKeyframes([
style({height: 100}), animate(1000, style({height: 0})),
animate(1000, keyframes([style({width: 0}), style({width: 1000})]))
]);
expect(MockAnimationDriver.log.length).toEqual(0);
engine.process(element, instructions);
expect(MockAnimationDriver.log.length).toBeGreaterThan(0);
});
it('should return a noOp player when an unsupported instruction is provided', () => {
const engine = makeEngine();
const instruction = <AnimationEngineInstruction>{type: -1};
expect(MockAnimationDriver.log.length).toEqual(0);
const player = engine.process(element, [instruction]);
expect(MockAnimationDriver.log.length).toEqual(0);
expect(player instanceof ɵNoOpAnimationPlayer).toBeTruthy();
});
});
describe('transition operations', () => {
it('should persist the styles on the element as actual styles once the animation is complete',
() => {
const engine = makeEngine();
const trig = trigger('something', [
state('on', style({height: '100px'})), state('off', style({height: '0px'})),
transition('on => off', animate(9876))
]);
const instruction = trig.matchTransition('on', 'off');
const player = engine.process(element, [instruction]);
expect(element.style.height).not.toEqual('0px');
player.finish();
expect(element.style.height).toEqual('0px');
});
it('should remove all existing state styling from an element when a follow-up transition occurs on the same trigger',
() => {
const engine = makeEngine();
const trig = trigger('something', [
state('a', style({height: '100px'})), state('b', style({height: '500px'})),
state('c', style({width: '200px'})), transition('* => *', animate(9876))
]);
const instruction1 = trig.matchTransition('a', 'b');
const player1 = engine.process(element, [instruction1]);
player1.finish();
expect(element.style.height).toEqual('500px');
const instruction2 = trig.matchTransition('b', 'c');
const player2 = engine.process(element, [instruction2]);
expect(element.style.height).not.toEqual('500px');
player2.finish();
expect(element.style.width).toEqual('200px');
expect(element.style.height).not.toEqual('500px');
});
it('should allow two animation transitions with different triggers to animate in parallel',
() => {
const engine = makeEngine();
const trig1 = trigger('something1', [
state('a', style({width: '100px'})), state('b', style({width: '200px'})),
transition('* => *', animate(1000))
]);
const trig2 = trigger('something2', [
state('x', style({height: '500px'})), state('y', style({height: '1000px'})),
transition('* => *', animate(2000))
]);
let doneCount = 0;
function doneCallback() { doneCount++; }
const instruction1 = trig1.matchTransition('a', 'b');
const instruction2 = trig2.matchTransition('x', 'y');
const player1 = engine.process(element, [instruction1]);
player1.onDone(doneCallback);
expect(doneCount).toEqual(0);
const player2 = engine.process(element, [instruction2]);
player2.onDone(doneCallback);
expect(doneCount).toEqual(0);
player1.finish();
expect(doneCount).toEqual(1);
player2.finish();
expect(doneCount).toEqual(2);
expect(element.style.width).toEqual('200px');
expect(element.style.height).toEqual('1000px');
});
it('should cancel a previously running animation when a follow-up transition kicks off on the same trigger',
() => {
const engine = makeEngine();
const trig = trigger('something', [
state('x', style({opacity: 0})), state('y', style({opacity: .5})),
state('z', style({opacity: 1})), transition('* => *', animate(1000))
]);
const instruction1 = trig.matchTransition('x', 'y');
const instruction2 = trig.matchTransition('y', 'z');
expect(parseFloat(element.style.opacity)).not.toEqual(.5);
const player1 = engine.process(element, [instruction1]);
const player2 = engine.process(element, [instruction2]);
expect(parseFloat(element.style.opacity)).toEqual(.5);
player2.finish();
expect(parseFloat(element.style.opacity)).toEqual(1);
player1.finish();
expect(parseFloat(element.style.opacity)).toEqual(1);
});
it('should pass in the previously running players into the follow-up transition player when cancelled',
() => {
const engine = makeEngine();
const trig = trigger('something', [
state('x', style({opacity: 0})), state('y', style({opacity: .5})),
state('z', style({opacity: 1})), transition('* => *', animate(1000))
]);
const instruction1 = trig.matchTransition('x', 'y');
const instruction2 = trig.matchTransition('y', 'z');
const instruction3 = trig.matchTransition('z', 'x');
const player1 = engine.process(element, [instruction1]);
engine.triggerAnimations();
player1.setPosition(0.5);
const player2 = <MockAnimationPlayer>engine.process(element, [instruction2]);
expect(player2.previousPlayers).toEqual([player1]);
player2.finish();
const player3 = <MockAnimationPlayer>engine.process(element, [instruction3]);
expect(player3.previousPlayers).toEqual([]);
});
it('should cancel all existing players if a removal animation is set to occur', () => {
const engine = makeEngine();
const trig = trigger('something', [
state('m', style({opacity: 0})), state('n', style({opacity: 1})),
transition('* => *', animate(1000))
]);
let doneCount = 0;
function doneCallback() { doneCount++; }
const instruction1 = trig.matchTransition('m', 'n');
const instructions2 =
buildAnimationKeyframes([style({height: 0}), animate(1000, style({height: 100}))]);
const instruction3 = trig.matchTransition('n', 'void');
const player1 = engine.process(element, [instruction1]);
player1.onDone(doneCallback);
const player2 = engine.process(element, instructions2);
player2.onDone(doneCallback);
expect(doneCount).toEqual(0);
const player3 = engine.process(element, [instruction3]);
expect(doneCount).toEqual(2);
});
it('should only persist styles that exist in the final state styles and not the last keyframe',
() => {
const engine = makeEngine();
const trig = trigger('something', [
state('0', style({width: '0px'})), state('1', style({width: '100px'})),
transition('* => *', [animate(1000, style({height: '200px'}))])
]);
const instruction = trig.matchTransition('0', '1');
const player = engine.process(element, [instruction]);
expect(element.style.width).not.toEqual('100px');
player.finish();
expect(element.style.height).not.toEqual('200px');
expect(element.style.width).toEqual('100px');
});
it('should default to using styling from the `*` state if a matching state is not found',
() => {
const engine = makeEngine();
const trig = trigger('something', [
state('a', style({opacity: 0})), state('*', style({opacity: .5})),
transition('* => *', animate(1000))
]);
const instruction = trig.matchTransition('a', 'z');
engine.process(element, [instruction]).finish();
expect(parseFloat(element.style.opacity)).toEqual(.5);
});
it('should treat `void` as `void`', () => {
const engine = makeEngine();
const trig = trigger('something', [
state('a', style({opacity: 0})), state('void', style({opacity: .8})),
transition('* => *', animate(1000))
]);
const instruction = trig.matchTransition('a', 'void');
engine.process(element, [instruction]).finish();
expect(parseFloat(element.style.opacity)).toEqual(.8);
});
});
describe('timeline operations', () => {
it('should not destroy timeline-based animations after they have finished', () => {
const engine = makeEngine();
const log: string[] = [];
function capture(value: string) {
return () => { log.push(value); };
}
const instructions =
buildAnimationKeyframes([style({height: 0}), animate(1000, style({height: 500}))]);
const player = engine.process(element, instructions);
player.onDone(capture('done'));
player.onDestroy(capture('destroy'));
expect(log).toEqual([]);
player.finish();
expect(log).toEqual(['done']);
player.destroy();
expect(log).toEqual(['done', 'destroy']);
});
});
describe('style normalizer', () => {
it('should normalize the style values that are processed within an a transition animation',
() => {
const engine = makeEngine(new SuffixNormalizer('-normalized'));
const trig = trigger('something', [
state('on', style({height: 100})), state('off', style({height: 0})),
transition('on => off', animate(9876))
]);
const instruction = trig.matchTransition('on', 'off');
const player = <MockAnimationPlayer>engine.process(element, [instruction]);
expect(player.keyframes).toEqual([
{'height-normalized': '100-normalized', offset: 0},
{'height-normalized': '0-normalized', offset: 1}
]);
});
it('should normalize the style values that are processed within an a timeline animation',
() => {
const engine = makeEngine(new SuffixNormalizer('-normalized'));
const instructions = buildAnimationKeyframes([
style({width: '333px'}),
animate(1000, style({width: '999px'})),
]);
const player = <MockAnimationPlayer>engine.process(element, instructions);
expect(player.keyframes).toEqual([
{'width-normalized': '333px-normalized', offset: 0},
{'width-normalized': '999px-normalized', offset: 1}
]);
});
it('should throw an error when normalization fails within a transition animation', () => {
const engine = makeEngine(new ExactCssValueNormalizer({left: '100px'}));
const trig = trigger('something', [
state('a', style({left: '0px', width: '200px'})),
state('b', style({left: '100px', width: '100px'})), transition('a => b', animate(9876))
]);
const instruction = trig.matchTransition('a', 'b');
let errorMessage = '';
try {
engine.process(element, [instruction]);
} catch (e) {
errorMessage = e.toString();
}
expect(errorMessage).toMatch(/Unable to animate due to the following errors:/);
expect(errorMessage).toMatch(/- The CSS property `left` is not allowed to be `0px`/);
expect(errorMessage).toMatch(/- The CSS property `width` is not allowed/);
});
});
describe('view operations', () => {
it('should perform insert operations immediately ', () => {
const engine = makeEngine();
let container = el('<div></div>');
let child1 = el('<div></div>');
let child2 = el('<div></div>');
engine.insertNode(container, child1);
engine.insertNode(container, child2);
expect(container.contains(child1)).toBe(true);
expect(container.contains(child2)).toBe(true);
});
it('should queue up all `remove` DOM operations until all animations are complete', () => {
let container = el('<div></div>');
let targetContainer = el('<div></div>');
let otherContainer = el('<div></div>');
let child1 = el('<div></div>');
let child2 = el('<div></div>');
container.appendChild(targetContainer);
container.appendChild(otherContainer);
targetContainer.appendChild(child1);
targetContainer.appendChild(child2);
/*----------------*
container
/ \
target other
/ \
c1 c2
*----------------*/
expect(container.contains(otherContainer)).toBe(true);
const engine = makeEngine();
engine.removeNode(child1);
engine.removeNode(child2);
engine.removeNode(otherContainer);
expect(container.contains(child1)).toBe(true);
expect(container.contains(child2)).toBe(true);
expect(container.contains(otherContainer)).toBe(true);
const instructions =
buildAnimationKeyframes([style({height: 0}), animate(1000, style({height: 100}))]);
const player = engine.process(targetContainer, instructions);
expect(container.contains(child1)).toBe(true);
expect(container.contains(child2)).toBe(true);
expect(container.contains(otherContainer)).toBe(true);
engine.triggerAnimations();
expect(container.contains(child1)).toBe(true);
expect(container.contains(child2)).toBe(true);
expect(container.contains(otherContainer)).toBe(false);
player.finish();
expect(container.contains(child1)).toBe(false);
expect(container.contains(child2)).toBe(false);
expect(container.contains(otherContainer)).toBe(false);
});
});
});
}
class SuffixNormalizer extends AnimationStyleNormalizer {
constructor(private _suffix: string) { super(); }
normalizePropertyName(propertyName: string, errors: string[]): string {
return propertyName + this._suffix;
}
normalizeStyleValue(
userProvidedProperty: string, normalizedProperty: string, value: string|number,
errors: string[]): string {
return value + this._suffix;
}
}
class ExactCssValueNormalizer extends AnimationStyleNormalizer {
constructor(private _allowedValues: {[propName: string]: any}) { super(); }
normalizePropertyName(propertyName: string, errors: string[]): string {
if (!this._allowedValues[propertyName]) {
errors.push(`The CSS property \`${propertyName}\` is not allowed`);
}
return propertyName;
}
normalizeStyleValue(
userProvidedProperty: string, normalizedProperty: string, value: string|number,
errors: string[]): string {
const expectedValue = this._allowedValues[userProvidedProperty];
if (expectedValue != value) {
errors.push(`The CSS property \`${userProvidedProperty}\` is not allowed to be \`${value}\``);
}
return expectedValue;
}
}

View File

@ -1,31 +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, ɵNoOpAnimationPlayer} from '@angular/core';
import {StyleData} from '../src/common/style_data';
import {AnimationDriver} from '../src/engine/animation_driver';
export class MockAnimationDriver implements AnimationDriver {
static log: AnimationPlayer[] = [];
animate(
element: any, keyframes: StyleData[], duration: number, delay: number, easing: string,
previousPlayers: AnimationPlayer[] = []): AnimationPlayer {
const player =
new MockAnimationPlayer(element, keyframes, duration, delay, easing, previousPlayers);
MockAnimationDriver.log.push(<AnimationPlayer>player);
return <AnimationPlayer>player;
}
}
export class MockAnimationPlayer extends ɵNoOpAnimationPlayer {
constructor(
public element: any, public keyframes: StyleData[], public duration: number,
public delay: number, public easing: string, public previousPlayers: AnimationPlayer[]) {
super();
}
}

View File

@ -1,18 +0,0 @@
{
"extends": "./tsconfig-build",
"compilerOptions": {
"outDir": "../../../dist/packages-dist/animation",
"paths": {
"@angular/core": ["../../../dist/packages-dist/core/"],
"@angular/animation": ["../../../dist/packages-dist/animation/"]
}
},
"files": [
"testing/index.ts",
"../../../node_modules/zone.js/dist/zone.js.d.ts"
],
"angularCompilerOptions": {
"strictMetadataEmit": true
}
}

View File

@ -4,7 +4,7 @@
"plugins": [["transform-es2015-modules-umd", {
"globals": {
"@angular/core": "ng.core",
"@angular/animation": "ng.animation",
"@angular/animations": "ng.animations",
"rxjs/Observable": "Rx",
"rxjs/Subject": "Rx"
},

View File

@ -4,8 +4,7 @@
"plugins": [["transform-es2015-modules-umd", {
"globals": {
"@angular/core": "ng.core",
"@angular/animation": "ng.animation",
"@angular/animation/testing": "ng.animation.testing",
"@angular/animations": "ng.animations",
"rxjs/Observable": "Rx",
"rxjs/Subject": "Rx"
},

View File

@ -0,0 +1,18 @@
{
"name": "@angular/animations",
"version": "0.0.0-PLACEHOLDER",
"description": "Angular - animations integration with web-animationss",
"main": "./bundles/animations.umd.js",
"module": "./@angular/animations.es5.js",
"es2015": "./@angular/animations.js",
"typings": "./typings/animations.d.ts",
"author": "angular",
"license": "MIT",
"peerDependencies": {
"@angular/core": "0.0.0-PLACEHOLDER"
},
"repository": {
"type": "git",
"url": "https://github.com/angular/angular.git"
}
}

View File

@ -11,4 +11,4 @@
* @description
* Entry point for all public APIs of the animation package.
*/
export * from './src/animation';
export * from './src/animations';

View File

@ -0,0 +1,47 @@
/**
* @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
*/
/**
* An instance of this class is returned as an event parameter when an animation
* callback is captured for an animation either during the start or done phase.
*
* ```typescript
* @Component({
* host: {
* '[@myAnimationTrigger]': 'someExpression',
* '(@myAnimationTrigger.start)': 'captureStartEvent($event)',
* '(@myAnimationTrigger.done)': 'captureDoneEvent($event)',
* },
* animations: [
* trigger("myAnimationTrigger", [
* // ...
* ])
* ]
* })
* class MyComponent {
* someExpression: any = false;
* captureStartEvent(event: AnimationEvent) {
* // the toState, fromState and totalTime data is accessible from the event variable
* }
*
* captureDoneEvent(event: AnimationEvent) {
* // the toState, fromState and totalTime data is accessible from the event variable
* }
* }
* ```
*
* @experimental Animation support is experimental.
*/
export interface AnimationEvent {
fromState: string;
toState: string;
totalTime: number;
phaseName: string;
element: any;
triggerName: string;
}

View File

@ -5,14 +5,20 @@
* 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 {StyleData} from '../common/style_data';
export interface ɵStyleData { [key: string]: string|number; }
/**
* @experimental Animation support is experimental.
*/
export declare type AnimateTimings = {
duration: number,
delay: number,
easing: string
};
/**
* @experimental Animation support is experimental.
*/
export const enum AnimationMetadataType {
State,
Transition,
@ -33,6 +39,14 @@ export const AUTO_STYLE = '*';
*/
export interface AnimationMetadata { type: AnimationMetadataType; }
/**
* @experimental Animation support is experimental.
*/
export interface AnimationTriggerMetadata {
name: string;
definitions: AnimationMetadata[];
}
/**
* Metadata representing the entry of animations. Instances of this class are provided via the
* animation DSL when the {@link state state animation function} is called.
@ -72,7 +86,7 @@ export interface AnimationKeyframesSequenceMetadata extends AnimationMetadata {
* @experimental Animation support is experimental.
*/
export interface AnimationStyleMetadata extends AnimationMetadata {
styles: StyleData[];
styles: {[key: string]: string | number}[];
offset: number;
}
@ -103,6 +117,61 @@ export interface AnimationSequenceMetadata extends AnimationMetadata { steps: An
*/
export interface AnimationGroupMetadata extends AnimationMetadata { steps: AnimationMetadata[]; }
/**
* `trigger` is an animation-specific function that is designed to be used inside of Angular2's
animation DSL language. If this information is new, please navigate to the {@link
Component#animations-anchor component animations metadata page} to gain a better understanding of
how animations in Angular2 are used.
*
* `trigger` Creates an animation trigger which will a list of {@link state state} and {@link
transition transition} entries that will be evaluated when the expression bound to the trigger
changes.
*
* Triggers are registered within the component annotation data under the {@link
Component#animations-anchor animations section}. An animation trigger can be placed on an element
within a template by referencing the name of the trigger followed by the expression value that the
trigger is bound to (in the form of `[@triggerName]="expression"`.
*
* ### Usage
*
* `trigger` will create an animation trigger reference based on the provided `name` value. The
provided `animation` value is expected to be an array consisting of {@link state state} and {@link
transition transition} declarations.
*
* ```typescript
* @Component({
* selector: 'my-component',
* templateUrl: 'my-component-tpl.html',
* animations: [
* trigger("myAnimationTrigger", [
* state(...),
* state(...),
* transition(...),
* transition(...)
* ])
* ]
* })
* class MyComponent {
* myStatusExp = "something";
* }
* ```
*
* The template associated with this component will make use of the `myAnimationTrigger` animation
trigger by binding to an element within its template code.
*
* ```html
* <!-- somewhere inside of my-component-tpl.html -->
* <div [@myAnimationTrigger]="myStatusExp">...</div>
tools/gulp-tasks/validate-commit-message.js ```
*
* {@example core/animation/ts/dsl/animation_example.ts region='Component'}
*
* @experimental Animation support is experimental.
*/
export function trigger(name: string, definitions: AnimationMetadata[]): AnimationTriggerMetadata {
return {name, definitions};
}
/**
* `animate` is an animation-specific function that is designed to be used inside of Angular2's
* animation DSL language. If this information is new, please navigate to the {@link
@ -272,15 +341,15 @@ export function sequence(steps: AnimationMetadata[]): AnimationSequenceMetadata
export function style(
tokens: {[key: string]: string | number} |
Array<{[key: string]: string | number}>): AnimationStyleMetadata {
let input: StyleData[];
let input: ɵStyleData[];
let offset: number = null;
if (Array.isArray(tokens)) {
input = <StyleData[]>tokens;
input = <ɵStyleData[]>tokens;
} else {
input = [<StyleData>tokens];
input = [<ɵStyleData>tokens];
}
input.forEach(entry => {
const entryOffset = (entry as StyleData)['offset'];
const entryOffset = (entry as ɵStyleData)['offset'];
if (entryOffset != null) {
offset = offset == null ? parseFloat(<string>entryOffset) : offset;
}
@ -288,7 +357,7 @@ export function style(
return _style(offset, input);
}
function _style(offset: number, styles: StyleData[]): AnimationStyleMetadata {
function _style(offset: number, styles: ɵStyleData[]): AnimationStyleMetadata {
return {type: AnimationMetadataType.Style, styles: styles, offset: offset};
}

View File

@ -0,0 +1,18 @@
/**
* @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
*/
/**
* @module
* @description
* Entry point for all animation APIs of the animation package.
*/
export {AnimationEvent} from './animation_event';
export {AUTO_STYLE, AnimateTimings, AnimationAnimateMetadata, AnimationGroupMetadata, AnimationKeyframesSequenceMetadata, AnimationMetadata, AnimationMetadataType, AnimationSequenceMetadata, AnimationStateMetadata, AnimationStyleMetadata, AnimationTransitionMetadata, AnimationTriggerMetadata, animate, group, keyframes, sequence, state, style, transition, trigger, ɵStyleData} from './animation_metadata';
export {AnimationPlayer, NoOpAnimationPlayer} from './players/animation_player';
export * from './private_export';

View File

@ -0,0 +1,109 @@
/**
* @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 {scheduleMicroTask} from '../util';
import {AnimationPlayer} from './animation_player';
export class AnimationGroupPlayer implements AnimationPlayer {
private _onDoneFns: Function[] = [];
private _onStartFns: Function[] = [];
private _finished = false;
private _started = false;
private _destroyed = false;
private _onDestroyFns: Function[] = [];
public parentPlayer: AnimationPlayer = null;
constructor(private _players: AnimationPlayer[]) {
let count = 0;
const total = this._players.length;
if (total == 0) {
scheduleMicroTask(() => this._onFinish());
} else {
this._players.forEach(player => {
player.parentPlayer = this;
player.onDone(() => {
if (++count >= total) {
this._onFinish();
}
});
});
}
}
private _onFinish() {
if (!this._finished) {
this._finished = true;
this._onDoneFns.forEach(fn => fn());
this._onDoneFns = [];
}
}
init(): void { this._players.forEach(player => player.init()); }
onStart(fn: () => void): void { this._onStartFns.push(fn); }
onDone(fn: () => void): void { this._onDoneFns.push(fn); }
onDestroy(fn: () => void): void { this._onDestroyFns.push(fn); }
hasStarted() { return this._started; }
play() {
if (!this.parentPlayer) {
this.init();
}
if (!this.hasStarted()) {
this._onStartFns.forEach(fn => fn());
this._onStartFns = [];
this._started = true;
}
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 {
if (!this._destroyed) {
this._onFinish();
this._players.forEach(player => player.destroy());
this._destroyed = true;
this._onDestroyFns.forEach(fn => fn());
this._onDestroyFns = [];
}
}
reset(): void {
this._players.forEach(player => player.reset());
this._destroyed = false;
this._finished = false;
this._started = false;
}
setPosition(p: number): void {
this._players.forEach(player => { player.setPosition(p); });
}
getPosition(): number {
let min = 0;
this._players.forEach(player => {
const p = player.getPosition();
min = Math.min(p, min);
});
return min;
}
get players(): AnimationPlayer[] { return this._players; }
}

View File

@ -0,0 +1,76 @@
/**
* @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 {scheduleMicroTask} from '../util';
/**
* @experimental Animation support is experimental.
*/
export abstract class AnimationPlayer {
abstract onDone(fn: () => void): void;
abstract onStart(fn: () => void): void;
abstract onDestroy(fn: () => void): void;
abstract init(): void;
abstract hasStarted(): boolean;
abstract play(): void;
abstract pause(): void;
abstract restart(): void;
abstract finish(): void;
abstract destroy(): void;
abstract reset(): void;
abstract setPosition(p: any /** TODO #9100 */): void;
abstract getPosition(): number;
get parentPlayer(): AnimationPlayer { throw new Error('NOT IMPLEMENTED: Base Class'); }
set parentPlayer(player: AnimationPlayer) { throw new Error('NOT IMPLEMENTED: Base Class'); }
}
/**
* @experimental Animation support is experimental.
*/
export class NoOpAnimationPlayer implements AnimationPlayer {
private _onDoneFns: Function[] = [];
private _onStartFns: Function[] = [];
private _onDestroyFns: Function[] = [];
private _started = false;
private _destroyed = false;
private _finished = false;
public parentPlayer: AnimationPlayer = null;
constructor() { scheduleMicroTask(() => this._onFinish()); }
private _onFinish() {
if (!this._finished) {
this._finished = true;
this._onDoneFns.forEach(fn => fn());
this._onDoneFns = [];
}
}
onStart(fn: () => void): void { this._onStartFns.push(fn); }
onDone(fn: () => void): void { this._onDoneFns.push(fn); }
onDestroy(fn: () => void): void { this._onDestroyFns.push(fn); }
hasStarted(): boolean { return this._started; }
init(): void {}
play(): void {
if (!this.hasStarted()) {
this._onStartFns.forEach(fn => fn());
this._onStartFns = [];
}
this._started = true;
}
pause(): void {}
restart(): void {}
finish(): void { this._onFinish(); }
destroy(): void {
if (!this._destroyed) {
this._destroyed = true;
this.finish();
this._onDestroyFns.forEach(fn => fn());
this._onDestroyFns = [];
}
}
reset(): void {}
setPosition(p: number): void {}
getPosition(): number { return 0; }
}

View File

@ -0,0 +1,8 @@
/**
* @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
*/
export {AnimationGroupPlayer as ɵAnimationGroupPlayer} from './players/animation_group_player';

View File

@ -5,4 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
export interface StyleData { [key: string]: string|number; }
export function scheduleMicroTask(cb: () => any) {
// FIXME
setTimeout(cb, 0);
}

View File

@ -6,9 +6,10 @@
"experimentalDecorators": true,
"module": "es2015",
"moduleResolution": "node",
"outDir": "../../../dist/packages-dist/animation",
"outDir": "../../../dist/packages-dist/animations",
"paths": {
"@angular/core": ["../../../dist/packages-dist/core"]
"@angular/core": ["../../../dist/packages-dist/core"],
"@angular/core/testing": ["../../../dist/packages-dist/core/testing"]
},
"rootDir": ".",
"sourceMap": true,
@ -28,6 +29,6 @@
"annotateForClosureCompiler": true,
"strictMetadataEmit": true,
"flatModuleOutFile": "index.js",
"flatModuleId": "@angular/animation"
"flatModuleId": "@angular/animations"
}
}

View File

@ -250,7 +250,7 @@ export class CompileTemplateMetadata {
styles: string[];
styleUrls: string[];
externalStylesheets: CompileStylesheetMetadata[];
animations: CompileAnimationEntryMetadata[];
animations: any[];
ngContentSelectors: string[];
interpolation: [string, string];
constructor(
@ -263,7 +263,7 @@ export class CompileTemplateMetadata {
styleUrls?: string[],
externalStylesheets?: CompileStylesheetMetadata[],
ngContentSelectors?: string[],
animations?: CompileAnimationEntryMetadata[],
animations?: any[],
interpolation?: [string, string],
} = {}) {
this.encapsulation = encapsulation;

View File

@ -8,7 +8,7 @@
import {Compiler, ComponentFactory, Inject, Injector, ModuleWithComponentFactories, NgModuleFactory, Type, ɵgetComponentFactoryViewClass as getComponentFactoryViewClass} from '@angular/core';
import {AnimationCompiler} from '../animation/animation_compiler';
import {AnimationCompiler, AnimationEntryCompileResult} from '../animation/animation_compiler';
import {AnimationParser} from '../animation/animation_parser';
import {CompileDirectiveMetadata, CompileIdentifierMetadata, CompileNgModuleMetadata, ProviderMeta, ProxyClass, createHostComponentMeta, identifierName} from '../compile_metadata';
import {CompilerConfig} from '../config';
@ -272,7 +272,8 @@ export class JitCompiler implements Compiler {
(r) => { externalStylesheetsByModuleUrl.set(r.meta.moduleUrl, r); });
this._resolveStylesCompileResult(
stylesCompileResult.componentStylesheet, externalStylesheetsByModuleUrl);
const parsedAnimations = this._animationParser.parseComponent(compMeta);
const parsedAnimations =
this._compilerConfig.useViewEngine ? [] : this._animationParser.parseComponent(compMeta);
const directives =
template.directives.map(dir => this._metadataResolver.getDirectiveSummary(dir.reference));
const pipes = template.ngModule.transitiveModule.pipes.map(
@ -280,7 +281,8 @@ export class JitCompiler implements Compiler {
const {template: parsedTemplate, pipes: usedPipes} = this._templateParser.parse(
compMeta, compMeta.template.template, directives, pipes, template.ngModule.schemas,
identifierName(compMeta.type));
const compiledAnimations =
const compiledAnimations = this._compilerConfig.useViewEngine ?
[] :
this._animationCompiler.compile(identifierName(compMeta.type), parsedAnimations);
const compileResult = this._viewCompiler.compileComponent(
compMeta, parsedTemplate, ir.variable(stylesCompileResult.componentStylesheet.stylesVar),

View File

@ -308,9 +308,14 @@ export class CompileMetadataResolver {
assertArrayOfStrings('styleUrls', dirMeta.styleUrls);
assertInterpolationSymbols('interpolation', dirMeta.interpolation);
const animations = dirMeta.animations ?
dirMeta.animations.map(e => this.getAnimationEntryMetadata(e)) :
null;
let animations: any[];
if (this._config.useViewEngine) {
animations = dirMeta.animations;
} else {
animations = dirMeta.animations ?
dirMeta.animations.map(e => this.getAnimationEntryMetadata(e)) :
null;
}
nonNormalizedTemplateMetadata = new cpl.CompileTemplateMetadata({
encapsulation: dirMeta.encapsulation,

View File

@ -414,6 +414,8 @@ class TemplateParseVisitor implements html.Visitor {
private _validateElementAnimationInputOutputs(
inputs: BoundElementPropertyAst[], outputs: BoundEventAst[],
template: CompileTemplateSummary) {
if (this.config.useViewEngine) return;
const triggerLookup = new Set<string>();
template.animations.forEach(entry => { triggerLookup.add(entry); });

View File

@ -49,8 +49,9 @@ export class ViewCompilerNext extends ViewCompiler {
new o.LiteralMapExpr([
new o.LiteralMapEntry('encapsulation', o.literal(component.template.encapsulation)),
new o.LiteralMapEntry('styles', styles),
// TODO: copy this from the @Component directive...
new o.LiteralMapEntry('data', o.literalMap([])),
new o.LiteralMapEntry('data', o.literalMap([
['animation', convertValueToOutputAst(component.template.animations)]
])),
])
]))
.toDeclStmt(
@ -347,11 +348,13 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver, BuiltinConverter
}
const usedEvents = new Map<string, [string, string]>();
ast.outputs.forEach((event) => {
usedEvents.set(elementEventFullName(event.target, event.name), [event.target, event.name]);
const en = eventName(event);
usedEvents.set(elementEventFullName(event.target, en), [event.target, en]);
});
ast.directives.forEach((dirAst) => {
dirAst.hostEvents.forEach((event) => {
usedEvents.set(elementEventFullName(event.target, event.name), [event.target, event.name]);
const en = eventName(event);
usedEvents.set(elementEventFullName(event.target, en), [event.target, en]);
});
});
const hostBindings: {value: AST, context: o.Expression}[] = [];
@ -713,7 +716,7 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver, BuiltinConverter
if (allowDefault) {
trueStmts.push(ALLOW_DEFAULT_VAR.set(allowDefault.and(ALLOW_DEFAULT_VAR)).toStmt());
}
const fullEventName = elementEventFullName(eventAst.target, eventAst.name);
const fullEventName = elementEventFullName(eventAst.target, eventName(eventAst));
handleEventStmts.push(
new o.IfStmt(o.literal(fullEventName).identical(EVENT_NAME_VAR), trueStmts));
});
@ -895,7 +898,7 @@ function elementBindingDefs(inputAsts: BoundElementPropertyAst[]): o.Expression[
]);
case PropertyBindingType.Animation:
return o.literalArr([
o.literal(BindingType.ElementProperty), o.literal(inputAst.name),
o.literal(BindingType.ElementProperty), o.literal('@' + inputAst.name),
o.literal(inputAst.securityContext)
]);
case PropertyBindingType.Class:
@ -1021,3 +1024,7 @@ function createComponentFactoryResolver(directives: DirectiveAst[]): ProviderAst
}
return null;
}
function eventName(eventAst: BoundEventAst): string {
return eventAst.isAnimation ? `@${eventAst.name}.${eventAst.phase}` : eventAst.name;
}

View File

@ -0,0 +1,150 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {AnimateTimings, AnimationMetadataType, animate as _animate, group as _group, keyframes as _keyframes, sequence as _sequence, state as _state, style as _style, transition as _transition, trigger as _trigger} from './dsl';
/**
* @deprecated This symbol has moved. Please Import from @angular/animations instead!
*/
export const AUTO_STYLE = '*';
/**
* @deprecated This symbol has moved. Please Import from @angular/animations instead!
*/
export interface AnimationMetadata { type: AnimationMetadataType; }
/**
* @deprecated This symbol has moved. Please Import from @angular/animations instead!
*/
export interface AnimationTriggerMetadata {
name: string;
definitions: AnimationMetadata[];
}
/**
* @deprecated This symbol has moved. Please Import from @angular/animations instead!
*/
export interface AnimationStateMetadata extends AnimationMetadata {
name: string;
styles: AnimationStyleMetadata;
}
/**
* @deprecated This symbol has moved. Please Import from @angular/animations instead!
*/
export interface AnimationTransitionMetadata extends AnimationMetadata {
expr: string|((fromState: string, toState: string) => boolean);
animation: AnimationMetadata;
}
/**
* @deprecated This symbol has moved. Please Import from @angular/animations instead!
*/
export interface AnimationKeyframesSequenceMetadata extends AnimationMetadata {
steps: AnimationStyleMetadata[];
}
/**
* @deprecated This symbol has moved. Please Import from @angular/animations instead!
*/
export interface AnimationStyleMetadata extends AnimationMetadata {
styles: {[key: string]: string | number}[];
offset: number;
}
/**
* @deprecated This symbol has moved. Please Import from @angular/animations instead!
*/
export interface AnimationAnimateMetadata extends AnimationMetadata {
timings: string|number|AnimateTimings;
styles: AnimationStyleMetadata|AnimationKeyframesSequenceMetadata;
}
/**
* @deprecated This symbol has moved. Please Import from @angular/animations instead!
*/
export interface AnimationSequenceMetadata extends AnimationMetadata { steps: AnimationMetadata[]; }
/**
* @deprecated This symbol has moved. Please Import from @angular/animations instead!
*/
export interface AnimationGroupMetadata extends AnimationMetadata { steps: AnimationMetadata[]; }
/**
* @deprecated This symbol has moved. Please Import from @angular/animations instead!
*/
export function trigger(name: string, definitions: AnimationMetadata[]): AnimationTriggerMetadata {
return _trigger(name, definitions);
}
/**
* @deprecated This symbol has moved. Please Import from @angular/animations instead!
*/
export function animate(
timings: string | number, styles: AnimationStyleMetadata | AnimationKeyframesSequenceMetadata =
null): AnimationAnimateMetadata {
return _animate(timings, styles);
}
/**
* @deprecated This symbol has moved. Please Import from @angular/animations instead!
*/
export function group(steps: AnimationMetadata[]): AnimationGroupMetadata {
return _group(steps);
}
/**
* @deprecated This symbol has moved. Please Import from @angular/animations instead!
*/
export function sequence(steps: AnimationMetadata[]): AnimationSequenceMetadata {
return _sequence(steps);
}
/**
* @deprecated This symbol has moved. Please Import from @angular/animations instead!
*/
export function style(
tokens: {[key: string]: string | number} |
Array<{[key: string]: string | number}>): AnimationStyleMetadata {
return _style(tokens);
}
/**
* @deprecated This symbol has moved. Please Import from @angular/animations instead!
*/
export function state(name: string, styles: AnimationStyleMetadata): AnimationStateMetadata {
return _state(name, styles);
}
/**
* @deprecated This symbol has moved. Please Import from @angular/animations instead!
*/
export function keyframes(steps: AnimationStyleMetadata[]): AnimationKeyframesSequenceMetadata {
return _keyframes(steps);
}
/**
* @deprecated This symbol has moved. Please Import from @angular/animations instead!
*/
export function transition(
stateChangeExpr: string | ((fromState: string, toState: string) => boolean),
steps: AnimationMetadata | AnimationMetadata[]): AnimationTransitionMetadata {
return _transition(stateChangeExpr, steps);
}
/**
* @deprecated This has been renamed to `AnimationEvent`. Please import it from @angular/animations.
*/
export interface AnimationTransitionEvent {
fromState: string;
toState: string;
totalTime: number;
phaseName: string;
element: any;
triggerName: string;
}

View File

@ -0,0 +1 @@
../../../animations/src/animation_metadata.ts

View File

@ -32,11 +32,14 @@ export {Type} from './type';
export {EventEmitter} from './facade/async';
export {ErrorHandler} from './error_handler';
export * from './core_private_export';
export * from './animation/metadata';
export {AnimationTransitionEvent} from './animation/animation_transition_event';
export {AnimationPlayer} from './animation/animation_player';
export {AnimationStyles} from './animation/animation_styles';
export {AnimationKeyframe} from './animation/animation_keyframe';
export {Sanitizer, SecurityContext} from './security';
export {TransitionFactory, TransitionInstruction, Trigger} from './triggers';
export * from './codegen_private_exports';
// TODO (matsko|tbosch): comment-out the two lines below, and enable the 3rd line when the view
// engine goes live!
export {AnimationTransitionEvent} from './animation/animation_transition_event';
export * from './animation/metadata';
// export * from './animation_next/animation_metadata_wrapped';

View File

@ -35,6 +35,5 @@ export {ReflectionCapabilities as ɵReflectionCapabilities} from './reflection/r
export {ReflectorReader as ɵReflectorReader} from './reflection/reflector_reader';
export {GetterFn as ɵGetterFn, MethodFn as ɵMethodFn, SetterFn as ɵSetterFn} from './reflection/types';
export {DirectRenderer as ɵDirectRenderer, RenderDebugInfo as ɵRenderDebugInfo} from './render/api';
export {TransitionEngine as ɵTransitionEngine} from './transition/transition_engine';
export {makeDecorator as ɵmakeDecorator} from './util/decorators';
export {isObservable as ɵisObservable, isPromise as ɵisPromise} from './util/lang';

View File

@ -40,7 +40,7 @@ let nextRenderComponentTypeId = 0;
export function createRenderComponentType(
templateUrl: string, slotCount: number, encapsulation: ViewEncapsulation,
styles: Array<string|any[]>, animations: {[key: string]: Function}): RenderComponentType {
styles: Array<string|any[]>, animations: any): RenderComponentType {
return new RenderComponentType(
`${nextRenderComponentTypeId++}`, templateUrl, slotCount, encapsulation, styles, animations);
}

View File

@ -12,7 +12,6 @@ import {Provider} from './di';
import {Reflector, reflector} from './reflection/reflection';
import {ReflectorReader} from './reflection/reflector_reader';
import {TestabilityRegistry} from './testability/testability';
import {NoOpTransitionEngine, TransitionEngine} from './transition/transition_engine';
function _reflector(): Reflector {
return reflector;
@ -23,7 +22,6 @@ const _CORE_PLATFORM_PROVIDERS: Provider[] = [
{provide: PlatformRef, useExisting: PlatformRef_},
{provide: Reflector, useFactory: _reflector, deps: []},
{provide: ReflectorReader, useExisting: Reflector},
{provide: TransitionEngine, useClass: NoOpTransitionEngine},
TestabilityRegistry,
Console,
];

View File

@ -20,7 +20,7 @@ export class RenderComponentType {
constructor(
public id: string, public templateUrl: string, public slotCount: number,
public encapsulation: ViewEncapsulation, public styles: Array<string|any[]>,
public animations: {[key: string]: Function}) {}
public animations: any) {}
}
export abstract class RenderDebugInfo {
@ -91,6 +91,8 @@ export abstract class Renderer {
previousPlayers?: AnimationPlayer[]): AnimationPlayer;
}
export const RendererV2Interceptor = new InjectionToken<RendererV2[]>('RendererV2Interceptor');
/**
* Injectable service that provides a low-level interface for modifying the UI.
*

View File

@ -1,41 +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, NoOpAnimationPlayer} from '../animation/animation_player';
import {TransitionInstruction} from '../triggers';
/**
* @experimental Transition support is experimental.
*/
export abstract class TransitionEngine {
abstract insertNode(container: any, element: any): void;
abstract removeNode(element: any): void;
abstract process(element: any, instructions: TransitionInstruction[]): AnimationPlayer;
abstract triggerAnimations(): void;
}
/**
* @experimental Transition support is experimental.
*/
export class NoOpTransitionEngine extends TransitionEngine {
constructor() { super(); }
insertNode(container: any, element: any): void { container.appendChild(element); }
removeNode(element: any): void { remove(element); }
process(element: any, instructions: TransitionInstruction[]): AnimationPlayer {
return new NoOpAnimationPlayer();
}
triggerAnimations(): void {}
}
function remove(element: any) {
element.parentNode.removeChild(element);
}

View File

@ -1,27 +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
*/
/**
* @experimental View triggers are experimental
*/
export interface Trigger {
name: string;
transitionFactories: TransitionFactory[];
}
/**
* @experimental View triggers are experimental
*/
export interface TransitionFactory {
match(currentState: any, nextState: any): TransitionInstruction;
}
/**
* @experimental View triggers are experimental
*/
export interface TransitionInstruction {}

View File

@ -229,7 +229,7 @@ function debugCheckFn(
function normalizeDebugBindingName(name: string) {
// Attribute names with `$` (eg `x-y$`) are valid per spec, but unsupported by some browsers
name = camelCaseToDashCase(name.replace(/\$/g, '_'));
name = camelCaseToDashCase(name.replace(/[$@]/g, '_'));
return `ng-reflect-${name}`;
}

View File

@ -0,0 +1,566 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {AUTO_STYLE, AnimationEvent, animate, keyframes, state, style, transition, trigger} from '@angular/animations';
import {USE_VIEW_ENGINE} from '@angular/compiler/src/config';
import {Component, HostBinding, HostListener, ViewChild} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {AnimationDriver, BrowserAnimationModule, ɵAnimationEngine} from '@angular/platform-browser/animations';
import {MockAnimationDriver, MockAnimationPlayer} from '@angular/platform-browser/animations/testing';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {TestBed} from '../../testing';
export function main() {
describe('view engine', () => {
beforeEach(() => {
TestBed.configureCompiler({
useJit: true,
providers: [{
provide: USE_VIEW_ENGINE,
useValue: true,
}],
});
});
declareTests({useJit: true});
});
}
function declareTests({useJit}: {useJit: boolean}) {
// these tests are only mean't to be run within the DOM (for now)
if (typeof Element == 'undefined') return;
describe('animation tests', function() {
function getLog(): MockAnimationPlayer[] {
return MockAnimationDriver.log as MockAnimationPlayer[];
}
function resetLog() { MockAnimationDriver.log = []; }
beforeEach(() => {
resetLog();
TestBed.configureTestingModule({
providers: [{provide: AnimationDriver, useClass: MockAnimationDriver}],
imports: [BrowserModule, BrowserAnimationModule]
});
});
describe('animation triggers', () => {
it('should trigger a state change animation from void => state', () => {
@Component({
selector: 'if-cmp',
template: `
<div *ngIf="exp" [@myAnimation]="exp"></div>
`,
animations: [trigger(
'myAnimation',
[transition(
'void => *', [style({'opacity': '0'}), animate(500, style({'opacity': '1'}))])])],
})
class Cmp {
exp: any = false;
}
TestBed.configureTestingModule({declarations: [Cmp]});
const engine = TestBed.get(ɵAnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
cmp.exp = true;
fixture.detectChanges();
engine.flush();
expect(getLog().length).toEqual(1);
expect(getLog().pop().keyframes).toEqual([
{offset: 0, opacity: '0'}, {offset: 1, opacity: '1'}
]);
});
xit('should trigger a state change animation from void => state on the component host element',
() => {
@Component({
selector: 'my-cmp',
template: '...',
animations: [trigger(
'myAnimation',
[transition(
'a => b',
[style({'opacity': '0'}), animate(500, style({'opacity': '1'}))])])],
})
class Cmp {
@HostBinding('@myAnimation')
get binding() { return this.exp ? 'b' : 'a'; }
exp: any = false;
}
TestBed.configureTestingModule({declarations: [Cmp]});
const engine = TestBed.get(ɵAnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
cmp.exp = false;
fixture.detectChanges();
engine.flush();
expect(getLog().length).toEqual(0);
cmp.exp = true;
fixture.detectChanges();
engine.flush();
expect(getLog().length).toEqual(1);
const data = getLog().pop();
expect(data.element).toEqual(fixture.elementRef.nativeElement);
expect(data.keyframes).toEqual([{offset: 0, opacity: '0'}, {offset: 1, opacity: '1'}]);
});
it('should cancel and merge in mid-animation styles into the follow-up animation', () => {
@Component({
selector: 'ani-cmp',
template: `
<div [@myAnimation]="exp"></div>
`,
animations: [trigger(
'myAnimation',
[
transition(
'a => b',
[
style({'opacity': '0'}),
animate(500, style({'opacity': '1'})),
]),
transition(
'b => c',
[
style({'width': '0'}),
animate(500, style({'width': '100px'})),
]),
])],
})
class Cmp {
exp: any = false;
}
TestBed.configureTestingModule({declarations: [Cmp]});
const engine = TestBed.get(ɵAnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
cmp.exp = 'a';
fixture.detectChanges();
engine.flush();
expect(getLog().length).toEqual(0);
resetLog();
cmp.exp = 'b';
fixture.detectChanges();
engine.flush();
expect(getLog().length).toEqual(1);
resetLog();
cmp.exp = 'c';
fixture.detectChanges();
engine.flush();
expect(getLog().length).toEqual(1);
const data = getLog().pop();
expect(data.previousStyles).toEqual({opacity: AUTO_STYLE});
});
it('should invoke an animation trigger that is state-less', () => {
@Component({
selector: 'ani-cmp',
template: `
<div *ngFor="let item of items" @myAnimation></div>
`,
animations: [trigger(
'myAnimation',
[transition(':enter', [style({opacity: 0}), animate(1000, style({opacity: 1}))])])]
})
class Cmp {
items: number[] = [];
}
TestBed.configureTestingModule({declarations: [Cmp]});
const engine = TestBed.get(ɵAnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
cmp.items = [1, 2, 3, 4, 5];
fixture.detectChanges();
engine.flush();
expect(getLog().length).toEqual(5);
for (let i = 0; i < 5; i++) {
const item = getLog()[i];
expect(item.duration).toEqual(1000);
expect(item.keyframes).toEqual([{opacity: '0', offset: 0}, {opacity: '1', offset: 1}]);
}
});
it('should retain styles on the element once the animation is complete', () => {
@Component({
selector: 'ani-cmp',
template: `
<div #green @green></div>
`,
animations: [trigger('green', [state('*', style({backgroundColor: 'green'}))])]
})
class Cmp {
@ViewChild('green') public element: any;
}
TestBed.configureTestingModule({declarations: [Cmp]});
const engine = TestBed.get(ɵAnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
fixture.detectChanges();
engine.flush();
const player = engine.activePlayers.pop();
player.finish();
expect(getDOM().hasStyle(cmp.element.nativeElement, 'background-color', 'green'))
.toBeTruthy();
});
it('should animate removals of nodes to the `void` state for each animation trigger', () => {
@Component({
selector: 'ani-cmp',
template: `
<div *ngIf="exp" class="ng-if" [@trig1]="exp2" @trig2></div>
`,
animations: [
trigger('trig1', [transition('state => void', [animate(1000, style({opacity: 0}))])]),
trigger('trig2', [transition(':leave', [animate(1000, style({width: '0px'}))])])
]
})
class Cmp {
public exp = true;
public exp2 = 'state';
}
TestBed.configureTestingModule({declarations: [Cmp]});
const engine = TestBed.get(ɵAnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
cmp.exp = true;
fixture.detectChanges();
engine.flush();
resetLog();
const element = getDOM().querySelector(fixture.nativeElement, '.ng-if');
assertHasParent(element, true);
cmp.exp = false;
fixture.detectChanges();
engine.flush();
assertHasParent(element, true);
expect(getLog().length).toEqual(2);
const player2 = getLog().pop();
const player1 = getLog().pop();
expect(player2.keyframes).toEqual([
{width: AUTO_STYLE, offset: 0},
{width: '0px', offset: 1},
]);
expect(player1.keyframes).toEqual([
{opacity: AUTO_STYLE, offset: 0}, {opacity: '0', offset: 1}
]);
player2.finish();
player1.finish();
assertHasParent(element, false);
});
it('should not run inner child animations when a parent is set to be removed', () => {
@Component({
selector: 'ani-cmp',
template: `
<div *ngIf="exp" class="parent" >
<div [@myAnimation]="exp2"></div>
</div>
`,
animations: [trigger(
'myAnimation', [transition('a => b', [animate(1000, style({width: '0px'}))])])]
})
class Cmp {
public exp = true;
public exp2 = '0';
}
TestBed.configureTestingModule({declarations: [Cmp]});
const engine = TestBed.get(ɵAnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
cmp.exp = true;
cmp.exp2 = 'a';
fixture.detectChanges();
engine.flush();
resetLog();
cmp.exp = false;
cmp.exp2 = 'b';
fixture.detectChanges();
engine.flush();
expect(getLog().length).toEqual(0);
resetLog();
});
});
describe('animation listeners', () => {
it('should trigger a `start` state change listener for when the animation changes state from void => state',
() => {
@Component({
selector: 'if-cmp',
template: `
<div *ngIf="exp" [@myAnimation]="exp" (@myAnimation.start)="callback($event)"></div>
`,
animations: [trigger(
'myAnimation',
[transition(
'void => *',
[style({'opacity': '0'}), animate(500, style({'opacity': '1'}))])])],
})
class Cmp {
exp: any = false;
event: AnimationEvent;
callback = (event: any) => { this.event = event; };
}
TestBed.configureTestingModule({declarations: [Cmp]});
const engine = TestBed.get(ɵAnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
cmp.exp = 'true';
fixture.detectChanges();
engine.flush();
expect(cmp.event.triggerName).toEqual('myAnimation');
expect(cmp.event.phaseName).toEqual('start');
expect(cmp.event.totalTime).toEqual(500);
expect(cmp.event.fromState).toEqual('void');
expect(cmp.event.toState).toEqual('true');
});
it('should trigger a `done` state change listener for when the animation changes state from a => b',
() => {
@Component({
selector: 'if-cmp',
template: `
<div *ngIf="exp" [@myAnimation123]="exp" (@myAnimation123.done)="callback($event)"></div>
`,
animations: [trigger(
'myAnimation123',
[transition(
'* => b', [style({'opacity': '0'}), animate(999, style({'opacity': '1'}))])])],
})
class Cmp {
exp: any = false;
event: AnimationEvent;
callback = (event: any) => { this.event = event; };
}
TestBed.configureTestingModule({declarations: [Cmp]});
const engine = TestBed.get(ɵAnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
cmp.exp = 'b';
fixture.detectChanges();
engine.flush();
expect(cmp.event).toBeFalsy();
const player = engine.activePlayers.pop();
player.finish();
expect(cmp.event.triggerName).toEqual('myAnimation123');
expect(cmp.event.phaseName).toEqual('done');
expect(cmp.event.totalTime).toEqual(999);
expect(cmp.event.fromState).toEqual('void');
expect(cmp.event.toState).toEqual('b');
});
it('should handle callbacks for multiple triggers running simultaneously', () => {
@Component({
selector: 'if-cmp',
template: `
<div [@ani1]="exp1" (@ani1.done)="callback1($event)"></div>
<div [@ani2]="exp2" (@ani2.done)="callback2($event)"></div>
`,
animations: [
trigger(
'ani1',
[
transition(
'* => a', [style({'opacity': '0'}), animate(999, style({'opacity': '1'}))]),
]),
trigger(
'ani2',
[
transition(
'* => b', [style({'width': '0px'}), animate(999, style({'width': '100px'}))]),
])
],
})
class Cmp {
exp1: any = false;
exp2: any = false;
event1: AnimationEvent;
event2: AnimationEvent;
callback1 = (event: any) => { this.event1 = event; };
callback2 = (event: any) => { this.event2 = event; };
}
TestBed.configureTestingModule({declarations: [Cmp]});
const engine = TestBed.get(ɵAnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
cmp.exp1 = 'a';
cmp.exp2 = 'b';
fixture.detectChanges();
engine.flush();
expect(cmp.event1).toBeFalsy();
expect(cmp.event2).toBeFalsy();
const player1 = engine.activePlayers[0];
const player2 = engine.activePlayers[1];
player1.finish();
expect(cmp.event1.triggerName).toBeTruthy('ani1');
expect(cmp.event2).toBeFalsy();
player2.finish();
expect(cmp.event1.triggerName).toBeTruthy('ani1');
expect(cmp.event2.triggerName).toBeTruthy('ani2');
});
it('should handle callbacks for multiple triggers running simultaneously on the same element',
() => {
@Component({
selector: 'if-cmp',
template: `
<div [@ani1]="exp1" (@ani1.done)="callback1($event)" [@ani2]="exp2" (@ani2.done)="callback2($event)"></div>
`,
animations: [
trigger(
'ani1',
[
transition(
'* => a',
[style({'opacity': '0'}), animate(999, style({'opacity': '1'}))]),
]),
trigger(
'ani2',
[
transition(
'* => b',
[style({'width': '0px'}), animate(999, style({'width': '100px'}))]),
])
],
})
class Cmp {
exp1: any = false;
exp2: any = false;
event1: AnimationEvent;
event2: AnimationEvent;
callback1 = (event: any) => { this.event1 = event; };
callback2 = (event: any) => { this.event2 = event; };
}
TestBed.configureTestingModule({declarations: [Cmp]});
const engine = TestBed.get(ɵAnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
cmp.exp1 = 'a';
cmp.exp2 = 'b';
fixture.detectChanges();
engine.flush();
expect(cmp.event1).toBeFalsy();
expect(cmp.event2).toBeFalsy();
const player1 = engine.activePlayers[0];
const player2 = engine.activePlayers[1];
player1.finish();
expect(cmp.event1.triggerName).toBeTruthy('ani1');
expect(cmp.event2).toBeFalsy();
player2.finish();
expect(cmp.event1.triggerName).toBeTruthy('ani1');
expect(cmp.event2.triggerName).toBeTruthy('ani2');
});
xit('should trigger a state change listener for when the animation changes state from void => state on the host element',
() => {
@Component({
selector: 'my-cmp',
template: `...`,
animations: [trigger(
'myAnimation2',
[transition(
'void => *',
[style({'opacity': '0'}), animate(1000, style({'opacity': '1'}))])])],
})
class Cmp {
event: AnimationEvent;
@HostBinding('@myAnimation2')
exp: any = false;
@HostListener('@myAnimation2.start')
callback = (event: any) => { this.event = event; };
}
TestBed.configureTestingModule({declarations: [Cmp]});
const engine = TestBed.get(ɵAnimationEngine);
const fixture = TestBed.createComponent(Cmp);
const cmp = fixture.componentInstance;
cmp.exp = 'TRUE';
fixture.detectChanges();
engine.flush();
expect(cmp.event.triggerName).toEqual('myAnimation2');
expect(cmp.event.phaseName).toEqual('start');
expect(cmp.event.totalTime).toEqual(1000);
expect(cmp.event.fromState).toEqual('void');
expect(cmp.event.toState).toEqual('TRUE');
});
});
});
}
function assertHasParent(element: any, yes: boolean) {
const parent = getDOM().parentElement(element);
if (yes) {
expect(parent).toBeTruthy();
} else {
expect(parent).toBeFalsy();
}
}

View File

@ -10,6 +10,9 @@ System.config({
map: {
'@angular/common': '/vendor/@angular/common/bundles/common.umd.js',
'@angular/compiler': '/vendor/@angular/compiler/bundles/compiler.umd.js',
'@angular/animations': '/vendor/@angular/animations/bundles/animations.umd.js',
'@angular/platform-browser/animations':
'/vendor/@angular/platform-browser/animations/bundles/platform-browser-animations.umd.js',
'@angular/core': '/vendor/@angular/core/bundles/core.umd.js',
'@angular/forms': '/vendor/@angular/forms/bundles/forms.umd.js',
'@angular/http': '/vendor/@angular/forms/bundles/http.umd.js',

View File

@ -52,7 +52,8 @@ export function validateCache(): {exists: string[], unused: string[], reported:
}
missingCache.set('/node_modules/@angular/core.d.ts', true);
missingCache.set('/node_modules/@angular/animation.d.ts', true);
missingCache.set('/node_modules/@angular/animations.d.ts', true);
missingCache.set('/node_modules/@angular/platform-browser/animations.d.ts', true);
missingCache.set('/node_modules/@angular/common.d.ts', true);
missingCache.set('/node_modules/@angular/forms.d.ts', true);
missingCache.set('/node_modules/@angular/core/src/di/provider.metadata.json', true);

View File

@ -11,6 +11,7 @@
"paths": {
"@angular/core": ["../../../dist/packages-dist/core"],
"@angular/animation": ["../../../dist/packages-dist/animation"],
"@angular/animation/browser": ["../../../dist/packages-dist/animation/browser"],
"@angular/core/testing": ["../../../dist/packages-dist/core/testing"],
"@angular/common": ["../../../dist/packages-dist/common"],
"@angular/compiler": ["../../../dist/packages-dist/compiler"],

View File

@ -0,0 +1,16 @@
{
"presets": ["es2015"],
"plugins": [["transform-es2015-modules-umd", {
"globals": {
"@angular/core": "ng.core",
"@angular/animations": "ng.animations",
"@angular/platform-browser": "ng.platformBrowser",
"@angular/platform-browser/animations": "ng.platformBrowser.animations",
"rxjs/Observable": "Rx",
"rxjs/Subject": "Rx"
},
"exactGlobals": true
}]],
"moduleId": "@angular/platform-browser/animations"
}

View File

@ -0,0 +1,17 @@
{
"presets": ["es2015"],
"plugins": [["transform-es2015-modules-umd", {
"globals": {
"@angular/core": "ng.core",
"@angular/animations": "ng.animations",
"@angular/platform-browser": "ng.platformBrowser",
"@angular/platform-browser/animations": "ng.platformBrowser.animations",
"@angular/platform-browser/testing": "ng.platformBrowser.testing",
"rxjs/Observable": "Rx",
"rxjs/Subject": "Rx"
},
"exactGlobals": true
}]],
"moduleId": "@angular/platform-browser/animations/testing"
}

View File

@ -0,0 +1,14 @@
/**
* @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
*/
// This file is not used to build this module. It is only used during editing
// by the TypeScript language serivce and during build for verifcation. `ngc`
// replaces this file with production index.ts when it rewrites private symbol
// names.
export * from './src/animations';

View File

@ -0,0 +1,14 @@
/**
* @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
*/
/**
* @module
* @description
* Entry point for all public APIs of the animation package.
*/
export * from './src/animations';

View File

@ -0,0 +1,16 @@
/**
* @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
*/
/**
* @module
* @description
* Entry point for all animation APIs of the animation browser package.
*/
export {BrowserAnimationModule} from './browser_animation_module';
export {AnimationDriver} from './render/animation_driver';
export * from './private_export';

View File

@ -0,0 +1,57 @@
/**
* @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 {Injectable, NgModule, RendererFactoryV2} from '@angular/core';
import {BrowserModule, ɵDomRendererFactoryV2} from '@angular/platform-browser';
import {AnimationStyleNormalizer} from './dsl/style_normalization/animation_style_normalizer';
import {WebAnimationsStyleNormalizer} from './dsl/style_normalization/web_animations_style_normalizer';
import {AnimationDriver, NoOpAnimationDriver} from './render/animation_driver';
import {AnimationEngine} from './render/animation_engine';
import {AnimationRendererFactory} from './render/animation_renderer';
import {WebAnimationsDriver, supportsWebAnimations} from './render/web_animations/web_animations_driver';
@Injectable()
export class InjectableAnimationEngine extends AnimationEngine {
constructor(driver: AnimationDriver, normalizer: AnimationStyleNormalizer) {
super(driver, normalizer);
}
}
export function instantiateSupportedAnimationDriver() {
if (supportsWebAnimations()) {
return new WebAnimationsDriver();
}
return new NoOpAnimationDriver();
}
export function instantiateDefaultStyleNormalizer() {
return new WebAnimationsStyleNormalizer();
}
export function instantiateRendererFactory(
renderer: ɵDomRendererFactoryV2, engine: AnimationEngine) {
return new AnimationRendererFactory(renderer, engine);
}
/**
* @experimental Animation support is experimental.
*/
@NgModule({
imports: [BrowserModule],
providers: [
{provide: AnimationDriver, useFactory: instantiateSupportedAnimationDriver},
{provide: AnimationStyleNormalizer, useFactory: instantiateDefaultStyleNormalizer},
{provide: AnimationEngine, useClass: InjectableAnimationEngine}, {
provide: RendererFactoryV2,
useFactory: instantiateRendererFactory,
deps: [ɵDomRendererFactoryV2, AnimationEngine]
}
]
})
export class BrowserAnimationModule {
}

View File

@ -5,20 +5,17 @@
* 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, AnimationStyles, Injector} from '@angular/core';
import {StyleData} from '../common/style_data';
import {normalizeStyles} from '../common/util';
import {AnimationDriver} from '../engine/animation_driver';
import {DomAnimationTransitionEngine} from '../engine/dom_animation_transition_engine';
import {AnimationMetadata, sequence} from './animation_metadata';
import {AnimationMetadata, AnimationPlayer, AnimationStyleMetadata, sequence, ɵStyleData} from '@angular/animations';
import {AnimationDriver} from '../render/animation_driver';
import {AnimationEngine} from '../render/animation_engine';
import {normalizeStyles} from '../util';
import {AnimationTimelineInstruction} from './animation_timeline_instruction';
import {buildAnimationKeyframes} from './animation_timeline_visitor';
import {validateAnimationSequence} from './animation_validator_visitor';
import {AnimationStyleNormalizer} from './style_normalization/animation_style_normalizer';
/**
* @experimental Animation support is experimental.
*/
export class Animation {
private _animationAst: AnimationMetadata;
constructor(input: AnimationMetadata|AnimationMetadata[]) {
@ -32,28 +29,27 @@ export class Animation {
this._animationAst = ast;
}
buildTimelines(startingStyles: StyleData|StyleData[], destinationStyles: StyleData|StyleData[]):
AnimationTimelineInstruction[] {
const start = Array.isArray(startingStyles) ?
normalizeStyles(new AnimationStyles(<StyleData[]>startingStyles)) :
<StyleData>startingStyles;
const dest = Array.isArray(destinationStyles) ?
normalizeStyles(new AnimationStyles(<StyleData[]>destinationStyles)) :
<StyleData>destinationStyles;
buildTimelines(
startingStyles: ɵStyleData|ɵStyleData[],
destinationStyles: ɵStyleData|ɵStyleData[]): AnimationTimelineInstruction[] {
const start = Array.isArray(startingStyles) ? normalizeStyles(startingStyles) :
<ɵStyleData>startingStyles;
const dest = Array.isArray(destinationStyles) ? normalizeStyles(destinationStyles) :
<ɵStyleData>destinationStyles;
return buildAnimationKeyframes(this._animationAst, start, dest);
}
// this is only used for development demo purposes for now
private create(
injector: Injector, element: any, startingStyles: StyleData = {},
destinationStyles: StyleData = {}): AnimationPlayer {
injector: any, element: any, startingStyles: ɵStyleData = {},
destinationStyles: ɵStyleData = {}): AnimationPlayer {
const instructions = this.buildTimelines(startingStyles, destinationStyles);
// note the code below is only here to make the tests happy (once the new renderer is
// within core then the code below will interact with Renderer.transition(...))
const driver: AnimationDriver = injector.get(AnimationDriver);
const normalizer: AnimationStyleNormalizer = injector.get(AnimationStyleNormalizer);
const engine = new DomAnimationTransitionEngine(driver, normalizer);
return engine.process(element, instructions);
const engine = new AnimationEngine(driver, normalizer);
return engine.animateTimeline(element, instructions);
}
}

View File

@ -0,0 +1,40 @@
/**
* @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 {AnimationAnimateMetadata, AnimationGroupMetadata, AnimationKeyframesSequenceMetadata, AnimationMetadata, AnimationMetadataType, AnimationSequenceMetadata, AnimationStateMetadata, AnimationStyleMetadata, AnimationTransitionMetadata} from '@angular/animations';
export interface AnimationDslVisitor {
visitState(ast: AnimationStateMetadata, context: any): any;
visitTransition(ast: AnimationTransitionMetadata, context: any): any;
visitSequence(ast: AnimationSequenceMetadata, context: any): any;
visitGroup(ast: AnimationGroupMetadata, context: any): any;
visitAnimate(ast: AnimationAnimateMetadata, context: any): any;
visitStyle(ast: AnimationStyleMetadata, context: any): any;
visitKeyframeSequence(ast: AnimationKeyframesSequenceMetadata, context: any): any;
}
export function visitAnimationNode(
visitor: AnimationDslVisitor, node: AnimationMetadata, context: any) {
switch (node.type) {
case AnimationMetadataType.State:
return visitor.visitState(<AnimationStateMetadata>node, context);
case AnimationMetadataType.Transition:
return visitor.visitTransition(<AnimationTransitionMetadata>node, context);
case AnimationMetadataType.Sequence:
return visitor.visitSequence(<AnimationSequenceMetadata>node, context);
case AnimationMetadataType.Group:
return visitor.visitGroup(<AnimationGroupMetadata>node, context);
case AnimationMetadataType.Animate:
return visitor.visitAnimate(<AnimationAnimateMetadata>node, context);
case AnimationMetadataType.KeyframeSequence:
return visitor.visitKeyframeSequence(<AnimationKeyframesSequenceMetadata>node, context);
case AnimationMetadataType.Style:
return visitor.visitStyle(<AnimationStyleMetadata>node, context);
default:
throw new Error(`Unable to resolve animation metadata node #${node.type}`);
}
}

View File

@ -5,24 +5,25 @@
* 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 {StyleData} from '../common/style_data';
import {AnimationEngineInstruction, AnimationTransitionInstructionType} from '../engine/animation_engine_instruction';
import {ɵStyleData} from '@angular/animations';
import {AnimationEngineInstruction, AnimationTransitionInstructionType} from '../render/animation_engine_instruction';
export interface AnimationTimelineInstruction extends AnimationEngineInstruction {
keyframes: StyleData[];
keyframes: ɵStyleData[];
duration: number;
delay: number;
totalTime: number;
easing: string;
}
export function createTimelineInstruction(
keyframes: StyleData[], duration: number, delay: number,
keyframes: ɵStyleData[], duration: number, delay: number,
easing: string): AnimationTimelineInstruction {
return {
type: AnimationTransitionInstructionType.TimelineAnimation,
keyframes,
duration,
delay,
easing
totalTime: duration + delay, easing
};
}

View File

@ -5,13 +5,15 @@
* 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 {AnimationStyles} from '@angular/core';
import {StyleData} from '../common/style_data';
import {copyStyles, normalizeStyles, parseTimeExpression} from '../common/util';
import {AUTO_STYLE, AnimateTimings, AnimationAnimateMetadata, AnimationGroupMetadata, AnimationKeyframesSequenceMetadata, AnimationMetadata, AnimationMetadataType, AnimationSequenceMetadata, AnimationStateMetadata, AnimationStyleMetadata, AnimationTransitionMetadata, sequence, ɵStyleData} from '@angular/animations';
import {copyStyles, normalizeStyles, parseTimeExpression} from '../util';
import {AnimationDslVisitor, visitAnimationNode} from './animation_dsl_visitor';
import * as meta from './animation_metadata';
import {AnimationTimelineInstruction, createTimelineInstruction} from './animation_timeline_instruction';
/*
* The code within this file aims to generate web-animations-compatible keyframes from Angular's
* animation DSL code.
@ -97,10 +99,10 @@ import {AnimationTimelineInstruction, createTimelineInstruction} from './animati
* the `AnimationValidatorVisitor` code.
*/
export function buildAnimationKeyframes(
ast: meta.AnimationMetadata | meta.AnimationMetadata[], startingStyles: StyleData = {},
finalStyles: StyleData = {}): AnimationTimelineInstruction[] {
const normalizedAst = Array.isArray(ast) ? meta.sequence(<meta.AnimationMetadata[]>ast) :
<meta.AnimationMetadata>ast;
ast: AnimationMetadata | AnimationMetadata[], startingStyles: ɵStyleData = {},
finalStyles: ɵStyleData = {}): AnimationTimelineInstruction[] {
const normalizedAst =
Array.isArray(ast) ? sequence(<AnimationMetadata[]>ast) : <AnimationMetadata>ast;
return new AnimationTimelineVisitor().buildKeyframes(normalizedAst, startingStyles, finalStyles);
}
@ -110,8 +112,8 @@ export declare type StyleAtTime = {
export class AnimationTimelineContext {
currentTimeline: TimelineBuilder;
currentAnimateTimings: meta.AnimateTimings;
previousNode: meta.AnimationMetadata = <meta.AnimationMetadata>{};
currentAnimateTimings: AnimateTimings;
previousNode: AnimationMetadata = <AnimationMetadata>{};
subContextCount = 0;
constructor(
@ -142,7 +144,7 @@ export class AnimationTimelineContext {
}
export class AnimationTimelineVisitor implements AnimationDslVisitor {
buildKeyframes(ast: meta.AnimationMetadata, startingStyles: StyleData, finalStyles: StyleData):
buildKeyframes(ast: AnimationMetadata, startingStyles: ɵStyleData, finalStyles: ɵStyleData):
AnimationTimelineInstruction[] {
const context = new AnimationTimelineContext([], []);
context.currentTimeline.setStyles(startingStyles);
@ -158,7 +160,7 @@ export class AnimationTimelineVisitor implements AnimationDslVisitor {
context.currentTimeline.properties.forEach(prop => {
const val = normalizedFinalStyles[prop];
if (val == null) {
normalizedFinalStyles[prop] = meta.AUTO_STYLE;
normalizedFinalStyles[prop] = AUTO_STYLE;
}
});
}
@ -178,17 +180,17 @@ export class AnimationTimelineVisitor implements AnimationDslVisitor {
return timelineInstructions;
}
visitState(ast: meta.AnimationStateMetadata, context: any): any {
visitState(ast: AnimationStateMetadata, context: any): any {
// these values are not visited in this AST
}
visitTransition(ast: meta.AnimationTransitionMetadata, context: any): any {
visitTransition(ast: AnimationTransitionMetadata, context: any): any {
// these values are not visited in this AST
}
visitSequence(ast: meta.AnimationSequenceMetadata, context: AnimationTimelineContext) {
visitSequence(ast: AnimationSequenceMetadata, context: AnimationTimelineContext) {
const subContextCount = context.subContextCount;
if (context.previousNode.type == meta.AnimationMetadataType.Style) {
if (context.previousNode.type == AnimationMetadataType.Style) {
context.currentTimeline.forwardFrame();
context.currentTimeline.snapshotCurrentStyles();
}
@ -205,7 +207,7 @@ export class AnimationTimelineVisitor implements AnimationDslVisitor {
context.previousNode = ast;
}
visitGroup(ast: meta.AnimationGroupMetadata, context: AnimationTimelineContext) {
visitGroup(ast: AnimationGroupMetadata, context: AnimationTimelineContext) {
const innerTimelines: TimelineBuilder[] = [];
let furthestTime = context.currentTimeline.currentTime;
ast.steps.forEach(s => {
@ -224,9 +226,9 @@ export class AnimationTimelineVisitor implements AnimationDslVisitor {
context.previousNode = ast;
}
visitAnimate(ast: meta.AnimationAnimateMetadata, context: AnimationTimelineContext) {
visitAnimate(ast: AnimationAnimateMetadata, context: AnimationTimelineContext) {
const timings = ast.timings.hasOwnProperty('duration') ?
<meta.AnimateTimings>ast.timings :
<AnimateTimings>ast.timings :
parseTimeExpression(<string|number>ast.timings, context.errors);
context.currentAnimateTimings = timings;
@ -236,12 +238,12 @@ export class AnimationTimelineVisitor implements AnimationDslVisitor {
}
const astType = ast.styles ? ast.styles.type : -1;
if (astType == meta.AnimationMetadataType.KeyframeSequence) {
this.visitKeyframeSequence(<meta.AnimationKeyframesSequenceMetadata>ast.styles, context);
if (astType == AnimationMetadataType.KeyframeSequence) {
this.visitKeyframeSequence(<AnimationKeyframesSequenceMetadata>ast.styles, context);
} else {
context.incrementTime(timings.duration);
if (astType == meta.AnimationMetadataType.Style) {
this.visitStyle(<meta.AnimationStyleMetadata>ast.styles, context);
if (astType == AnimationMetadataType.Style) {
this.visitStyle(<AnimationStyleMetadata>ast.styles, context);
}
}
@ -249,17 +251,17 @@ export class AnimationTimelineVisitor implements AnimationDslVisitor {
context.previousNode = ast;
}
visitStyle(ast: meta.AnimationStyleMetadata, context: AnimationTimelineContext) {
visitStyle(ast: AnimationStyleMetadata, context: AnimationTimelineContext) {
// this is a special case when a style() call is issued directly after
// a call to animate(). If the clock is not forwarded by one frame then
// the style() calls will be merged into the previous animate() call
// which is incorrect.
if (!context.currentAnimateTimings &&
context.previousNode.type == meta.AnimationMetadataType.Animate) {
context.previousNode.type == AnimationMetadataType.Animate) {
context.currentTimeline.forwardFrame();
}
const normalizedStyles = normalizeStyles(new AnimationStyles(ast.styles));
const normalizedStyles = normalizeStyles(ast.styles);
const easing = context.currentAnimateTimings && context.currentAnimateTimings.easing;
if (easing) {
normalizedStyles['easing'] = easing;
@ -270,7 +272,7 @@ export class AnimationTimelineVisitor implements AnimationDslVisitor {
}
visitKeyframeSequence(
ast: meta.AnimationKeyframesSequenceMetadata, context: AnimationTimelineContext) {
ast: AnimationKeyframesSequenceMetadata, context: AnimationTimelineContext) {
const MAX_KEYFRAME_OFFSET = 1;
const limit = ast.steps.length - 1;
const firstKeyframe = ast.steps[0];
@ -287,8 +289,8 @@ export class AnimationTimelineVisitor implements AnimationDslVisitor {
const innerTimeline = innerContext.currentTimeline;
innerTimeline.easing = context.currentAnimateTimings.easing;
ast.steps.forEach((step: meta.AnimationStyleMetadata, i: number) => {
const normalizedStyles = normalizeStyles(new AnimationStyles(step.styles));
ast.steps.forEach((step: AnimationStyleMetadata, i: number) => {
const normalizedStyles = normalizeStyles(step.styles);
const offset = containsOffsets ? <number>normalizedStyles['offset'] :
(i == limit ? MAX_KEYFRAME_OFFSET : i * offsetGap);
innerTimeline.forwardTime(offset * duration);
@ -309,13 +311,13 @@ export class AnimationTimelineVisitor implements AnimationDslVisitor {
export class TimelineBuilder {
public duration: number = 0;
public easing: string = '';
private _currentKeyframe: StyleData;
private _keyframes = new Map<number, StyleData>();
private _currentKeyframe: ɵStyleData;
private _keyframes = new Map<number, ɵStyleData>();
private _styleSummary: {[prop: string]: StyleAtTime} = {};
private _localTimelineStyles: StyleData;
private _backFill: StyleData = {};
private _localTimelineStyles: ɵStyleData;
private _backFill: ɵStyleData = {};
constructor(public startTime: number, private _globalTimelineStyles: StyleData = null) {
constructor(public startTime: number, private _globalTimelineStyles: ɵStyleData = null) {
this._localTimelineStyles = Object.create(this._backFill, {});
if (!this._globalTimelineStyles) {
this._globalTimelineStyles = this._localTimelineStyles;
@ -357,13 +359,13 @@ export class TimelineBuilder {
}
}
setStyles(styles: StyleData) {
setStyles(styles: ɵStyleData) {
Object.keys(styles).forEach(prop => {
if (prop !== 'offset') {
const val = styles[prop];
this._currentKeyframe[prop] = val;
if (prop !== 'easing' && !this._localTimelineStyles[prop]) {
this._backFill[prop] = this._globalTimelineStyles[prop] || meta.AUTO_STYLE;
this._backFill[prop] = this._globalTimelineStyles[prop] || AUTO_STYLE;
}
this._updateStyle(prop, val);
}
@ -398,7 +400,7 @@ export class TimelineBuilder {
}
buildKeyframes(): AnimationTimelineInstruction {
const finalKeyframes: StyleData[] = [];
const finalKeyframes: ɵStyleData[] = [];
// special case for when there are only start/destination
// styles but no actual animation animate steps...
if (this.duration == 0) {

View File

@ -5,20 +5,18 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {TransitionFactory} from '@angular/core';
import {StyleData} from '../common/style_data';
import {AnimationMetadata, AnimationTransitionMetadata} from './animation_metadata';
import {AnimationMetadata, AnimationTransitionMetadata, ɵStyleData} from '@angular/animations';
import {buildAnimationKeyframes} from './animation_timeline_visitor';
import {TransitionMatcherFn} from './animation_transition_expr';
import {AnimationTransitionInstruction, createTransitionInstruction} from './animation_transition_instruction';
export class AnimationTransitionFactory implements TransitionFactory {
export class AnimationTransitionFactory {
private _animationAst: AnimationMetadata;
constructor(
private _triggerName: string, ast: AnimationTransitionMetadata,
private matchFns: TransitionMatcherFn[],
private _stateStyles: {[stateName: string]: StyleData}) {
private _stateStyles: {[stateName: string]: ɵStyleData}) {
this._animationAst = ast.animation;
}
@ -33,7 +31,8 @@ export class AnimationTransitionFactory implements TransitionFactory {
buildAnimationKeyframes(this._animationAst, currentStateStyles, nextStateStyles);
return createTransitionInstruction(
this._triggerName, nextState === 'void', currentStateStyles, nextStateStyles, timelines);
this._triggerName, currentState, nextState, nextState === 'void', currentStateStyles,
nextStateStyles, timelines);
}
}

View File

@ -5,26 +5,31 @@
* 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 {StyleData} from '../common/style_data';
import {AnimationEngineInstruction, AnimationTransitionInstructionType} from '../engine/animation_engine_instruction';
import {ɵStyleData} from '@angular/animations';
import {AnimationEngineInstruction, AnimationTransitionInstructionType} from '../render/animation_engine_instruction';
import {AnimationTimelineInstruction} from './animation_timeline_instruction';
export interface AnimationTransitionInstruction extends AnimationEngineInstruction {
triggerName: string;
isRemovalTransition: boolean;
fromStyles: StyleData;
toStyles: StyleData;
fromState: string;
fromStyles: ɵStyleData;
toState: string;
toStyles: ɵStyleData;
timelines: AnimationTimelineInstruction[];
}
export function createTransitionInstruction(
triggerName: string, isRemovalTransition: boolean, fromStyles: StyleData, toStyles: StyleData,
triggerName: string, fromState: string, toState: string, isRemovalTransition: boolean,
fromStyles: ɵStyleData, toStyles: ɵStyleData,
timelines: AnimationTimelineInstruction[]): AnimationTransitionInstruction {
return {
type: AnimationTransitionInstructionType.TransitionAnimation,
triggerName,
isRemovalTransition,
fromState,
fromStyles,
toState,
toStyles,
timelines
};

View File

@ -5,81 +5,33 @@
* 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 {AnimationStyles, Trigger} from '@angular/core';
import {StyleData} from '../common/style_data';
import {copyStyles, normalizeStyles} from '../common/util';
import {AnimationAnimateMetadata, AnimationGroupMetadata, AnimationKeyframesSequenceMetadata, AnimationMetadata, AnimationSequenceMetadata, AnimationStateMetadata, AnimationStyleMetadata, AnimationTransitionMetadata, ɵStyleData} from '@angular/animations';
import {copyStyles, normalizeStyles} from '../util';
import {AnimationDslVisitor, visitAnimationNode} from './animation_dsl_visitor';
import {AnimationAnimateMetadata, AnimationGroupMetadata, AnimationKeyframesSequenceMetadata, AnimationMetadata, AnimationSequenceMetadata, AnimationStateMetadata, AnimationStyleMetadata, AnimationTransitionMetadata} from './animation_metadata';
import {parseTransitionExpr} from './animation_transition_expr';
import {AnimationTransitionFactory} from './animation_transition_factory';
import {AnimationTransitionInstruction} from './animation_transition_instruction';
import {AnimationTransitionInstruction, createTransitionInstruction} from './animation_transition_instruction';
import {validateAnimationSequence} from './animation_validator_visitor';
/**
* `trigger` is an animation-specific function that is designed to be used inside of Angular2's
animation DSL language. If this information is new, please navigate to the {@link
Component#animations-anchor component animations metadata page} to gain a better understanding of
how animations in Angular2 are used.
*
* `trigger` Creates an animation trigger which will a list of {@link state state} and {@link
transition transition} entries that will be evaluated when the expression bound to the trigger
changes.
*
* Triggers are registered within the component annotation data under the {@link
Component#animations-anchor animations section}. An animation trigger can be placed on an element
within a template by referencing the name of the trigger followed by the expression value that the
trigger is bound to (in the form of `[@triggerName]="expression"`.
*
* ### Usage
*
* `trigger` will create an animation trigger reference based on the provided `name` value. The
provided `animation` value is expected to be an array consisting of {@link state state} and {@link
transition transition} declarations.
*
* ```typescript
* @Component({
* selector: 'my-component',
* templateUrl: 'my-component-tpl.html',
* animations: [
* trigger("myAnimationTrigger", [
* state(...),
* state(...),
* transition(...),
* transition(...)
* ])
* ]
* })
* class MyComponent {
* myStatusExp = "something";
* }
* ```
*
* The template associated with this component will make use of the `myAnimationTrigger` animation
* trigger by binding to an element within its template code.
*
* ```html
* <!-- somewhere inside of my-component-tpl.html -->
* <div [@myAnimationTrigger]="myStatusExp">...</div>
* ```
*
* {@example core/animation/ts/dsl/animation_example.ts region='Component'}
*
* @experimental Animation support is experimental.
*/
export function trigger(name: string, definitions: AnimationMetadata[]): AnimationTrigger {
export function buildTrigger(name: string, definitions: AnimationMetadata[]): AnimationTrigger {
return new AnimationTriggerVisitor().buildTrigger(name, definitions);
}
/**
* @experimental Animation support is experimental.
*/
export class AnimationTrigger implements Trigger {
export class AnimationTrigger {
public transitionFactories: AnimationTransitionFactory[] = [];
public states: {[stateName: string]: StyleData} = {};
public states: {[stateName: string]: ɵStyleData} = {};
constructor(
public name: string, states: {[stateName: string]: StyleData},
public name: string, states: {[stateName: string]: ɵStyleData},
private _transitionAsts: AnimationTransitionMetadata[]) {
Object.keys(states).forEach(
stateName => { this.states[stateName] = copyStyles(states[stateName], false); });
@ -103,18 +55,26 @@ export class AnimationTrigger implements Trigger {
}
}
createFallbackInstruction(currentState: any, nextState: any): AnimationTransitionInstruction {
const backupStateStyles = this.states['*'] || {};
const currentStateStyles = this.states[currentState] || backupStateStyles;
const nextStateStyles = this.states[nextState] || backupStateStyles;
return createTransitionInstruction(
this.name, currentState, nextState, nextState == 'void', currentStateStyles,
nextStateStyles, []);
}
matchTransition(currentState: any, nextState: any): AnimationTransitionInstruction {
for (let i = 0; i < this.transitionFactories.length; i++) {
let result = this.transitionFactories[i].match(currentState, nextState);
if (result) return result;
}
return null;
}
}
class AnimationTriggerContext {
public errors: string[] = [];
public states: {[stateName: string]: StyleData} = {};
public states: {[stateName: string]: ɵStyleData} = {};
public transitions: AnimationTransitionMetadata[] = [];
}
@ -126,7 +86,7 @@ class AnimationTriggerVisitor implements AnimationDslVisitor {
}
visitState(ast: AnimationStateMetadata, context: any): any {
context.states[ast.name] = normalizeStyles(new AnimationStyles(ast.styles.styles));
context.states[ast.name] = normalizeStyles(ast.styles.styles);
}
visitTransition(ast: AnimationTransitionMetadata, context: any): any {

View File

@ -5,10 +5,11 @@
* 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 {AnimationStyles} from '@angular/core';
import {normalizeStyles, parseTimeExpression} from '../common/util';
import {AnimateTimings, AnimationAnimateMetadata, AnimationGroupMetadata, AnimationKeyframesSequenceMetadata, AnimationMetadata, AnimationMetadataType, AnimationSequenceMetadata, AnimationStateMetadata, AnimationStyleMetadata, AnimationTransitionMetadata} from '@angular/animations';
import {normalizeStyles, parseTimeExpression} from '../util';
import {AnimationDslVisitor, visitAnimationNode} from './animation_dsl_visitor';
import * as meta from './animation_metadata';
export type StyleTimeTuple = {
startTime: number; endTime: number;
@ -50,26 +51,26 @@ export type StyleTimeTuple = {
*
* Otherwise an error will be thrown.
*/
export function validateAnimationSequence(ast: meta.AnimationMetadata) {
export function validateAnimationSequence(ast: AnimationMetadata) {
return new AnimationValidatorVisitor().validate(ast);
}
export class AnimationValidatorVisitor implements AnimationDslVisitor {
validate(ast: meta.AnimationMetadata): string[] {
validate(ast: AnimationMetadata): string[] {
const context = new AnimationValidatorContext();
visitAnimationNode(this, ast, context);
return context.errors;
}
visitState(ast: meta.AnimationStateMetadata, context: any): any {}
visitState(ast: AnimationStateMetadata, context: any): any {}
visitTransition(ast: meta.AnimationTransitionMetadata, context: any): any {}
visitTransition(ast: AnimationTransitionMetadata, context: any): any {}
visitSequence(ast: meta.AnimationSequenceMetadata, context: AnimationValidatorContext): any {
visitSequence(ast: AnimationSequenceMetadata, context: AnimationValidatorContext): any {
ast.steps.forEach(step => visitAnimationNode(this, step, context));
}
visitGroup(ast: meta.AnimationGroupMetadata, context: AnimationValidatorContext): any {
visitGroup(ast: AnimationGroupMetadata, context: AnimationValidatorContext): any {
const currentTime = context.currentTime;
let furthestTime = 0;
ast.steps.forEach(step => {
@ -80,28 +81,28 @@ export class AnimationValidatorVisitor implements AnimationDslVisitor {
context.currentTime = furthestTime;
}
visitAnimate(ast: meta.AnimationAnimateMetadata, context: AnimationValidatorContext): any {
visitAnimate(ast: AnimationAnimateMetadata, context: AnimationValidatorContext): any {
// we reassign the timings here so that they are not reparsed each
// time an animation occurs
context.currentAnimateTimings = ast.timings =
parseTimeExpression(<string|number>ast.timings, context.errors);
const astType = ast.styles && ast.styles.type;
if (astType == meta.AnimationMetadataType.KeyframeSequence) {
this.visitKeyframeSequence(<meta.AnimationKeyframesSequenceMetadata>ast.styles, context);
if (astType == AnimationMetadataType.KeyframeSequence) {
this.visitKeyframeSequence(<AnimationKeyframesSequenceMetadata>ast.styles, context);
} else {
context.currentTime +=
context.currentAnimateTimings.duration + context.currentAnimateTimings.delay;
if (astType == meta.AnimationMetadataType.Style) {
this.visitStyle(<meta.AnimationStyleMetadata>ast.styles, context);
if (astType == AnimationMetadataType.Style) {
this.visitStyle(<AnimationStyleMetadata>ast.styles, context);
}
}
context.currentAnimateTimings = null;
}
visitStyle(ast: meta.AnimationStyleMetadata, context: AnimationValidatorContext): any {
const styleData = normalizeStyles(new AnimationStyles(ast.styles));
visitStyle(ast: AnimationStyleMetadata, context: AnimationValidatorContext): any {
const styleData = normalizeStyles(ast.styles);
const timings = context.currentAnimateTimings;
let endTime = context.currentTime;
let startTime = context.currentTime;
@ -131,14 +132,14 @@ export class AnimationValidatorVisitor implements AnimationDslVisitor {
}
visitKeyframeSequence(
ast: meta.AnimationKeyframesSequenceMetadata, context: AnimationValidatorContext): any {
ast: AnimationKeyframesSequenceMetadata, context: AnimationValidatorContext): any {
let totalKeyframesWithOffsets = 0;
const offsets: number[] = [];
let offsetsOutOfOrder = false;
let keyframesOutOfRange = false;
let previousOffset: number = 0;
ast.steps.forEach(step => {
const styleData = normalizeStyles(new AnimationStyles(step.styles));
const styleData = normalizeStyles(step.styles);
let offset = 0;
if (styleData.hasOwnProperty('offset')) {
totalKeyframesWithOffsets++;
@ -183,6 +184,6 @@ export class AnimationValidatorVisitor implements AnimationDslVisitor {
export class AnimationValidatorContext {
public errors: string[] = [];
public currentTime: number = 0;
public currentAnimateTimings: meta.AnimateTimings;
public currentAnimateTimings: AnimateTimings;
public collectedStyles: {[propName: string]: StyleTimeTuple} = {};
}

View File

@ -5,6 +5,10 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/**
* @experimental Animation support is experimental.
*/
export abstract class AnimationStyleNormalizer {
abstract normalizePropertyName(propertyName: string, errors: string[]): string;
abstract normalizeStyleValue(
@ -12,6 +16,9 @@ export abstract class AnimationStyleNormalizer {
errors: string[]): string;
}
/**
* @experimental Animation support is experimental.
*/
export class NoOpAnimationStyleNormalizer {
normalizePropertyName(propertyName: string, errors: string[]): string { return propertyName; }

View File

@ -0,0 +1,11 @@
/**
* @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
*/
export {Animation as ɵAnimation} from './dsl/animation';
export {AnimationStyleNormalizer as ɵAnimationStyleNormalizer} from './dsl/style_normalization/animation_style_normalizer';
export {AnimationEngine as ɵAnimationEngine} from './render/animation_engine';
export {AnimationRenderer as ɵAnimationRenderer, AnimationRendererFactory as ɵAnimationRendererFactory} from './render/animation_renderer';

View File

@ -0,0 +1,32 @@
/**
* @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, NoOpAnimationPlayer} from '@angular/animations';
/**
* @experimental
*/
export class NoOpAnimationDriver implements AnimationDriver {
animate(
element: any, keyframes: {[key: string]: string | number}[], duration: number, delay: number,
easing: string, previousPlayers: any[] = []): AnimationPlayer {
return new NoOpAnimationPlayer();
}
}
/**
* @experimental
*/
export abstract class AnimationDriver {
static NOOP: AnimationDriver = new NoOpAnimationDriver();
abstract animate(
element: any, keyframes: {[key: string]: string | number}[], duration: number, delay: number,
easing: string, previousPlayers?: any[]): any;
}

View File

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

View File

@ -5,10 +5,6 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {TransitionInstruction} from '@angular/core';
export const enum AnimationTransitionInstructionType {TransitionAnimation, TimelineAnimation}
export interface AnimationEngineInstruction extends TransitionInstruction {
type: AnimationTransitionInstructionType;
}
export interface AnimationEngineInstruction { type: AnimationTransitionInstructionType; }

View File

@ -0,0 +1,134 @@
/**
* @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 {AnimationTriggerMetadata} from '@angular/animations';
import {Injectable, RendererFactoryV2, RendererTypeV2, RendererV2} from '@angular/core';
import {AnimationEngine} from './animation_engine';
@Injectable()
export class AnimationRendererFactory implements RendererFactoryV2 {
constructor(private delegate: RendererFactoryV2, private _engine: AnimationEngine) {}
createRenderer(hostElement: any, type: RendererTypeV2): RendererV2 {
let delegate = this.delegate.createRenderer(hostElement, type);
if (!hostElement || !type) return delegate;
let animationRenderer = type.data['__animationRenderer__'] as any as AnimationRenderer;
if (animationRenderer && delegate == animationRenderer.delegate) {
return animationRenderer;
}
const animationTriggers = type.data['animation'] as AnimationTriggerMetadata[];
animationRenderer = (type.data as any)['__animationRenderer__'] =
new AnimationRenderer(delegate, this._engine, animationTriggers);
return animationRenderer;
}
}
export class AnimationRenderer implements RendererV2 {
public destroyNode: (node: any) => (void|any) = null;
constructor(
public delegate: RendererV2, private _engine: AnimationEngine,
_triggers: AnimationTriggerMetadata[] = null) {
this.destroyNode = this.delegate.destroyNode ? (n) => delegate.destroyNode(n) : null;
if (_triggers) {
_triggers.forEach(trigger => _engine.registerTrigger(trigger));
}
}
destroy(): void { this.delegate.destroy(); }
createElement(name: string, namespace?: string): any {
return this.delegate.createElement(name, namespace);
}
createComment(value: string): any { return this.delegate.createComment(value); }
createText(value: string): any { return this.delegate.createText(value); }
selectRootElement(selectorOrNode: string|any): any {
return this.delegate.selectRootElement(selectorOrNode);
}
parentNode(node: any): any { return this.delegate.parentNode(node); }
nextSibling(node: any): any { return this.delegate.nextSibling(node); }
setAttribute(el: any, name: string, value: string, namespace?: string): void {
this.delegate.setAttribute(el, name, value, namespace);
}
removeAttribute(el: any, name: string, namespace?: string): void {
this.delegate.removeAttribute(el, name, namespace);
}
addClass(el: any, name: string): void { this.delegate.addClass(el, name); }
removeClass(el: any, name: string): void { this.delegate.removeClass(el, name); }
setStyle(el: any, style: string, value: any, hasVendorPrefix: boolean, hasImportant: boolean):
void {
this.delegate.setStyle(el, style, value, hasVendorPrefix, hasImportant);
}
removeStyle(el: any, style: string, hasVendorPrefix: boolean): void {
this.delegate.removeStyle(el, style, hasVendorPrefix);
}
setValue(node: any, value: string): void { this.delegate.setValue(node, value); }
appendChild(parent: any, newChild: any): void {
this._engine.onInsert(newChild, () => this.delegate.appendChild(parent, newChild));
}
insertBefore(parent: any, newChild: any, refChild: any): void {
this._engine.onInsert(newChild, () => this.delegate.insertBefore(parent, newChild, refChild));
}
removeChild(parent: any, oldChild: any): void {
this._engine.onRemove(oldChild, () => this.delegate.removeChild(parent, oldChild));
}
setProperty(el: any, name: string, value: any): void {
if (name.charAt(0) == '@') {
this._engine.setProperty(el, name.substr(1), value);
} else {
this.delegate.setProperty(el, name, value);
}
}
listen(target: 'window'|'document'|'body'|any, eventName: string, callback: (event: any) => any):
() => void {
if (eventName.charAt(0) == '@') {
const element = resolveElementFromTarget(target);
const [name, phase] = parseTriggerCallbackName(eventName.substr(1));
return this._engine.listen(element, name, phase, callback);
}
return this.delegate.listen(target, eventName, callback);
}
}
function resolveElementFromTarget(target: 'window' | 'document' | 'body' | any): any {
switch (target) {
case 'body':
return document.body;
case 'document':
return document;
case 'window':
return window;
default:
return target;
}
}
function parseTriggerCallbackName(triggerName: string) {
const dotIndex = triggerName.indexOf('.');
const trigger = triggerName.substring(0, dotIndex);
const phase = triggerName.substr(dotIndex + 1);
return [trigger, phase];
}

View File

@ -5,16 +5,15 @@
* 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} from '@angular/core';
import {AnimationPlayer, ɵStyleData} from '@angular/animations';
import {StyleData} from '../../common/style_data';
import {AnimationDriver} from '../animation_driver';
import {WebAnimationsPlayer} from './web_animations_player';
export class WebAnimationsDriver implements AnimationDriver {
animate(
element: any, keyframes: StyleData[], duration: number, delay: number, easing: string,
element: any, keyframes: ɵStyleData[], duration: number, delay: number, easing: string,
previousPlayers: AnimationPlayer[] = []): WebAnimationsPlayer {
const playerOptions: {[key: string]: string |
number} = {'duration': duration, 'delay': delay, 'fill': 'forwards'};

View File

@ -5,9 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {AUTO_STYLE, AnimationPlayer} from '@angular/core';
import {AUTO_STYLE, AnimationPlayer} from '@angular/animations';
import {DOMAnimation} from './dom_animation';
export class WebAnimationsPlayer implements AnimationPlayer {

View File

@ -5,9 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {AnimationStyles} from '@angular/core';
import {AnimateTimings} from './../dsl/animation_metadata';
import {StyleData} from './style_data';
import {AnimateTimings, ɵStyleData} from '@angular/animations';
export const ONE_SECOND = 1000;
@ -51,14 +49,14 @@ export function parseTimeExpression(exp: string | number, errors: string[]): Ani
return {duration, delay, easing};
}
export function normalizeStyles(styles: AnimationStyles): StyleData {
const normalizedStyles: StyleData = {};
styles.styles.forEach((styleMap: any) => copyStyles(styleMap, false, normalizedStyles));
export function normalizeStyles(styles: ɵStyleData[]): ɵStyleData {
const normalizedStyles: ɵStyleData = {};
styles.forEach(data => copyStyles(data, false, normalizedStyles));
return normalizedStyles;
}
export function copyStyles(
styles: StyleData, readPrototype: boolean, destination: StyleData = {}): StyleData {
styles: ɵStyleData, readPrototype: boolean, destination: ɵStyleData = {}): ɵStyleData {
if (readPrototype) {
// we make use of a for-in loop so that the
// prototypically inherited properties are

View File

@ -5,10 +5,9 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {AUTO_STYLE, AnimationMetadata, animate, group, keyframes, sequence, style, ɵStyleData} from '@angular/animations';
import {StyleData} from '../../src/common/style_data';
import {Animation} from '../../src/dsl/animation';
import {AUTO_STYLE, AnimationMetadata, animate, group, keyframes, sequence, style} from '../../src/dsl/animation_metadata';
import {AnimationTimelineInstruction} from '../../src/dsl/animation_timeline_instruction';
import {validateAnimationSequence} from '../../src/dsl/animation_validator_visitor';
@ -561,9 +560,9 @@ export function main() {
it('should create an empty animation if there are zero animation steps', () => {
const steps: AnimationMetadata[] = [];
const fromStyles: StyleData[] = [{background: 'blue', height: 100}];
const fromStyles: ɵStyleData[] = [{background: 'blue', height: 100}];
const toStyles: StyleData[] = [{background: 'red'}];
const toStyles: ɵStyleData[] = [{background: 'red'}];
const player = invokeAnimationSequence(steps, fromStyles, toStyles)[0];
expect(player.duration).toEqual(0);
@ -574,9 +573,9 @@ export function main() {
() => {
const steps: AnimationMetadata[] = [animate(1000)];
const fromStyles: StyleData[] = [{background: 'blue', height: 100}];
const fromStyles: ɵStyleData[] = [{background: 'blue', height: 100}];
const toStyles: StyleData[] = [{background: 'red'}];
const toStyles: ɵStyleData[] = [{background: 'red'}];
const players = invokeAnimationSequence(steps, fromStyles, toStyles);
expect(players[0].keyframes).toEqual([
@ -589,7 +588,7 @@ export function main() {
});
}
function humanizeOffsets(keyframes: StyleData[], digits: number = 3): StyleData[] {
function humanizeOffsets(keyframes: ɵStyleData[], digits: number = 3): ɵStyleData[] {
return keyframes.map(keyframe => {
keyframe['offset'] = Number(parseFloat(<any>keyframe['offset']).toFixed(digits));
return keyframe;
@ -597,8 +596,8 @@ function humanizeOffsets(keyframes: StyleData[], digits: number = 3): StyleData[
}
function invokeAnimationSequence(
steps: AnimationMetadata | AnimationMetadata[], startingStyles: StyleData[] = [],
destinationStyles: StyleData[] = []): AnimationTimelineInstruction[] {
steps: AnimationMetadata | AnimationMetadata[], startingStyles: ɵStyleData[] = [],
destinationStyles: ɵStyleData[] = []): AnimationTimelineInstruction[] {
return new Animation(steps).buildTimelines(startingStyles, destinationStyles);
}

View File

@ -6,35 +6,42 @@
* found in the LICENSE file at https://angular.io/license
*/
import {animate, state, style, transition} from '../../src/dsl/animation_metadata';
import {trigger} from '../../src/dsl/animation_trigger';
import {animate, state, style, transition, trigger} from '@angular/animations';
import {buildTrigger} from '../../src/dsl/animation_trigger';
function makeTrigger(name: string, steps: any) {
const triggerData = trigger(name, steps);
const triggerInstance = buildTrigger(triggerData.name, triggerData.definitions);
return triggerInstance;
}
export function main() {
describe('AnimationTrigger', () => {
describe('trigger validation', () => {
it('should group errors together for an animation trigger', () => {
expect(() => {
trigger('myTrigger', [transition('12345', animate(3333))]);
makeTrigger('myTrigger', [transition('12345', animate(3333))]);
}).toThrowError(/Animation parsing for the myTrigger trigger have failed/);
});
it('should throw an error when a transition within a trigger contains an invalid expression',
() => {
expect(() => { trigger('name', [transition('somethingThatIsWrong', animate(3333))]); })
expect(
() => { makeTrigger('name', [transition('somethingThatIsWrong', animate(3333))]); })
.toThrowError(
/- The provided transition expression "somethingThatIsWrong" is not supported/);
});
it('should throw an error if an animation alias is used that is not yet supported', () => {
expect(() => {
trigger('name', [transition(':angular', animate(3333))]);
makeTrigger('name', [transition(':angular', animate(3333))]);
}).toThrowError(/- The transition alias value ":angular" is not supported/);
});
});
describe('trigger usage', () => {
it('should construct a trigger based on the states and transition data', () => {
const result = trigger('name', [
const result = makeTrigger('name', [
state('on', style({width: 0})), state('off', style({width: 100})),
transition('on => off', animate(1000)), transition('off => on', animate(1000))
]);
@ -45,7 +52,7 @@ export function main() {
});
it('should find the first transition that matches', () => {
const result = trigger(
const result = makeTrigger(
'name', [transition('a => b', animate(1234)), transition('b => c', animate(5678))]);
const trans = result.matchTransition('b', 'c');
@ -55,7 +62,7 @@ export function main() {
});
it('should find a transition with a `*` value', () => {
const result = trigger('name', [
const result = makeTrigger('name', [
transition('* => b', animate(1234)), transition('b => *', animate(5678)),
transition('* => *', animate(9999))
]);
@ -71,7 +78,7 @@ export function main() {
});
it('should null when no results are found', () => {
const result = trigger('name', [transition('a => b', animate(1111))]);
const result = makeTrigger('name', [transition('a => b', animate(1111))]);
const trans = result.matchTransition('b', 'a');
expect(trans).toBeFalsy();
@ -80,7 +87,7 @@ export function main() {
it('should allow a function to be used as a predicate for the transition', () => {
let returnValue = false;
const result = trigger('name', [transition((from, to) => returnValue, animate(1111))]);
const result = makeTrigger('name', [transition((from, to) => returnValue, animate(1111))]);
expect(result.matchTransition('a', 'b')).toBeFalsy();
expect(result.matchTransition('1', 2)).toBeFalsy();
@ -102,7 +109,7 @@ export function main() {
};
}
const result = trigger('name', [
const result = makeTrigger('name', [
transition(countAndReturn(false), animate(1111)),
transition(countAndReturn(false), animate(2222)),
transition(countAndReturn(true), animate(3333)),
@ -116,7 +123,7 @@ export function main() {
});
it('should support bi-directional transition expressions', () => {
const result = trigger('name', [transition('a <=> b', animate(2222))]);
const result = makeTrigger('name', [transition('a <=> b', animate(2222))]);
const t1 = result.matchTransition('a', 'b');
expect(t1.timelines[0].duration).toEqual(2222);
@ -126,7 +133,7 @@ export function main() {
});
it('should support multiple transition statements in one string', () => {
const result = trigger('name', [transition('a => b, b => a, c => *', animate(1234))]);
const result = makeTrigger('name', [transition('a => b, b => a, c => *', animate(1234))]);
const t1 = result.matchTransition('a', 'b');
expect(t1.timelines[0].duration).toEqual(1234);
@ -140,14 +147,14 @@ export function main() {
describe('aliases', () => {
it('should alias the :enter transition as void => *', () => {
const result = trigger('name', [transition(':enter', animate(3333))]);
const result = makeTrigger('name', [transition(':enter', animate(3333))]);
const trans = result.matchTransition('void', 'something');
expect(trans.timelines[0].duration).toEqual(3333);
});
it('should alias the :leave transition as * => void', () => {
const result = trigger('name', [transition(':leave', animate(3333))]);
const result = makeTrigger('name', [transition(':leave', animate(3333))]);
const trans = result.matchTransition('something', 'void');
expect(trans.timelines[0].duration).toEqual(3333);

View File

@ -0,0 +1,761 @@
/**
* @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, NoOpAnimationPlayer, animate, keyframes, state, style, transition, trigger} from '@angular/animations';
import {fakeAsync, flushMicrotasks} from '@angular/core/testing';
import {el} from '@angular/platform-browser/testing/browser_util';
import {buildAnimationKeyframes} from '../../src/dsl/animation_timeline_visitor';
import {buildTrigger} from '../../src/dsl/animation_trigger';
import {AnimationStyleNormalizer, NoOpAnimationStyleNormalizer} from '../../src/dsl/style_normalization/animation_style_normalizer';
import {AnimationEngine} from '../../src/render/animation_engine';
import {MockAnimationDriver, MockAnimationPlayer} from '../../testing/mock_animation_driver';
function makeTrigger(name: string, steps: any) {
const triggerData = trigger(name, steps);
const triggerInstance = buildTrigger(triggerData.name, triggerData.definitions);
return triggerInstance;
}
export function main() {
const driver = new MockAnimationDriver();
// these tests are only mean't to be run within the DOM
if (typeof Element == 'undefined') return;
describe('AnimationEngine', () => {
let element: any;
beforeEach(() => {
MockAnimationDriver.log = [];
element = el('<div></div>');
});
function makeEngine(normalizer: AnimationStyleNormalizer = null) {
return new AnimationEngine(driver, normalizer || new NoOpAnimationStyleNormalizer());
}
describe('trigger registration', () => {
it('should throw an error if the same trigger is registered twice', () => {
const engine = makeEngine();
engine.registerTrigger(trigger('trig', []));
expect(() => {
engine.registerTrigger(trigger('trig', []));
}).toThrowError(/The provided animation trigger "trig" has already been registered!/);
});
});
describe('property setting', () => {
it('should invoke a transition based on a property change', () => {
const engine = makeEngine();
const trig = trigger('myTrigger', [
transition('* => *', [style({height: '0px'}), animate(1000, style({height: '100px'}))])
]);
engine.registerTrigger(trig);
expect(engine.queuedPlayers.length).toEqual(0);
engine.setProperty(element, 'myTrigger', 'value');
expect(engine.queuedPlayers.length).toEqual(1);
const player = MockAnimationDriver.log.pop() as MockAnimationPlayer;
expect(player.keyframes).toEqual([
{height: '0px', offset: 0}, {height: '100px', offset: 1}
]);
});
it('should always invoke an animation even if the property change is not matched', () => {
const engine = makeEngine();
const trig = trigger(
'myTrigger',
[transition(
'yes => no', [style({height: '0px'}), animate(1000, style({height: '100px'}))])]);
engine.registerTrigger(trig);
expect(engine.queuedPlayers.length).toEqual(0);
engine.setProperty(element, 'myTrigger', 'no');
expect(engine.queuedPlayers.length).toEqual(1);
expect(engine.queuedPlayers.pop() instanceof NoOpAnimationPlayer).toBe(true);
engine.flush();
engine.setProperty(element, 'myTrigger', 'yes');
expect(engine.queuedPlayers.length).toEqual(1);
expect(engine.queuedPlayers.pop() instanceof NoOpAnimationPlayer).toBe(true);
});
it('should not queue an animation if the property value has not changed at all', () => {
const engine = makeEngine();
const trig = trigger('myTrigger', [
transition('* => *', [style({height: '0px'}), animate(1000, style({height: '100px'}))])
]);
engine.registerTrigger(trig);
expect(engine.queuedPlayers.length).toEqual(0);
engine.setProperty(element, 'myTrigger', 'abc');
expect(engine.queuedPlayers.length).toEqual(1);
engine.setProperty(element, 'myTrigger', 'abc');
expect(engine.queuedPlayers.length).toEqual(1);
});
it('should throw an error if an animation property without a matching trigger is changed',
() => {
const engine = makeEngine();
expect(() => {
engine.setProperty(element, 'myTrigger', 'no');
}).toThrowError(/The provided animation trigger "myTrigger" has not been registered!/);
});
});
describe('event listeners', () => {
it('should listen to the onStart operation for the animation', () => {
const engine = makeEngine();
const trig = trigger('myTrigger', [
transition('* => *', [style({height: '0px'}), animate(1000, style({height: '100px'}))])
]);
let count = 0;
engine.registerTrigger(trig);
engine.listen(element, 'myTrigger', 'start', () => count++);
engine.setProperty(element, 'myTrigger', 'value');
expect(count).toEqual(0);
engine.flush();
expect(count).toEqual(1);
});
it('should listen to the onDone operation for the animation', () => {
const engine = makeEngine();
const trig = trigger('myTrigger', [
transition('* => *', [style({height: '0px'}), animate(1000, style({height: '100px'}))])
]);
let count = 0;
engine.registerTrigger(trig);
engine.listen(element, 'myTrigger', 'done', () => count++);
engine.setProperty(element, 'myTrigger', 'value');
expect(count).toEqual(0);
engine.flush();
expect(count).toEqual(0);
const player = engine.activePlayers.pop();
player.finish();
expect(count).toEqual(1);
});
it('should throw an error when an event is listened to that isn\'t supported', () => {
const engine = makeEngine();
const trig = trigger('myTrigger', []);
engine.registerTrigger(trig);
expect(() => { engine.listen(element, 'myTrigger', 'explode', () => {}); })
.toThrowError(
/The provided animation trigger event "explode" for the animation trigger "myTrigger" is not supported!/);
});
it('should throw an error when an event is listened for a trigger that doesn\'t exist', () => {
const engine = makeEngine();
expect(() => { engine.listen(element, 'myTrigger', 'explode', () => {}); })
.toThrowError(
/Unable to listen on the animation trigger event "explode" because the animation trigger "myTrigger" doesn\'t exist!/);
});
it('should throw an error when an undefined event is listened for', () => {
const engine = makeEngine();
const trig = trigger('myTrigger', []);
engine.registerTrigger(trig);
expect(() => { engine.listen(element, 'myTrigger', '', () => {}); })
.toThrowError(
/Unable to listen on the animation trigger "myTrigger" because the provided event is undefined!/);
});
it('should retain event listeners and call them for sucessive animation state changes',
() => {
const engine = makeEngine();
const trig = trigger(
'myTrigger',
[transition(
'* => *', [style({height: '0px'}), animate(1000, style({height: '100px'}))])]);
engine.registerTrigger(trig);
let count = 0;
engine.listen(element, 'myTrigger', 'start', () => count++);
engine.setProperty(element, 'myTrigger', '123');
engine.flush();
expect(count).toEqual(1);
engine.setProperty(element, 'myTrigger', '456');
engine.flush();
expect(count).toEqual(2);
});
it('should only fire event listener changes for when the corresponding trigger changes state',
() => {
const engine = makeEngine();
const trig1 = trigger(
'myTrigger1',
[transition(
'* => 123', [style({height: '0px'}), animate(1000, style({height: '100px'}))])]);
engine.registerTrigger(trig1);
const trig2 = trigger(
'myTrigger2',
[transition(
'* => 123', [style({width: '0px'}), animate(1000, style({width: '100px'}))])]);
engine.registerTrigger(trig2);
let count = 0;
engine.listen(element, 'myTrigger1', 'start', () => count++);
engine.setProperty(element, 'myTrigger1', '123');
engine.flush();
expect(count).toEqual(1);
engine.setProperty(element, 'myTrigger2', '123');
engine.flush();
expect(count).toEqual(1);
});
it('should allow a listener to be deregistered', () => {
const engine = makeEngine();
const trig = trigger(
'myTrigger',
[transition(
'* => 123', [style({height: '0px'}), animate(1000, style({height: '100px'}))])]);
engine.registerTrigger(trig);
let count = 0;
const deregisterFn = engine.listen(element, 'myTrigger', 'start', () => count++);
engine.setProperty(element, 'myTrigger', '123');
engine.flush();
expect(count).toEqual(1);
deregisterFn();
engine.setProperty(element, 'myTrigger', '456');
engine.flush();
expect(count).toEqual(1);
});
it('should trigger a listener callback with an AnimationEvent argument', () => {
const engine = makeEngine();
engine.registerTrigger(trigger(
'myTrigger',
[transition(
'* => *', [style({height: '0px'}), animate(1234, style({height: '100px'}))])]));
// we do this so that the next transition has a starting value that isnt null
engine.setProperty(element, 'myTrigger', '123');
engine.flush();
let capture: AnimationEvent = null;
engine.listen(element, 'myTrigger', 'start', (e) => capture = e);
engine.listen(element, 'myTrigger', 'done', (e) => capture = e);
engine.setProperty(element, 'myTrigger', '456');
engine.flush();
expect(capture).toEqual({
element,
triggerName: 'myTrigger',
phaseName: 'start',
fromState: '123',
toState: '456',
totalTime: 1234
});
capture = null;
const player = engine.activePlayers.pop();
player.finish();
expect(capture).toEqual({
element,
triggerName: 'myTrigger',
phaseName: 'done',
fromState: '123',
toState: '456',
totalTime: 1234
});
});
});
describe('flushing animations', () => {
let ticks: (() => any)[];
let _raf: () => any;
beforeEach(() => {
ticks = [];
_raf = <() => any>AnimationEngine.raf;
AnimationEngine.raf = (cb: () => any) => { ticks.push(cb); };
});
afterEach(() => AnimationEngine.raf = _raf);
function flushTicks() {
ticks.forEach(tick => tick());
ticks = [];
}
it('should invoke queued transition animations after a requestAnimationFrame flushes', () => {
const engine = makeEngine();
engine.registerTrigger(trigger('myTrigger', [transition('* => *', animate(1234))]));
engine.setProperty(element, 'myTrigger', 'on');
expect(engine.queuedPlayers.length).toEqual(1);
expect(engine.activePlayers.length).toEqual(0);
flushTicks();
expect(engine.queuedPlayers.length).toEqual(0);
expect(engine.activePlayers.length).toEqual(1);
});
it('should not flush the animations twice when flushed right away before a frame changes',
() => {
const engine = makeEngine();
engine.registerTrigger(trigger('myTrigger', [transition('* => *', animate(1234))]));
engine.setProperty(element, 'myTrigger', 'on');
expect(engine.activePlayers.length).toEqual(0);
engine.flush();
expect(engine.activePlayers.length).toEqual(1);
flushTicks();
expect(engine.activePlayers.length).toEqual(1);
});
});
describe('instructions', () => {
it('should animate a transition instruction', () => {
const engine = makeEngine();
const trig = makeTrigger('something', [
state('on', style({height: 100})), state('off', style({height: 0})),
transition('on => off', animate(9876))
]);
const instruction = trig.matchTransition('on', 'off');
expect(MockAnimationDriver.log.length).toEqual(0);
engine.animateTransition(element, instruction);
expect(MockAnimationDriver.log.length).toEqual(1);
});
it('should animate a timeline instruction', () => {
const engine = makeEngine();
const timelines =
buildAnimationKeyframes([style({height: 100}), animate(1000, style({height: 0}))]);
expect(MockAnimationDriver.log.length).toEqual(0);
engine.animateTimeline(element, timelines);
expect(MockAnimationDriver.log.length).toEqual(1);
});
it('should animate an array of animation instructions', () => {
const engine = makeEngine();
const instructions = buildAnimationKeyframes([
style({height: 100}), animate(1000, style({height: 0})),
animate(1000, keyframes([style({width: 0}), style({width: 1000})]))
]);
expect(MockAnimationDriver.log.length).toEqual(0);
engine.animateTimeline(element, instructions);
expect(MockAnimationDriver.log.length).toBeGreaterThan(0);
});
});
describe('transition operations', () => {
it('should persist the styles on the element as actual styles once the animation is complete',
() => {
const engine = makeEngine();
const trig = makeTrigger('something', [
state('on', style({height: '100px'})), state('off', style({height: '0px'})),
transition('on => off', animate(9876))
]);
const instruction = trig.matchTransition('on', 'off');
const player = engine.animateTransition(element, instruction);
expect(element.style.height).not.toEqual('0px');
player.finish();
expect(element.style.height).toEqual('0px');
});
it('should remove all existing state styling from an element when a follow-up transition occurs on the same trigger',
() => {
const engine = makeEngine();
const trig = makeTrigger('something', [
state('a', style({height: '100px'})), state('b', style({height: '500px'})),
state('c', style({width: '200px'})), transition('* => *', animate(9876))
]);
const instruction1 = trig.matchTransition('a', 'b');
const player1 = engine.animateTransition(element, instruction1);
player1.finish();
expect(element.style.height).toEqual('500px');
const instruction2 = trig.matchTransition('b', 'c');
const player2 = engine.animateTransition(element, instruction2);
expect(element.style.height).not.toEqual('500px');
player2.finish();
expect(element.style.width).toEqual('200px');
expect(element.style.height).not.toEqual('500px');
});
it('should allow two animation transitions with different triggers to animate in parallel',
() => {
const engine = makeEngine();
const trig1 = makeTrigger('something1', [
state('a', style({width: '100px'})), state('b', style({width: '200px'})),
transition('* => *', animate(1000))
]);
const trig2 = makeTrigger('something2', [
state('x', style({height: '500px'})), state('y', style({height: '1000px'})),
transition('* => *', animate(2000))
]);
let doneCount = 0;
function doneCallback() { doneCount++; }
const instruction1 = trig1.matchTransition('a', 'b');
const instruction2 = trig2.matchTransition('x', 'y');
const player1 = engine.animateTransition(element, instruction1);
player1.onDone(doneCallback);
expect(doneCount).toEqual(0);
const player2 = engine.animateTransition(element, instruction2);
player2.onDone(doneCallback);
expect(doneCount).toEqual(0);
player1.finish();
expect(doneCount).toEqual(1);
player2.finish();
expect(doneCount).toEqual(2);
expect(element.style.width).toEqual('200px');
expect(element.style.height).toEqual('1000px');
});
it('should cancel a previously running animation when a follow-up transition kicks off on the same trigger',
() => {
const engine = makeEngine();
const trig = makeTrigger('something', [
state('x', style({opacity: 0})), state('y', style({opacity: .5})),
state('z', style({opacity: 1})), transition('* => *', animate(1000))
]);
const instruction1 = trig.matchTransition('x', 'y');
const instruction2 = trig.matchTransition('y', 'z');
expect(parseFloat(element.style.opacity)).not.toEqual(.5);
const player1 = engine.animateTransition(element, instruction1);
const player2 = engine.animateTransition(element, instruction2);
expect(parseFloat(element.style.opacity)).toEqual(.5);
player2.finish();
expect(parseFloat(element.style.opacity)).toEqual(1);
player1.finish();
expect(parseFloat(element.style.opacity)).toEqual(1);
});
it('should pass in the previously running players into the follow-up transition player when cancelled',
() => {
const engine = makeEngine();
const trig = makeTrigger('something', [
state('x', style({opacity: 0})), state('y', style({opacity: .5})),
state('z', style({opacity: 1})), transition('* => *', animate(1000))
]);
const instruction1 = trig.matchTransition('x', 'y');
const instruction2 = trig.matchTransition('y', 'z');
const instruction3 = trig.matchTransition('z', 'x');
const player1 = engine.animateTransition(element, instruction1);
engine.flush();
player1.setPosition(0.5);
const player2 = <MockAnimationPlayer>engine.animateTransition(element, instruction2);
expect(player2.previousPlayers).toEqual([player1]);
player2.finish();
const player3 = <MockAnimationPlayer>engine.animateTransition(element, instruction3);
expect(player3.previousPlayers).toEqual([]);
});
it('should cancel all existing players if a removal animation is set to occur', () => {
const engine = makeEngine();
const trig = makeTrigger('something', [
state('m', style({opacity: 0})), state('n', style({opacity: 1})),
transition('* => *', animate(1000))
]);
let doneCount = 0;
function doneCallback() { doneCount++; }
const instruction1 = trig.matchTransition('m', 'n');
const instructions2 =
buildAnimationKeyframes([style({height: 0}), animate(1000, style({height: 100}))]);
const instruction3 = trig.matchTransition('n', 'void');
const player1 = engine.animateTransition(element, instruction1);
player1.onDone(doneCallback);
const player2 = engine.animateTimeline(element, instructions2);
player2.onDone(doneCallback);
engine.flush();
expect(doneCount).toEqual(0);
const player3 = engine.animateTransition(element, instruction3);
expect(doneCount).toEqual(2);
});
it('should only persist styles that exist in the final state styles and not the last keyframe',
() => {
const engine = makeEngine();
const trig = makeTrigger('something', [
state('0', style({width: '0px'})), state('1', style({width: '100px'})),
transition('* => *', [animate(1000, style({height: '200px'}))])
]);
const instruction = trig.matchTransition('0', '1');
const player = engine.animateTransition(element, instruction);
expect(element.style.width).not.toEqual('100px');
player.finish();
expect(element.style.height).not.toEqual('200px');
expect(element.style.width).toEqual('100px');
});
it('should default to using styling from the `*` state if a matching state is not found',
() => {
const engine = makeEngine();
const trig = makeTrigger('something', [
state('a', style({opacity: 0})), state('*', style({opacity: .5})),
transition('* => *', animate(1000))
]);
const instruction = trig.matchTransition('a', 'z');
engine.animateTransition(element, instruction).finish();
expect(parseFloat(element.style.opacity)).toEqual(.5);
});
it('should treat `void` as `void`', () => {
const engine = makeEngine();
const trig = makeTrigger('something', [
state('a', style({opacity: 0})), state('void', style({opacity: .8})),
transition('* => *', animate(1000))
]);
const instruction = trig.matchTransition('a', 'void');
engine.animateTransition(element, instruction).finish();
expect(parseFloat(element.style.opacity)).toEqual(.8);
});
});
describe('timeline operations', () => {
it('should not destroy timeline-based animations after they have finished', () => {
const engine = makeEngine();
const log: string[] = [];
function capture(value: string) {
return () => { log.push(value); };
}
const instructions =
buildAnimationKeyframes([style({height: 0}), animate(1000, style({height: 500}))]);
const player = engine.animateTimeline(element, instructions);
player.onDone(capture('done'));
player.onDestroy(capture('destroy'));
expect(log).toEqual([]);
player.finish();
expect(log).toEqual(['done']);
player.destroy();
expect(log).toEqual(['done', 'destroy']);
});
});
describe('style normalizer', () => {
it('should normalize the style values that are animateTransitioned within an a transition animation',
() => {
const engine = makeEngine(new SuffixNormalizer('-normalized'));
const trig = makeTrigger('something', [
state('on', style({height: 100})), state('off', style({height: 0})),
transition('on => off', animate(9876))
]);
const instruction = trig.matchTransition('on', 'off');
const player = <MockAnimationPlayer>engine.animateTransition(element, instruction);
expect(player.keyframes).toEqual([
{'height-normalized': '100-normalized', offset: 0},
{'height-normalized': '0-normalized', offset: 1}
]);
});
it('should normalize the style values that are animateTransitioned within an a timeline animation',
() => {
const engine = makeEngine(new SuffixNormalizer('-normalized'));
const instructions = buildAnimationKeyframes([
style({width: '333px'}),
animate(1000, style({width: '999px'})),
]);
const player = <MockAnimationPlayer>engine.animateTimeline(element, instructions);
expect(player.keyframes).toEqual([
{'width-normalized': '333px-normalized', offset: 0},
{'width-normalized': '999px-normalized', offset: 1}
]);
});
it('should throw an error when normalization fails within a transition animation', () => {
const engine = makeEngine(new ExactCssValueNormalizer({left: '100px'}));
const trig = makeTrigger('something', [
state('a', style({left: '0px', width: '200px'})),
state('b', style({left: '100px', width: '100px'})), transition('a => b', animate(9876))
]);
const instruction = trig.matchTransition('a', 'b');
let errorMessage = '';
try {
engine.animateTransition(element, instruction);
} catch (e) {
errorMessage = e.toString();
}
expect(errorMessage).toMatch(/Unable to animate due to the following errors:/);
expect(errorMessage).toMatch(/- The CSS property `left` is not allowed to be `0px`/);
expect(errorMessage).toMatch(/- The CSS property `width` is not allowed/);
});
});
describe('view operations', () => {
it('should perform insert operations immediately ', () => {
const engine = makeEngine();
let container = <any>el('<div></div>');
let child1 = <any>el('<div></div>');
let child2 = <any>el('<div></div>');
engine.onInsert(container, () => container.appendChild(child1));
engine.onInsert(container, () => container.appendChild(child2));
expect(container.contains(child1)).toBe(true);
expect(container.contains(child2)).toBe(true);
});
it('should queue up all `remove` DOM operations until all animations are complete', () => {
let container = <any>el('<div></div>');
let targetContainer = <any>el('<div></div>');
let otherContainer = <any>el('<div></div>');
let child1 = <any>el('<div></div>');
let child2 = <any>el('<div></div>');
container.appendChild(targetContainer);
container.appendChild(otherContainer);
targetContainer.appendChild(child1);
targetContainer.appendChild(child2);
/*----------------*
container
/ \
target other
/ \
c1 c2
*----------------*/
expect(container.contains(otherContainer)).toBe(true);
const engine = makeEngine();
engine.onRemove(child1, () => targetContainer.removeChild(child1));
engine.onRemove(child2, () => targetContainer.removeChild(child2));
engine.onRemove(otherContainer, () => container.removeChild(otherContainer));
expect(container.contains(child1)).toBe(true);
expect(container.contains(child2)).toBe(true);
expect(container.contains(otherContainer)).toBe(true);
const instructions =
buildAnimationKeyframes([style({height: 0}), animate(1000, style({height: 100}))]);
const player = engine.animateTimeline(targetContainer, instructions);
expect(container.contains(child1)).toBe(true);
expect(container.contains(child2)).toBe(true);
expect(container.contains(otherContainer)).toBe(true);
engine.flush();
expect(container.contains(child1)).toBe(true);
expect(container.contains(child2)).toBe(true);
expect(container.contains(otherContainer)).toBe(false);
player.finish();
expect(container.contains(child1)).toBe(false);
expect(container.contains(child2)).toBe(false);
expect(container.contains(otherContainer)).toBe(false);
});
});
});
}
class SuffixNormalizer extends AnimationStyleNormalizer {
constructor(private _suffix: string) { super(); }
normalizePropertyName(propertyName: string, errors: string[]): string {
return propertyName + this._suffix;
}
normalizeStyleValue(
userProvidedProperty: string, normalizedProperty: string, value: string|number,
errors: string[]): string {
return value + this._suffix;
}
}
class ExactCssValueNormalizer extends AnimationStyleNormalizer {
constructor(private _allowedValues: {[propName: string]: any}) { super(); }
normalizePropertyName(propertyName: string, errors: string[]): string {
if (!this._allowedValues[propertyName]) {
errors.push(`The CSS property \`${propertyName}\` is not allowed`);
}
return propertyName;
}
normalizeStyleValue(
userProvidedProperty: string, normalizedProperty: string, value: string|number,
errors: string[]): string {
const expectedValue = this._allowedValues[userProvidedProperty];
if (expectedValue != value) {
errors.push(`The CSS property \`${userProvidedProperty}\` is not allowed to be \`${value}\``);
}
return expectedValue;
}
}

View File

@ -5,10 +5,4 @@
* 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
*/
/**
* @module
* @description
* Entry point for all public APIs of the animation/testing package.
*/
export {MockAnimationDriver, MockAnimationPlayer} from './mock_animation_driver';

View File

@ -0,0 +1,80 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {AUTO_STYLE, AnimationPlayer, NoOpAnimationPlayer, ɵStyleData} from '@angular/animations';
import {AnimationDriver} from '../src/render/animation_driver';
/**
* @experimental Animation support is experimental.
*/
export class MockAnimationDriver implements AnimationDriver {
static log: AnimationPlayer[] = [];
animate(
element: any, keyframes: {[key: string]: string | number}[], duration: number, delay: number,
easing: string, previousPlayers: any[] = []): MockAnimationPlayer {
const player =
new MockAnimationPlayer(element, keyframes, duration, delay, easing, previousPlayers);
MockAnimationDriver.log.push(<AnimationPlayer>player);
return player;
}
}
/**
* @experimental Animation support is experimental.
*/
export class MockAnimationPlayer extends NoOpAnimationPlayer {
private __finished = false;
public previousStyles: {[key: string]: string | number} = {};
constructor(
public element: any, public keyframes: {[key: string]: string | number}[],
public duration: number, public delay: number, public easing: string,
public previousPlayers: any[]) {
super();
previousPlayers.forEach(player => {
if (player instanceof MockAnimationPlayer) {
const styles = player._captureStyles();
Object.keys(styles).forEach(prop => { this.previousStyles[prop] = styles[prop]; });
}
});
}
finish(): void {
super.finish();
this.__finished = true;
}
destroy(): void {
super.destroy();
this.__finished = true;
}
private _captureStyles(): {[styleName: string]: string | number} {
const captures: ɵStyleData = {};
Object.keys(this.previousStyles).forEach(prop => {
captures[prop] = this.previousStyles[prop];
});
if (this.hasStarted()) {
// when assembling the captured styles, it's important that
// we build the keyframe styles in the following order:
// {other styles within keyframes, ... previousStyles }
this.keyframes.forEach(kf => {
Object.keys(kf).forEach(prop => {
if (prop != 'offset') {
captures[prop] = this.__finished ? kf[prop] : AUTO_STYLE;
}
});
});
}
return captures;
}
}

View File

@ -0,0 +1,146 @@
/**
* @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 {AnimationTriggerMetadata, trigger} from '@angular/animations';
import {Injectable, RendererFactoryV2, RendererTypeV2} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {BrowserAnimationModule, ɵAnimationEngine, ɵAnimationRendererFactory} from '@angular/platform-browser/animations';
import {BrowserModule} from '../../src/browser';
import {el} from '../../testing/browser_util';
export function main() {
describe('ɵAnimationRenderer', () => {
let element: any;
beforeEach(() => {
element = el('<div></div>');
TestBed.configureTestingModule({
providers: [{provide: ɵAnimationEngine, useClass: MockAnimationEngine}],
imports: [BrowserModule, BrowserAnimationModule]
});
});
function makeRenderer(animationTriggers: any[] = []) {
const type = <RendererTypeV2>{
id: 'id',
encapsulation: null,
styles: [],
data: {'animation': animationTriggers}
};
return (TestBed.get(RendererFactoryV2) as ɵAnimationRendererFactory)
.createRenderer(element, type);
}
it('should register the provided triggers with the view engine when created', () => {
const renderer = makeRenderer([trigger('trig1', []), trigger('trig2', [])]);
const engine = TestBed.get(ɵAnimationEngine) as MockAnimationEngine;
expect(engine.triggers.map(t => t.name)).toEqual(['trig1', 'trig2']);
});
it('should hook into the engine\'s insert operations when appending children', () => {
const renderer = makeRenderer();
const engine = TestBed.get(ɵAnimationEngine) as MockAnimationEngine;
const container = el('<div></div>');
renderer.appendChild(container, element);
expect(engine.captures['onInsert'].pop()).toEqual([element]);
});
it('should hook into the engine\'s insert operations when inserting a child before another',
() => {
const renderer = makeRenderer();
const engine = TestBed.get(ɵAnimationEngine) as MockAnimationEngine;
const container = el('<div></div>');
const element2 = el('<div></div>');
container.appendChild(element2);
renderer.insertBefore(container, element, element2);
expect(engine.captures['onInsert'].pop()).toEqual([element]);
});
it('should hook into the engine\'s insert operations when removing children', () => {
const renderer = makeRenderer();
const engine = TestBed.get(ɵAnimationEngine) as MockAnimationEngine;
const container = el('<div></div>');
renderer.removeChild(container, element);
expect(engine.captures['onRemove'].pop()).toEqual([element]);
});
it('should hook into the engine\'s setProperty call if the property begins with `@`', () => {
const renderer = makeRenderer();
const engine = TestBed.get(ɵAnimationEngine) as MockAnimationEngine;
renderer.setProperty(element, 'prop', 'value');
expect(engine.captures['setProperty']).toBeFalsy();
renderer.setProperty(element, '@prop', 'value');
expect(engine.captures['setProperty'].pop()).toEqual([element, 'prop', 'value']);
});
describe('listen', () => {
it('should hook into the engine\'s listen call if the property begins with `@`', () => {
const renderer = makeRenderer();
const engine = TestBed.get(ɵAnimationEngine) as MockAnimationEngine;
const cb = (event: any): boolean => { return true; };
renderer.listen(element, 'event', cb);
expect(engine.captures['listen']).toBeFalsy();
renderer.listen(element, '@event.phase', cb);
expect(engine.captures['listen'].pop()).toEqual([element, 'event', 'phase', cb]);
});
it('should resolve the body|document|window nodes given their values as strings as input',
() => {
const renderer = makeRenderer();
const engine = TestBed.get(ɵAnimationEngine) as MockAnimationEngine;
const cb = (event: any): boolean => { return true; };
renderer.listen('body', '@event', cb);
expect(engine.captures['listen'].pop()[0]).toBe(document.body);
renderer.listen('document', '@event', cb);
expect(engine.captures['listen'].pop()[0]).toBe(document);
renderer.listen('window', '@event', cb);
expect(engine.captures['listen'].pop()[0]).toBe(window);
});
});
});
}
@Injectable()
class MockAnimationEngine extends ɵAnimationEngine {
captures: {[method: string]: any[]} = {};
triggers: AnimationTriggerMetadata[] = [];
private _capture(name: string, args: any[]) {
const data = this.captures[name] = this.captures[name] || [];
data.push(args);
}
registerTrigger(trigger: AnimationTriggerMetadata) { this.triggers.push(trigger); }
onInsert(element: any, domFn: () => any): void { this._capture('onInsert', [element]); }
onRemove(element: any, domFn: () => any): void { this._capture('onRemove', [element]); }
setProperty(element: any, property: string, value: any): void {
this._capture('setProperty', [element, property, value]);
}
listen(element: any, eventName: string, eventPhase: string, callback: (event: any) => any):
() => void {
this._capture('listen', [element, eventName, eventPhase, callback]);
return () => {};
}
}

View File

@ -0,0 +1,24 @@
{
"extends": "./tsconfig-build",
"compilerOptions": {
"outDir": "../../../dist/packages-dist/platform-browser",
"paths": {
"@angular/core": ["../../../dist/packages-dist/core"],
"@angular/core/testing": ["../../../dist/packages-dist/core/testing"],
"@angular/animations": ["../../../dist/packages-dist/animations"],
"@angular/platform-browser": ["../../../dist/packages-dist/platform-browser"],
"@angular/platform-browser/animations": ["../../../dist/packages-dist/platform-browser/animations"],
"@angular/common": ["../../../dist/packages-dist/common"],
"@angular/common/testing": ["../../../dist/packages-dist/common/testing"]
}
},
"files": [
"animations/testing/index.ts",
"../../../node_modules/@types/hammerjs/index.d.ts",
"../../../node_modules/@types/jasmine/index.d.ts",
"../../../node_modules/zone.js/dist/zone.js.d.ts"
],
"angularCompilerOptions": {
"strictMetadataEmit": true
}
}

View File

@ -0,0 +1,25 @@
{
"extends": "./tsconfig-build",
"compilerOptions": {
"outDir": "../../../dist/packages-dist/platform-browser",
"paths": {
"rxjs/*": ["../../../node_modules/rxjs/*"],
"@angular/core": ["../../../dist/packages-dist/core"],
"@angular/core/testing": ["../../../dist/packages-dist/core/testing"],
"@angular/animations": ["../../../dist/packages-dist/animations"],
"@angular/platform-browser": ["../../../dist/packages-dist/platform-browser"]
}
},
"files": [
"animations/public_api.ts",
"../../../node_modules/zone.js/dist/zone.js.d.ts",
"../../system.d.ts"
],
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
"strictMetadataEmit": true,
"flatModuleOutFile": "index.js",
"flatModuleId": "@angular/platform-browser/animations"
}
}

View File

@ -9,6 +9,7 @@
"outDir": "../../../dist/packages-dist/platform-browser",
"paths": {
"@angular/core": ["../../../dist/packages-dist/core"],
"@angular/platform-browser/animations": ["../../../dist/packages-dist/platform-browser/animations"],
"@angular/common": ["../../../dist/packages-dist/common"]
},
"rootDir": ".",

View File

@ -24,7 +24,9 @@
defaultJSExtensions: true,
map: {
'@angular/core': '/packages-dist/core/bundles/core.umd.js',
'@angular/animation': '/packages-dist/common/bundles/animation.umd.js',
'@angular/animations': '/packages-dist/common/bundles/animations.umd.js',
'@angular/platform-browser/animations':
'/packages-dist/platform-browser/bundles/platform-browser-animations.umd.js',
'@angular/common': '/packages-dist/common/bundles/common.umd.js',
'@angular/forms': '/packages-dist/forms/bundles/forms.umd.js',
'@angular/compiler': '/packages-dist/compiler/bundles/compiler.umd.js',
@ -52,7 +54,8 @@
map: {'@angular': '/all/@angular', 'rxjs': '/all/benchmarks/vendor/rxjs'},
packages: {
'@angular/core': {main: 'index.js', defaultExtension: 'js'},
'@angular/animation': {main: 'index.js', defaultExtension: 'js'},
'@angular/animations': {main: 'index.js', defaultExtension: 'js'},
'@angular/platform-browser/animations': {main: 'index.js', defaultExtension: 'js'},
'@angular/compiler': {main: 'index.js', defaultExtension: 'js'},
'@angular/router': {main: 'index.js', defaultExtension: 'js'},
'@angular/common': {main: 'index.js', defaultExtension: 'js'},

View File

@ -25,7 +25,9 @@
map: {
'index': 'index.js',
'@angular/common': '/packages-dist/common/bundles/common.umd.js',
'@angular/animation': '/packages-dist/common/bundles/animation.umd.js',
'@angular/animations': '/packages-dist/animation/bundles/animations.umd.js',
'@angular/platform-browser/animations':
'/packages-dist/platform-browser/animations/bundles/platform-browser-animations.umd.js',
'@angular/compiler': '/packages-dist/compiler/bundles/compiler.umd.js',
'@angular/core': '/packages-dist/core/bundles/core.umd.js',
'@angular/forms': '/packages-dist/forms/bundles/forms.umd.js',
@ -61,7 +63,7 @@
packages: {
'app': {defaultExtension: 'js'},
'@angular/common': {main: 'index.js', defaultExtension: 'js'},
'@angular/animation': {main: 'index.js', defaultExtension: 'js'},
'@angular/animations': {main: 'index.js', defaultExtension: 'js'},
'@angular/compiler': {main: 'index.js', defaultExtension: 'js'},
'@angular/core': {main: 'index.js', defaultExtension: 'js'},
'@angular/forms': {main: 'index.js', defaultExtension: 'js'},

View File

@ -33,6 +33,8 @@ System.config({
packages: {
'@angular/core/testing': {main: 'index.js', defaultExtension: 'js'},
'@angular/core': {main: 'index.js', defaultExtension: 'js'},
'@angular/animations/testing': {main: 'index.js', defaultExtension: 'js'},
'@angular/animations': {main: 'index.js', defaultExtension: 'js'},
'@angular/compiler/testing': {main: 'index.js', defaultExtension: 'js'},
'@angular/compiler': {main: 'index.js', defaultExtension: 'js'},
'@angular/common/testing': {main: 'index.js', defaultExtension: 'js'},
@ -45,6 +47,8 @@ System.config({
'@angular/http/testing': {main: 'index.js', defaultExtension: 'js'},
'@angular/http': {main: 'index.js', defaultExtension: 'js'},
'@angular/upgrade': {main: 'index.js', defaultExtension: 'js'},
'@angular/platform-browser/animations/testing': {main: 'index.js', defaultExtension: 'js'},
'@angular/platform-browser/animations': {main: 'index.js', defaultExtension: 'js'},
'@angular/platform-browser/testing': {main: 'index.js', defaultExtension: 'js'},
'@angular/platform-browser': {main: 'index.js', defaultExtension: 'js'},
'@angular/platform-browser-dynamic/testing': {main: 'index.js', defaultExtension: 'js'},

View File

@ -20,7 +20,9 @@ const entrypoints = [
'dist/packages-dist/http/typings/http.d.ts',
'dist/packages-dist/http/typings/testing/testing.d.ts',
'dist/packages-dist/forms/typings/forms.d.ts', 'dist/packages-dist/router/typings/router.d.ts',
'dist/packages-dist/animation/typings/animation.d.ts'
'dist/packages-dist/animations/typings/animations.d.ts',
'dist/packages-dist/platform-browser/typings/animations/animations.d.ts',
'dist/packages-dist/platform-browser/typings/animations/testing/testing.d.ts'
];
const publicApiDir = 'tools/public_api_guard';

View File

@ -0,0 +1,147 @@
/** @experimental */
export declare function animate(timings: string | number, styles?: AnimationStyleMetadata | AnimationKeyframesSequenceMetadata): AnimationAnimateMetadata;
/** @experimental */
export declare type AnimateTimings = {
duration: number;
delay: number;
easing: string;
};
/** @experimental */
export interface AnimationAnimateMetadata extends AnimationMetadata {
styles: AnimationStyleMetadata | AnimationKeyframesSequenceMetadata;
timings: string | number | AnimateTimings;
}
/** @experimental */
export interface AnimationEvent {
element: any;
fromState: string;
phaseName: string;
toState: string;
totalTime: number;
triggerName: string;
}
/** @experimental */
export interface AnimationGroupMetadata extends AnimationMetadata {
steps: AnimationMetadata[];
}
/** @experimental */
export interface AnimationKeyframesSequenceMetadata extends AnimationMetadata {
steps: AnimationStyleMetadata[];
}
/** @experimental */
export interface AnimationMetadata {
type: AnimationMetadataType;
}
/** @experimental */
export declare const enum AnimationMetadataType {
State = 0,
Transition = 1,
Sequence = 2,
Group = 3,
Animate = 4,
KeyframeSequence = 5,
Style = 6,
}
/** @experimental */
export declare abstract class AnimationPlayer {
parentPlayer: AnimationPlayer;
abstract destroy(): void;
abstract finish(): void;
abstract getPosition(): number;
abstract hasStarted(): boolean;
abstract init(): void;
abstract onDestroy(fn: () => void): void;
abstract onDone(fn: () => void): void;
abstract onStart(fn: () => void): void;
abstract pause(): void;
abstract play(): void;
abstract reset(): void;
abstract restart(): void;
abstract setPosition(p: any): void;
}
/** @experimental */
export interface AnimationSequenceMetadata extends AnimationMetadata {
steps: AnimationMetadata[];
}
/** @experimental */
export interface AnimationStateMetadata extends AnimationMetadata {
name: string;
styles: AnimationStyleMetadata;
}
/** @experimental */
export interface AnimationStyleMetadata extends AnimationMetadata {
offset: number;
styles: {
[key: string]: string | number;
}[];
}
/** @experimental */
export interface AnimationTransitionMetadata extends AnimationMetadata {
animation: AnimationMetadata;
expr: string | ((fromState: string, toState: string) => boolean);
}
/** @experimental */
export interface AnimationTriggerMetadata {
definitions: AnimationMetadata[];
name: string;
}
/** @experimental */
export declare const AUTO_STYLE = "*";
/** @experimental */
export declare function group(steps: AnimationMetadata[]): AnimationGroupMetadata;
/** @experimental */
export declare function keyframes(steps: AnimationStyleMetadata[]): AnimationKeyframesSequenceMetadata;
/** @experimental */
export declare class NoOpAnimationPlayer implements AnimationPlayer {
parentPlayer: AnimationPlayer;
constructor();
destroy(): void;
finish(): void;
getPosition(): number;
hasStarted(): boolean;
init(): void;
onDestroy(fn: () => void): void;
onDone(fn: () => void): void;
onStart(fn: () => void): void;
pause(): void;
play(): void;
reset(): void;
restart(): void;
setPosition(p: number): void;
}
/** @experimental */
export declare function sequence(steps: AnimationMetadata[]): AnimationSequenceMetadata;
/** @experimental */
export declare function state(name: string, styles: AnimationStyleMetadata): AnimationStateMetadata;
/** @experimental */
export declare function style(tokens: {
[key: string]: string | number;
} | Array<{
[key: string]: string | number;
}>): AnimationStyleMetadata;
/** @experimental */
export declare function transition(stateChangeExpr: string | ((fromState: string, toState: string) => boolean), steps: AnimationMetadata | AnimationMetadata[]): AnimationTransitionMetadata;
/** @experimental */
export declare function trigger(name: string, definitions: AnimationMetadata[]): AnimationTriggerMetadata;

View File

@ -811,17 +811,13 @@ export declare class ReflectiveKey {
/** @experimental */
export declare class RenderComponentType {
animations: {
[key: string]: Function;
};
animations: any;
encapsulation: ViewEncapsulation;
id: string;
slotCount: number;
styles: Array<string | any[]>;
templateUrl: string;
constructor(id: string, templateUrl: string, slotCount: number, encapsulation: ViewEncapsulation, styles: Array<string | any[]>, animations: {
[key: string]: Function;
});
constructor(id: string, templateUrl: string, slotCount: number, encapsulation: ViewEncapsulation, styles: Array<string | any[]>, animations: any);
}
/** @experimental */
@ -1031,15 +1027,6 @@ export interface TrackByFunction<T> {
/** @experimental */
export declare function transition(stateChangeExpr: string | ((fromState: string, toState: string) => boolean), steps: AnimationMetadata | AnimationMetadata[]): AnimationStateTransitionMetadata;
/** @experimental */
export interface TransitionFactory {
match(currentState: any, nextState: any): TransitionInstruction;
}
/** @experimental */
export interface TransitionInstruction {
}
/** @experimental */
export declare const TRANSLATIONS: InjectionToken<string>;
@ -1049,12 +1036,6 @@ export declare const TRANSLATIONS_FORMAT: InjectionToken<string>;
/** @experimental */
export declare function trigger(name: string, animation: AnimationMetadata[]): AnimationEntryMetadata;
/** @experimental */
export interface Trigger {
name: string;
transitionFactories: TransitionFactory[];
}
/** @stable */
export declare const Type: FunctionConstructor;

View File

@ -0,0 +1,11 @@
/** @experimental */
export declare abstract class AnimationDriver {
abstract animate(element: any, keyframes: {
[key: string]: string | number;
}[], duration: number, delay: number, easing: string, previousPlayers?: any[]): any;
static NOOP: AnimationDriver;
}
/** @experimental */
export declare class BrowserAnimationModule {
}

View File

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