refactor(animations): support browser animation rendering (#14578)
This commit is contained in:
parent
88755b0dae
commit
830393d234
71
build.sh
71
build.sh
|
@ -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
|
||||
|
|
|
@ -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'));
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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';
|
|
@ -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 {
|
||||
}
|
|
@ -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}`);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
},
|
|
@ -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"
|
||||
},
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -11,4 +11,4 @@
|
|||
* @description
|
||||
* Entry point for all public APIs of the animation package.
|
||||
*/
|
||||
export * from './src/animation';
|
||||
export * from './src/animations';
|
|
@ -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;
|
||||
}
|
83
modules/@angular/animation/src/dsl/animation_metadata.ts → modules/@angular/animations/src/animation_metadata.ts
Normal file → Executable file
83
modules/@angular/animation/src/dsl/animation_metadata.ts → modules/@angular/animations/src/animation_metadata.ts
Normal file → Executable 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};
|
||||
}
|
||||
|
|
@ -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';
|
|
@ -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; }
|
||||
}
|
|
@ -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; }
|
||||
}
|
|
@ -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';
|
|
@ -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);
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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); });
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
../../../animations/src/animation_metadata.ts
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
];
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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 {}
|
|
@ -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}`;
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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',
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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"],
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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';
|
|
@ -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';
|
|
@ -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';
|
|
@ -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 {
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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}`);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
};
|
||||
}
|
|
@ -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) {
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
};
|
|
@ -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 {
|
|
@ -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} = {};
|
||||
}
|
|
@ -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; }
|
||||
|
|
@ -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';
|
|
@ -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;
|
||||
}
|
|
@ -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};
|
||||
}
|
|
@ -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; }
|
|
@ -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];
|
||||
}
|
|
@ -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'};
|
|
@ -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 {
|
|
@ -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
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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';
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 () => {};
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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": ".",
|
||||
|
|
|
@ -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'},
|
||||
|
|
|
@ -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'},
|
||||
|
|
|
@ -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'},
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 {
|
||||
}
|
27
tools/public_api_guard/platform-browser/typings/animations/testing/testing.d.ts
vendored
Normal file
27
tools/public_api_guard/platform-browser/typings/animations/testing/testing.d.ts
vendored
Normal 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;
|
||||
}
|
Loading…
Reference in New Issue