From 830393d234d2b7a41a5f1db375613ffa8dde97c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Wed, 22 Feb 2017 15:14:49 -0800 Subject: [PATCH] refactor(animations): support browser animation rendering (#14578) --- build.sh | 71 +- gulpfile.js | 2 +- modules/@angular/animation/package.json | 18 - modules/@angular/animation/src/animation.ts | 11 - .../animation/src/animation_module.ts | 35 - .../src/dsl/animation_dsl_visitor.ts | 40 - .../animation/src/engine/animation_driver.ts | 31 - .../engine/dom_animation_transition_engine.ts | 235 ------ .../dom_animation_transition_engine_spec.ts | 470 ----------- .../testing/mock_animation_driver.ts | 31 - .../@angular/animation/tsconfig-testing.json | 18 - .../{animation => animations}/.babelrc | 2 +- .../.babelrc-testing | 3 +- .../{animation => animations}/index.ts | 0 modules/@angular/animations/package.json | 18 + .../{animation => animations}/public_api.ts | 2 +- .../animations/src/animation_event.ts | 47 ++ .../src}/animation_metadata.ts | 83 +- modules/@angular/animations/src/animations.ts | 18 + .../src/players/animation_group_player.ts | 109 +++ .../src/players/animation_player.ts | 76 ++ .../@angular/animations/src/private_export.ts | 8 + .../style_data.ts => animations/src/util.ts} | 5 +- .../{animation => animations}/src/version.ts | 0 .../tsconfig-build.json | 7 +- .../@angular/compiler/src/compile_metadata.ts | 4 +- modules/@angular/compiler/src/jit/compiler.ts | 8 +- .../compiler/src/metadata_resolver.ts | 11 +- .../src/template_parser/template_parser.ts | 2 + .../src/view_compiler_next/view_compiler.ts | 19 +- .../animation_metadata_wrapped.ts | 150 ++++ .../@angular/core/src/animation_next/dsl.ts | 1 + modules/@angular/core/src/core.ts | 9 +- .../@angular/core/src/core_private_export.ts | 1 - .../@angular/core/src/linker/view_utils.ts | 2 +- .../core/src/platform_core_providers.ts | 2 - modules/@angular/core/src/render/api.ts | 4 +- .../core/src/transition/transition_engine.ts | 41 - modules/@angular/core/src/triggers.ts | 27 - modules/@angular/core/src/view/services.ts | 2 +- .../animation_integration_next_spec.ts | 566 +++++++++++++ .../examples/_common/system-config.ts | 3 + .../language-service/test/test_utils.ts | 3 +- .../language-service/tsconfig-build.json | 1 + .../platform-browser/.babelrc-animations | 16 + .../.babelrc-animations-testing | 17 + .../platform-browser/animations/index.ts | 14 + .../platform-browser/animations/public_api.ts | 14 + .../animations/src/animations.ts | 16 + .../src/browser_animation_module.ts | 57 ++ .../animations}/src/dsl/animation.ts | 38 +- .../src/dsl/animation_dsl_visitor.ts | 40 + .../src/dsl/animation_timeline_instruction.ts | 11 +- .../src/dsl/animation_timeline_visitor.ts | 76 +- .../src/dsl/animation_transition_expr.ts | 0 .../src/dsl/animation_transition_factory.ts | 11 +- .../dsl/animation_transition_instruction.ts | 15 +- .../animations}/src/dsl/animation_trigger.ts | 80 +- .../src/dsl/animation_validator_visitor.ts | 39 +- .../animation_style_normalizer.ts | 7 + .../web_animations_style_normalizer.ts | 0 .../animations/src/private_export.ts | 11 + .../animations/src/render/animation_driver.ts | 32 + .../animations/src/render/animation_engine.ts | 477 +++++++++++ .../render}/animation_engine_instruction.ts | 6 +- .../src/render/animation_renderer.ts | 134 +++ .../render}/web_animations/dom_animation.ts | 0 .../web_animations/web_animations_driver.ts | 5 +- .../web_animations/web_animations_player.ts | 4 +- .../animations/src}/util.ts | 12 +- .../animations}/test/dsl/animation_spec.ts | 17 +- .../test/dsl/animation_trigger_spec.ts | 37 +- .../web_animations_style_normalizer_spec.ts | 0 .../test/engine/animation_engine_spec.ts | 761 ++++++++++++++++++ .../animations}/testing/index.ts | 6 - .../testing/mock_animation_driver.ts | 80 ++ .../test/animation/animation_renderer_spec.ts | 146 ++++ .../tsconfig-animations-testing.json | 24 + .../platform-browser/tsconfig-animations.json | 25 + .../platform-browser/tsconfig-build.json | 1 + modules/benchmarks/src/bootstrap_ng2.ts | 7 +- modules/playground/src/bootstrap.ts | 6 +- test-main.js | 4 + tools/gulp-tasks/public-api.js | 4 +- .../animations/typings/animations.d.ts | 147 ++++ tools/public_api_guard/core/typings/core.d.ts | 23 +- .../typings/animations/animations.d.ts | 11 + .../typings/animations/testing/testing.d.ts | 27 + 88 files changed, 3429 insertions(+), 1225 deletions(-) delete mode 100644 modules/@angular/animation/package.json delete mode 100644 modules/@angular/animation/src/animation.ts delete mode 100644 modules/@angular/animation/src/animation_module.ts delete mode 100644 modules/@angular/animation/src/dsl/animation_dsl_visitor.ts delete mode 100644 modules/@angular/animation/src/engine/animation_driver.ts delete mode 100644 modules/@angular/animation/src/engine/dom_animation_transition_engine.ts delete mode 100644 modules/@angular/animation/test/animation_engine/dom_animation_transition_engine_spec.ts delete mode 100644 modules/@angular/animation/testing/mock_animation_driver.ts delete mode 100644 modules/@angular/animation/tsconfig-testing.json rename modules/@angular/{animation => animations}/.babelrc (85%) rename modules/@angular/{animation => animations}/.babelrc-testing (72%) rename modules/@angular/{animation => animations}/index.ts (100%) create mode 100644 modules/@angular/animations/package.json rename modules/@angular/{animation => animations}/public_api.ts (89%) create mode 100644 modules/@angular/animations/src/animation_event.ts rename modules/@angular/{animation/src/dsl => animations/src}/animation_metadata.ts (88%) mode change 100644 => 100755 create mode 100644 modules/@angular/animations/src/animations.ts create mode 100644 modules/@angular/animations/src/players/animation_group_player.ts create mode 100644 modules/@angular/animations/src/players/animation_player.ts create mode 100644 modules/@angular/animations/src/private_export.ts rename modules/@angular/{animation/src/common/style_data.ts => animations/src/util.ts} (70%) rename modules/@angular/{animation => animations}/src/version.ts (100%) rename modules/@angular/{animation => animations}/tsconfig-build.json (75%) create mode 100644 modules/@angular/core/src/animation_next/animation_metadata_wrapped.ts create mode 120000 modules/@angular/core/src/animation_next/dsl.ts delete mode 100644 modules/@angular/core/src/transition/transition_engine.ts delete mode 100644 modules/@angular/core/src/triggers.ts create mode 100644 modules/@angular/core/test/animation/animation_integration_next_spec.ts create mode 100644 modules/@angular/platform-browser/.babelrc-animations create mode 100644 modules/@angular/platform-browser/.babelrc-animations-testing create mode 100644 modules/@angular/platform-browser/animations/index.ts create mode 100644 modules/@angular/platform-browser/animations/public_api.ts create mode 100644 modules/@angular/platform-browser/animations/src/animations.ts create mode 100644 modules/@angular/platform-browser/animations/src/browser_animation_module.ts rename modules/@angular/{animation => platform-browser/animations}/src/dsl/animation.ts (57%) create mode 100644 modules/@angular/platform-browser/animations/src/dsl/animation_dsl_visitor.ts rename modules/@angular/{animation => platform-browser/animations}/src/dsl/animation_timeline_instruction.ts (70%) rename modules/@angular/{animation => platform-browser/animations}/src/dsl/animation_timeline_visitor.ts (86%) rename modules/@angular/{animation => platform-browser/animations}/src/dsl/animation_transition_expr.ts (100%) rename modules/@angular/{animation => platform-browser/animations}/src/dsl/animation_transition_factory.ts (77%) rename modules/@angular/{animation => platform-browser/animations}/src/dsl/animation_transition_instruction.ts (69%) rename modules/@angular/{animation => platform-browser/animations}/src/dsl/animation_trigger.ts (54%) rename modules/@angular/{animation => platform-browser/animations}/src/dsl/animation_validator_visitor.ts (81%) rename modules/@angular/{animation => platform-browser/animations}/src/dsl/style_normalization/animation_style_normalizer.ts (86%) rename modules/@angular/{animation => platform-browser/animations}/src/dsl/style_normalization/web_animations_style_normalizer.ts (100%) create mode 100644 modules/@angular/platform-browser/animations/src/private_export.ts create mode 100644 modules/@angular/platform-browser/animations/src/render/animation_driver.ts create mode 100644 modules/@angular/platform-browser/animations/src/render/animation_engine.ts rename modules/@angular/{animation/src/engine => platform-browser/animations/src/render}/animation_engine_instruction.ts (62%) create mode 100644 modules/@angular/platform-browser/animations/src/render/animation_renderer.ts rename modules/@angular/{animation/src/engine => platform-browser/animations/src/render}/web_animations/dom_animation.ts (100%) rename modules/@angular/{animation/src/engine => platform-browser/animations/src/render}/web_animations/web_animations_driver.ts (86%) rename modules/@angular/{animation/src/engine => platform-browser/animations/src/render}/web_animations/web_animations_player.ts (98%) rename modules/@angular/{animation/src/common => platform-browser/animations/src}/util.ts (80%) rename modules/@angular/{animation => platform-browser/animations}/test/dsl/animation_spec.ts (97%) rename modules/@angular/{animation => platform-browser/animations}/test/dsl/animation_trigger_spec.ts (78%) rename modules/@angular/{animation => platform-browser/animations}/test/dsl/style_normalizer/web_animations_style_normalizer_spec.ts (100%) create mode 100644 modules/@angular/platform-browser/animations/test/engine/animation_engine_spec.ts rename modules/@angular/{animation => platform-browser/animations}/testing/index.ts (73%) create mode 100644 modules/@angular/platform-browser/animations/testing/mock_animation_driver.ts create mode 100644 modules/@angular/platform-browser/test/animation/animation_renderer_spec.ts create mode 100644 modules/@angular/platform-browser/tsconfig-animations-testing.json create mode 100644 modules/@angular/platform-browser/tsconfig-animations.json create mode 100644 tools/public_api_guard/animations/typings/animations.d.ts create mode 100644 tools/public_api_guard/platform-browser/typings/animations/animations.d.ts create mode 100644 tools/public_api_guard/platform-browser/typings/animations/testing/testing.d.ts diff --git a/build.sh b/build.sh index 99501d448dbb1..25c47d28fbcf5 100755 --- a/build.sh +++ b/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 diff --git a/gulpfile.js b/gulpfile.js index b25ec972399ab..31e6b56e36759 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -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')); \ No newline at end of file +gulp.task('changelog', loadTask('changelog')); diff --git a/modules/@angular/animation/package.json b/modules/@angular/animation/package.json deleted file mode 100644 index d0fec906d9a7a..0000000000000 --- a/modules/@angular/animation/package.json +++ /dev/null @@ -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" - } -} diff --git a/modules/@angular/animation/src/animation.ts b/modules/@angular/animation/src/animation.ts deleted file mode 100644 index 3de54172d27b7..0000000000000 --- a/modules/@angular/animation/src/animation.ts +++ /dev/null @@ -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'; diff --git a/modules/@angular/animation/src/animation_module.ts b/modules/@angular/animation/src/animation_module.ts deleted file mode 100644 index 245584a5b0b3e..0000000000000 --- a/modules/@angular/animation/src/animation_module.ts +++ /dev/null @@ -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 { -} diff --git a/modules/@angular/animation/src/dsl/animation_dsl_visitor.ts b/modules/@angular/animation/src/dsl/animation_dsl_visitor.ts deleted file mode 100644 index d6c573c8bf28b..0000000000000 --- a/modules/@angular/animation/src/dsl/animation_dsl_visitor.ts +++ /dev/null @@ -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(node, context); - case meta.AnimationMetadataType.Transition: - return visitor.visitTransition(node, context); - case meta.AnimationMetadataType.Sequence: - return visitor.visitSequence(node, context); - case meta.AnimationMetadataType.Group: - return visitor.visitGroup(node, context); - case meta.AnimationMetadataType.Animate: - return visitor.visitAnimate(node, context); - case meta.AnimationMetadataType.KeyframeSequence: - return visitor.visitKeyframeSequence(node, context); - case meta.AnimationMetadataType.Style: - return visitor.visitStyle(node, context); - default: - throw new Error(`Unable to resolve animation metadata node #${node.type}`); - } -} diff --git a/modules/@angular/animation/src/engine/animation_driver.ts b/modules/@angular/animation/src/engine/animation_driver.ts deleted file mode 100644 index c22cf3f8c7d37..0000000000000 --- a/modules/@angular/animation/src/engine/animation_driver.ts +++ /dev/null @@ -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; -} diff --git a/modules/@angular/animation/src/engine/dom_animation_transition_engine.ts b/modules/@angular/animation/src/engine/dom_animation_transition_engine.ts deleted file mode 100644 index 4062caff620c2..0000000000000 --- a/modules/@angular/animation/src/engine/dom_animation_transition_engine.ts +++ /dev/null @@ -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(); - private _queuedRemovals: any[] = []; - private _queuedAnimations: AnimationPlayerTuple[] = []; - private _activeElementAnimations = new Map(); - private _activeTransitionAnimations = new Map(); - - 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, instruction); - } - if (instruction.type == AnimationTransitionInstructionType.TimelineAnimation) { - return this._handleTimelineAnimation( - element, 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 = {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, key: any, defaultValue: any) { - let value = map.get(key); - if (!value) { - map.set(key, value = defaultValue); - } - return value; -} - -function deleteFromArrayMap(map: Map, 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); -} diff --git a/modules/@angular/animation/test/animation_engine/dom_animation_transition_engine_spec.ts b/modules/@angular/animation/test/animation_engine/dom_animation_transition_engine_spec.ts deleted file mode 100644 index 785ae6e663936..0000000000000 --- a/modules/@angular/animation/test/animation_engine/dom_animation_transition_engine_spec.ts +++ /dev/null @@ -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('
'); - }); - - 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 = {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 = engine.process(element, [instruction2]); - expect(player2.previousPlayers).toEqual([player1]); - player2.finish(); - - const player3 = 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 = 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 = 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('
'); - let child1 = el('
'); - let child2 = el('
'); - - 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('
'); - let targetContainer = el('
'); - let otherContainer = el('
'); - let child1 = el('
'); - let child2 = el('
'); - 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; - } -} diff --git a/modules/@angular/animation/testing/mock_animation_driver.ts b/modules/@angular/animation/testing/mock_animation_driver.ts deleted file mode 100644 index bc1418875ce03..0000000000000 --- a/modules/@angular/animation/testing/mock_animation_driver.ts +++ /dev/null @@ -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(player); - return 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(); - } -} diff --git a/modules/@angular/animation/tsconfig-testing.json b/modules/@angular/animation/tsconfig-testing.json deleted file mode 100644 index 00e85e5b559ab..0000000000000 --- a/modules/@angular/animation/tsconfig-testing.json +++ /dev/null @@ -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 - } -} diff --git a/modules/@angular/animation/.babelrc b/modules/@angular/animations/.babelrc similarity index 85% rename from modules/@angular/animation/.babelrc rename to modules/@angular/animations/.babelrc index fdaee566fe461..e7447c278837b 100644 --- a/modules/@angular/animation/.babelrc +++ b/modules/@angular/animations/.babelrc @@ -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" }, diff --git a/modules/@angular/animation/.babelrc-testing b/modules/@angular/animations/.babelrc-testing similarity index 72% rename from modules/@angular/animation/.babelrc-testing rename to modules/@angular/animations/.babelrc-testing index 12997b861c8fa..f7a9afe4ce319 100644 --- a/modules/@angular/animation/.babelrc-testing +++ b/modules/@angular/animations/.babelrc-testing @@ -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" }, diff --git a/modules/@angular/animation/index.ts b/modules/@angular/animations/index.ts similarity index 100% rename from modules/@angular/animation/index.ts rename to modules/@angular/animations/index.ts diff --git a/modules/@angular/animations/package.json b/modules/@angular/animations/package.json new file mode 100644 index 0000000000000..c5eee8ab580db --- /dev/null +++ b/modules/@angular/animations/package.json @@ -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" + } +} diff --git a/modules/@angular/animation/public_api.ts b/modules/@angular/animations/public_api.ts similarity index 89% rename from modules/@angular/animation/public_api.ts rename to modules/@angular/animations/public_api.ts index fc890095d3039..698d901fbde9b 100644 --- a/modules/@angular/animation/public_api.ts +++ b/modules/@angular/animations/public_api.ts @@ -11,4 +11,4 @@ * @description * Entry point for all public APIs of the animation package. */ -export * from './src/animation'; +export * from './src/animations'; diff --git a/modules/@angular/animations/src/animation_event.ts b/modules/@angular/animations/src/animation_event.ts new file mode 100644 index 0000000000000..cfe5ab46e9593 --- /dev/null +++ b/modules/@angular/animations/src/animation_event.ts @@ -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; +} diff --git a/modules/@angular/animation/src/dsl/animation_metadata.ts b/modules/@angular/animations/src/animation_metadata.ts old mode 100644 new mode 100755 similarity index 88% rename from modules/@angular/animation/src/dsl/animation_metadata.ts rename to modules/@angular/animations/src/animation_metadata.ts index 38a6da05afd69..85e2a11ef6fb7 --- a/modules/@angular/animation/src/dsl/animation_metadata.ts +++ b/modules/@angular/animations/src/animation_metadata.ts @@ -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 + * + *
...
+ 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 = tokens; + input = <ɵStyleData[]>tokens; } else { - input = [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(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}; } diff --git a/modules/@angular/animations/src/animations.ts b/modules/@angular/animations/src/animations.ts new file mode 100644 index 0000000000000..2ff5636d62d47 --- /dev/null +++ b/modules/@angular/animations/src/animations.ts @@ -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'; diff --git a/modules/@angular/animations/src/players/animation_group_player.ts b/modules/@angular/animations/src/players/animation_group_player.ts new file mode 100644 index 0000000000000..0ccd459c18c0c --- /dev/null +++ b/modules/@angular/animations/src/players/animation_group_player.ts @@ -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; } +} diff --git a/modules/@angular/animations/src/players/animation_player.ts b/modules/@angular/animations/src/players/animation_player.ts new file mode 100644 index 0000000000000..c7ae2d0d93412 --- /dev/null +++ b/modules/@angular/animations/src/players/animation_player.ts @@ -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; } +} diff --git a/modules/@angular/animations/src/private_export.ts b/modules/@angular/animations/src/private_export.ts new file mode 100644 index 0000000000000..635a56c87d0cc --- /dev/null +++ b/modules/@angular/animations/src/private_export.ts @@ -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'; diff --git a/modules/@angular/animation/src/common/style_data.ts b/modules/@angular/animations/src/util.ts similarity index 70% rename from modules/@angular/animation/src/common/style_data.ts rename to modules/@angular/animations/src/util.ts index 2b67654b82491..97f38a28fad35 100644 --- a/modules/@angular/animation/src/common/style_data.ts +++ b/modules/@angular/animations/src/util.ts @@ -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); +} diff --git a/modules/@angular/animation/src/version.ts b/modules/@angular/animations/src/version.ts similarity index 100% rename from modules/@angular/animation/src/version.ts rename to modules/@angular/animations/src/version.ts diff --git a/modules/@angular/animation/tsconfig-build.json b/modules/@angular/animations/tsconfig-build.json similarity index 75% rename from modules/@angular/animation/tsconfig-build.json rename to modules/@angular/animations/tsconfig-build.json index a1f2b0610abb3..7bca914a078f0 100644 --- a/modules/@angular/animation/tsconfig-build.json +++ b/modules/@angular/animations/tsconfig-build.json @@ -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" } } diff --git a/modules/@angular/compiler/src/compile_metadata.ts b/modules/@angular/compiler/src/compile_metadata.ts index 59a89eb016daa..d5cdc5a59ba4d 100644 --- a/modules/@angular/compiler/src/compile_metadata.ts +++ b/modules/@angular/compiler/src/compile_metadata.ts @@ -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; diff --git a/modules/@angular/compiler/src/jit/compiler.ts b/modules/@angular/compiler/src/jit/compiler.ts index d9d702263d238..8193c435f89b4 100644 --- a/modules/@angular/compiler/src/jit/compiler.ts +++ b/modules/@angular/compiler/src/jit/compiler.ts @@ -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), diff --git a/modules/@angular/compiler/src/metadata_resolver.ts b/modules/@angular/compiler/src/metadata_resolver.ts index f86fc9b26cc07..1748256d319a0 100644 --- a/modules/@angular/compiler/src/metadata_resolver.ts +++ b/modules/@angular/compiler/src/metadata_resolver.ts @@ -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, diff --git a/modules/@angular/compiler/src/template_parser/template_parser.ts b/modules/@angular/compiler/src/template_parser/template_parser.ts index 986f3900ce4e3..5810496e49e61 100644 --- a/modules/@angular/compiler/src/template_parser/template_parser.ts +++ b/modules/@angular/compiler/src/template_parser/template_parser.ts @@ -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(); template.animations.forEach(entry => { triggerLookup.add(entry); }); diff --git a/modules/@angular/compiler/src/view_compiler_next/view_compiler.ts b/modules/@angular/compiler/src/view_compiler_next/view_compiler.ts index 3273f055af7cd..ca66573669785 100644 --- a/modules/@angular/compiler/src/view_compiler_next/view_compiler.ts +++ b/modules/@angular/compiler/src/view_compiler_next/view_compiler.ts @@ -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(); 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; +} diff --git a/modules/@angular/core/src/animation_next/animation_metadata_wrapped.ts b/modules/@angular/core/src/animation_next/animation_metadata_wrapped.ts new file mode 100644 index 0000000000000..9c0823f05d847 --- /dev/null +++ b/modules/@angular/core/src/animation_next/animation_metadata_wrapped.ts @@ -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; +} diff --git a/modules/@angular/core/src/animation_next/dsl.ts b/modules/@angular/core/src/animation_next/dsl.ts new file mode 120000 index 0000000000000..a39f319031fcd --- /dev/null +++ b/modules/@angular/core/src/animation_next/dsl.ts @@ -0,0 +1 @@ +../../../animations/src/animation_metadata.ts \ No newline at end of file diff --git a/modules/@angular/core/src/core.ts b/modules/@angular/core/src/core.ts index fa61a53f36d22..7e8dafd7f5c2d 100644 --- a/modules/@angular/core/src/core.ts +++ b/modules/@angular/core/src/core.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'; diff --git a/modules/@angular/core/src/core_private_export.ts b/modules/@angular/core/src/core_private_export.ts index c98b02e4e881e..955f98aa453b3 100644 --- a/modules/@angular/core/src/core_private_export.ts +++ b/modules/@angular/core/src/core_private_export.ts @@ -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'; diff --git a/modules/@angular/core/src/linker/view_utils.ts b/modules/@angular/core/src/linker/view_utils.ts index 1590e08b3dcce..b623f9f1aa445 100644 --- a/modules/@angular/core/src/linker/view_utils.ts +++ b/modules/@angular/core/src/linker/view_utils.ts @@ -40,7 +40,7 @@ let nextRenderComponentTypeId = 0; export function createRenderComponentType( templateUrl: string, slotCount: number, encapsulation: ViewEncapsulation, - styles: Array, animations: {[key: string]: Function}): RenderComponentType { + styles: Array, animations: any): RenderComponentType { return new RenderComponentType( `${nextRenderComponentTypeId++}`, templateUrl, slotCount, encapsulation, styles, animations); } diff --git a/modules/@angular/core/src/platform_core_providers.ts b/modules/@angular/core/src/platform_core_providers.ts index 191f3fbaddf52..e5a71a01543db 100644 --- a/modules/@angular/core/src/platform_core_providers.ts +++ b/modules/@angular/core/src/platform_core_providers.ts @@ -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, ]; diff --git a/modules/@angular/core/src/render/api.ts b/modules/@angular/core/src/render/api.ts index d477ee2eefcd5..32028f46a8d11 100644 --- a/modules/@angular/core/src/render/api.ts +++ b/modules/@angular/core/src/render/api.ts @@ -20,7 +20,7 @@ export class RenderComponentType { constructor( public id: string, public templateUrl: string, public slotCount: number, public encapsulation: ViewEncapsulation, public styles: Array, - 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('RendererV2Interceptor'); + /** * Injectable service that provides a low-level interface for modifying the UI. * diff --git a/modules/@angular/core/src/transition/transition_engine.ts b/modules/@angular/core/src/transition/transition_engine.ts deleted file mode 100644 index b32dd8f6d3ca2..0000000000000 --- a/modules/@angular/core/src/transition/transition_engine.ts +++ /dev/null @@ -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); -} diff --git a/modules/@angular/core/src/triggers.ts b/modules/@angular/core/src/triggers.ts deleted file mode 100644 index 343e01524e40f..0000000000000 --- a/modules/@angular/core/src/triggers.ts +++ /dev/null @@ -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 {} diff --git a/modules/@angular/core/src/view/services.ts b/modules/@angular/core/src/view/services.ts index da838c966fb2a..c0d5f997838fd 100644 --- a/modules/@angular/core/src/view/services.ts +++ b/modules/@angular/core/src/view/services.ts @@ -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}`; } diff --git a/modules/@angular/core/test/animation/animation_integration_next_spec.ts b/modules/@angular/core/test/animation/animation_integration_next_spec.ts new file mode 100644 index 0000000000000..d01a8f7029262 --- /dev/null +++ b/modules/@angular/core/test/animation/animation_integration_next_spec.ts @@ -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: ` +
+ `, + 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: ` +
+ `, + 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: ` +
+ `, + 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: ` +
+ `, + 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: ` +
+ `, + 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: ` +
+
+
+ `, + 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: ` +
+ `, + 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: ` +
+ `, + 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: ` +
+
+ `, + 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: ` +
+ `, + 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(); + } +} diff --git a/modules/@angular/examples/_common/system-config.ts b/modules/@angular/examples/_common/system-config.ts index eb05bd648ee64..7bfa87e5b1ef1 100644 --- a/modules/@angular/examples/_common/system-config.ts +++ b/modules/@angular/examples/_common/system-config.ts @@ -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', diff --git a/modules/@angular/language-service/test/test_utils.ts b/modules/@angular/language-service/test/test_utils.ts index 579e2906da69e..4c89813dc93c3 100644 --- a/modules/@angular/language-service/test/test_utils.ts +++ b/modules/@angular/language-service/test/test_utils.ts @@ -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); diff --git a/modules/@angular/language-service/tsconfig-build.json b/modules/@angular/language-service/tsconfig-build.json index 8a9a54a396ac7..25a3bdc89e9fe 100644 --- a/modules/@angular/language-service/tsconfig-build.json +++ b/modules/@angular/language-service/tsconfig-build.json @@ -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"], diff --git a/modules/@angular/platform-browser/.babelrc-animations b/modules/@angular/platform-browser/.babelrc-animations new file mode 100644 index 0000000000000..d7e2d7791a7b4 --- /dev/null +++ b/modules/@angular/platform-browser/.babelrc-animations @@ -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" +} diff --git a/modules/@angular/platform-browser/.babelrc-animations-testing b/modules/@angular/platform-browser/.babelrc-animations-testing new file mode 100644 index 0000000000000..c7eb429c51af5 --- /dev/null +++ b/modules/@angular/platform-browser/.babelrc-animations-testing @@ -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" +} diff --git a/modules/@angular/platform-browser/animations/index.ts b/modules/@angular/platform-browser/animations/index.ts new file mode 100644 index 0000000000000..1904b8e43c0b7 --- /dev/null +++ b/modules/@angular/platform-browser/animations/index.ts @@ -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'; diff --git a/modules/@angular/platform-browser/animations/public_api.ts b/modules/@angular/platform-browser/animations/public_api.ts new file mode 100644 index 0000000000000..698d901fbde9b --- /dev/null +++ b/modules/@angular/platform-browser/animations/public_api.ts @@ -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'; diff --git a/modules/@angular/platform-browser/animations/src/animations.ts b/modules/@angular/platform-browser/animations/src/animations.ts new file mode 100644 index 0000000000000..cc38dab476884 --- /dev/null +++ b/modules/@angular/platform-browser/animations/src/animations.ts @@ -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'; diff --git a/modules/@angular/platform-browser/animations/src/browser_animation_module.ts b/modules/@angular/platform-browser/animations/src/browser_animation_module.ts new file mode 100644 index 0000000000000..4c36d36fc678e --- /dev/null +++ b/modules/@angular/platform-browser/animations/src/browser_animation_module.ts @@ -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 { +} diff --git a/modules/@angular/animation/src/dsl/animation.ts b/modules/@angular/platform-browser/animations/src/dsl/animation.ts similarity index 57% rename from modules/@angular/animation/src/dsl/animation.ts rename to modules/@angular/platform-browser/animations/src/dsl/animation.ts index 88cc8fb009039..d236bff966a23 100644 --- a/modules/@angular/animation/src/dsl/animation.ts +++ b/modules/@angular/platform-browser/animations/src/dsl/animation.ts @@ -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(startingStyles)) : - startingStyles; - const dest = Array.isArray(destinationStyles) ? - normalizeStyles(new AnimationStyles(destinationStyles)) : - 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); } } diff --git a/modules/@angular/platform-browser/animations/src/dsl/animation_dsl_visitor.ts b/modules/@angular/platform-browser/animations/src/dsl/animation_dsl_visitor.ts new file mode 100644 index 0000000000000..92b8e6f0e571b --- /dev/null +++ b/modules/@angular/platform-browser/animations/src/dsl/animation_dsl_visitor.ts @@ -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(node, context); + case AnimationMetadataType.Transition: + return visitor.visitTransition(node, context); + case AnimationMetadataType.Sequence: + return visitor.visitSequence(node, context); + case AnimationMetadataType.Group: + return visitor.visitGroup(node, context); + case AnimationMetadataType.Animate: + return visitor.visitAnimate(node, context); + case AnimationMetadataType.KeyframeSequence: + return visitor.visitKeyframeSequence(node, context); + case AnimationMetadataType.Style: + return visitor.visitStyle(node, context); + default: + throw new Error(`Unable to resolve animation metadata node #${node.type}`); + } +} diff --git a/modules/@angular/animation/src/dsl/animation_timeline_instruction.ts b/modules/@angular/platform-browser/animations/src/dsl/animation_timeline_instruction.ts similarity index 70% rename from modules/@angular/animation/src/dsl/animation_timeline_instruction.ts rename to modules/@angular/platform-browser/animations/src/dsl/animation_timeline_instruction.ts index c9f66f30cedde..408f532e527d3 100644 --- a/modules/@angular/animation/src/dsl/animation_timeline_instruction.ts +++ b/modules/@angular/platform-browser/animations/src/dsl/animation_timeline_instruction.ts @@ -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 }; } diff --git a/modules/@angular/animation/src/dsl/animation_timeline_visitor.ts b/modules/@angular/platform-browser/animations/src/dsl/animation_timeline_visitor.ts similarity index 86% rename from modules/@angular/animation/src/dsl/animation_timeline_visitor.ts rename to modules/@angular/platform-browser/animations/src/dsl/animation_timeline_visitor.ts index 187e8132d65e5..084df085b31a0 100644 --- a/modules/@angular/animation/src/dsl/animation_timeline_visitor.ts +++ b/modules/@angular/platform-browser/animations/src/dsl/animation_timeline_visitor.ts @@ -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(ast) : - ast; + ast: AnimationMetadata | AnimationMetadata[], startingStyles: ɵStyleData = {}, + finalStyles: ɵStyleData = {}): AnimationTimelineInstruction[] { + const normalizedAst = + Array.isArray(ast) ? sequence(ast) : 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 = {}; + currentAnimateTimings: AnimateTimings; + previousNode: 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') ? - ast.timings : + ast.timings : parseTimeExpression(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(ast.styles, context); + if (astType == AnimationMetadataType.KeyframeSequence) { + this.visitKeyframeSequence(ast.styles, context); } else { context.incrementTime(timings.duration); - if (astType == meta.AnimationMetadataType.Style) { - this.visitStyle(ast.styles, context); + if (astType == AnimationMetadataType.Style) { + this.visitStyle(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 ? 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(); + private _currentKeyframe: ɵStyleData; + private _keyframes = new Map(); 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) { diff --git a/modules/@angular/animation/src/dsl/animation_transition_expr.ts b/modules/@angular/platform-browser/animations/src/dsl/animation_transition_expr.ts similarity index 100% rename from modules/@angular/animation/src/dsl/animation_transition_expr.ts rename to modules/@angular/platform-browser/animations/src/dsl/animation_transition_expr.ts diff --git a/modules/@angular/animation/src/dsl/animation_transition_factory.ts b/modules/@angular/platform-browser/animations/src/dsl/animation_transition_factory.ts similarity index 77% rename from modules/@angular/animation/src/dsl/animation_transition_factory.ts rename to modules/@angular/platform-browser/animations/src/dsl/animation_transition_factory.ts index 5f0333950d3c1..946d97d2ebdd2 100644 --- a/modules/@angular/animation/src/dsl/animation_transition_factory.ts +++ b/modules/@angular/platform-browser/animations/src/dsl/animation_transition_factory.ts @@ -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); } } diff --git a/modules/@angular/animation/src/dsl/animation_transition_instruction.ts b/modules/@angular/platform-browser/animations/src/dsl/animation_transition_instruction.ts similarity index 69% rename from modules/@angular/animation/src/dsl/animation_transition_instruction.ts rename to modules/@angular/platform-browser/animations/src/dsl/animation_transition_instruction.ts index eaf2bd43ead4e..d2cc70fcaf9ea 100644 --- a/modules/@angular/animation/src/dsl/animation_transition_instruction.ts +++ b/modules/@angular/platform-browser/animations/src/dsl/animation_transition_instruction.ts @@ -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 }; diff --git a/modules/@angular/animation/src/dsl/animation_trigger.ts b/modules/@angular/platform-browser/animations/src/dsl/animation_trigger.ts similarity index 54% rename from modules/@angular/animation/src/dsl/animation_trigger.ts rename to modules/@angular/platform-browser/animations/src/dsl/animation_trigger.ts index 85eb8ac53ca15..384c556783be7 100644 --- a/modules/@angular/animation/src/dsl/animation_trigger.ts +++ b/modules/@angular/platform-browser/animations/src/dsl/animation_trigger.ts @@ -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 - * - *
...
- * ``` - * - * {@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 { diff --git a/modules/@angular/animation/src/dsl/animation_validator_visitor.ts b/modules/@angular/platform-browser/animations/src/dsl/animation_validator_visitor.ts similarity index 81% rename from modules/@angular/animation/src/dsl/animation_validator_visitor.ts rename to modules/@angular/platform-browser/animations/src/dsl/animation_validator_visitor.ts index 27e3dc34de59b..e9a0d52c3f367 100644 --- a/modules/@angular/animation/src/dsl/animation_validator_visitor.ts +++ b/modules/@angular/platform-browser/animations/src/dsl/animation_validator_visitor.ts @@ -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(ast.timings, context.errors); const astType = ast.styles && ast.styles.type; - if (astType == meta.AnimationMetadataType.KeyframeSequence) { - this.visitKeyframeSequence(ast.styles, context); + if (astType == AnimationMetadataType.KeyframeSequence) { + this.visitKeyframeSequence(ast.styles, context); } else { context.currentTime += context.currentAnimateTimings.duration + context.currentAnimateTimings.delay; - if (astType == meta.AnimationMetadataType.Style) { - this.visitStyle(ast.styles, context); + if (astType == AnimationMetadataType.Style) { + this.visitStyle(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} = {}; } diff --git a/modules/@angular/animation/src/dsl/style_normalization/animation_style_normalizer.ts b/modules/@angular/platform-browser/animations/src/dsl/style_normalization/animation_style_normalizer.ts similarity index 86% rename from modules/@angular/animation/src/dsl/style_normalization/animation_style_normalizer.ts rename to modules/@angular/platform-browser/animations/src/dsl/style_normalization/animation_style_normalizer.ts index 1859e4c34419f..26022b4875872 100644 --- a/modules/@angular/animation/src/dsl/style_normalization/animation_style_normalizer.ts +++ b/modules/@angular/platform-browser/animations/src/dsl/style_normalization/animation_style_normalizer.ts @@ -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; } diff --git a/modules/@angular/animation/src/dsl/style_normalization/web_animations_style_normalizer.ts b/modules/@angular/platform-browser/animations/src/dsl/style_normalization/web_animations_style_normalizer.ts similarity index 100% rename from modules/@angular/animation/src/dsl/style_normalization/web_animations_style_normalizer.ts rename to modules/@angular/platform-browser/animations/src/dsl/style_normalization/web_animations_style_normalizer.ts diff --git a/modules/@angular/platform-browser/animations/src/private_export.ts b/modules/@angular/platform-browser/animations/src/private_export.ts new file mode 100644 index 0000000000000..0366f0906b724 --- /dev/null +++ b/modules/@angular/platform-browser/animations/src/private_export.ts @@ -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'; diff --git a/modules/@angular/platform-browser/animations/src/render/animation_driver.ts b/modules/@angular/platform-browser/animations/src/render/animation_driver.ts new file mode 100644 index 0000000000000..cbe6d1aeb3722 --- /dev/null +++ b/modules/@angular/platform-browser/animations/src/render/animation_driver.ts @@ -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; +} diff --git a/modules/@angular/platform-browser/animations/src/render/animation_engine.ts b/modules/@angular/platform-browser/animations/src/render/animation_engine.ts new file mode 100644 index 0000000000000..cc148105b18dc --- /dev/null +++ b/modules/@angular/platform-browser/animations/src/render/animation_engine.ts @@ -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(); + private _queuedRemovals = new Map any>(); + private _queuedTransitionAnimations: QueuedAnimationTransitionTuple[] = []; + private _activeTransitionAnimations = new Map(); + private _activeElementAnimations = new Map(); + + private _elementTriggerStates = new Map(); + + private _triggers: {[triggerName: string]: AnimationTrigger} = {}; + private _triggerListeners = new Map(); + + 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 = {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 = {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, key: any, defaultValue: any) { + let value = map.get(key); + if (!value) { + map.set(key, value = defaultValue); + } + return value; +} + +function deleteFromArrayMap(map: Map, 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 {element, triggerName, fromState, toState, phaseName, totalTime}; +} diff --git a/modules/@angular/animation/src/engine/animation_engine_instruction.ts b/modules/@angular/platform-browser/animations/src/render/animation_engine_instruction.ts similarity index 62% rename from modules/@angular/animation/src/engine/animation_engine_instruction.ts rename to modules/@angular/platform-browser/animations/src/render/animation_engine_instruction.ts index eb28ee3245766..43c9fcd773ef2 100644 --- a/modules/@angular/animation/src/engine/animation_engine_instruction.ts +++ b/modules/@angular/platform-browser/animations/src/render/animation_engine_instruction.ts @@ -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; } diff --git a/modules/@angular/platform-browser/animations/src/render/animation_renderer.ts b/modules/@angular/platform-browser/animations/src/render/animation_renderer.ts new file mode 100644 index 0000000000000..e05f7afee57fb --- /dev/null +++ b/modules/@angular/platform-browser/animations/src/render/animation_renderer.ts @@ -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]; +} diff --git a/modules/@angular/animation/src/engine/web_animations/dom_animation.ts b/modules/@angular/platform-browser/animations/src/render/web_animations/dom_animation.ts similarity index 100% rename from modules/@angular/animation/src/engine/web_animations/dom_animation.ts rename to modules/@angular/platform-browser/animations/src/render/web_animations/dom_animation.ts diff --git a/modules/@angular/animation/src/engine/web_animations/web_animations_driver.ts b/modules/@angular/platform-browser/animations/src/render/web_animations/web_animations_driver.ts similarity index 86% rename from modules/@angular/animation/src/engine/web_animations/web_animations_driver.ts rename to modules/@angular/platform-browser/animations/src/render/web_animations/web_animations_driver.ts index 5f06ab49d944b..c08401110f26c 100644 --- a/modules/@angular/animation/src/engine/web_animations/web_animations_driver.ts +++ b/modules/@angular/platform-browser/animations/src/render/web_animations/web_animations_driver.ts @@ -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'}; diff --git a/modules/@angular/animation/src/engine/web_animations/web_animations_player.ts b/modules/@angular/platform-browser/animations/src/render/web_animations/web_animations_player.ts similarity index 98% rename from modules/@angular/animation/src/engine/web_animations/web_animations_player.ts rename to modules/@angular/platform-browser/animations/src/render/web_animations/web_animations_player.ts index 51f3b796d4634..a20febf3ccdba 100644 --- a/modules/@angular/animation/src/engine/web_animations/web_animations_player.ts +++ b/modules/@angular/platform-browser/animations/src/render/web_animations/web_animations_player.ts @@ -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 { diff --git a/modules/@angular/animation/src/common/util.ts b/modules/@angular/platform-browser/animations/src/util.ts similarity index 80% rename from modules/@angular/animation/src/common/util.ts rename to modules/@angular/platform-browser/animations/src/util.ts index e6d2c75bad956..38e3562a3f496 100644 --- a/modules/@angular/animation/src/common/util.ts +++ b/modules/@angular/platform-browser/animations/src/util.ts @@ -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 diff --git a/modules/@angular/animation/test/dsl/animation_spec.ts b/modules/@angular/platform-browser/animations/test/dsl/animation_spec.ts similarity index 97% rename from modules/@angular/animation/test/dsl/animation_spec.ts rename to modules/@angular/platform-browser/animations/test/dsl/animation_spec.ts index 1f0f009d95fdd..c45456f37f65c 100644 --- a/modules/@angular/animation/test/dsl/animation_spec.ts +++ b/modules/@angular/platform-browser/animations/test/dsl/animation_spec.ts @@ -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(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); } diff --git a/modules/@angular/animation/test/dsl/animation_trigger_spec.ts b/modules/@angular/platform-browser/animations/test/dsl/animation_trigger_spec.ts similarity index 78% rename from modules/@angular/animation/test/dsl/animation_trigger_spec.ts rename to modules/@angular/platform-browser/animations/test/dsl/animation_trigger_spec.ts index bfc123f0dcbb9..0c316dc85a8f0 100644 --- a/modules/@angular/animation/test/dsl/animation_trigger_spec.ts +++ b/modules/@angular/platform-browser/animations/test/dsl/animation_trigger_spec.ts @@ -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); diff --git a/modules/@angular/animation/test/dsl/style_normalizer/web_animations_style_normalizer_spec.ts b/modules/@angular/platform-browser/animations/test/dsl/style_normalizer/web_animations_style_normalizer_spec.ts similarity index 100% rename from modules/@angular/animation/test/dsl/style_normalizer/web_animations_style_normalizer_spec.ts rename to modules/@angular/platform-browser/animations/test/dsl/style_normalizer/web_animations_style_normalizer_spec.ts diff --git a/modules/@angular/platform-browser/animations/test/engine/animation_engine_spec.ts b/modules/@angular/platform-browser/animations/test/engine/animation_engine_spec.ts new file mode 100644 index 0000000000000..4334be82a57eb --- /dev/null +++ b/modules/@angular/platform-browser/animations/test/engine/animation_engine_spec.ts @@ -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('
'); + }); + + 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 = engine.animateTransition(element, instruction2); + expect(player2.previousPlayers).toEqual([player1]); + player2.finish(); + + const player3 = 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 = 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 = 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 = el('
'); + let child1 = el('
'); + let child2 = el('
'); + + 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 = el('
'); + let targetContainer = el('
'); + let otherContainer = el('
'); + let child1 = el('
'); + let child2 = el('
'); + 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; + } +} diff --git a/modules/@angular/animation/testing/index.ts b/modules/@angular/platform-browser/animations/testing/index.ts similarity index 73% rename from modules/@angular/animation/testing/index.ts rename to modules/@angular/platform-browser/animations/testing/index.ts index 511a350c0151a..8d396ea92e971 100644 --- a/modules/@angular/animation/testing/index.ts +++ b/modules/@angular/platform-browser/animations/testing/index.ts @@ -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'; diff --git a/modules/@angular/platform-browser/animations/testing/mock_animation_driver.ts b/modules/@angular/platform-browser/animations/testing/mock_animation_driver.ts new file mode 100644 index 0000000000000..4b6b792306cad --- /dev/null +++ b/modules/@angular/platform-browser/animations/testing/mock_animation_driver.ts @@ -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(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; + } +} diff --git a/modules/@angular/platform-browser/test/animation/animation_renderer_spec.ts b/modules/@angular/platform-browser/test/animation/animation_renderer_spec.ts new file mode 100644 index 0000000000000..59363a75e9790 --- /dev/null +++ b/modules/@angular/platform-browser/test/animation/animation_renderer_spec.ts @@ -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('
'); + + TestBed.configureTestingModule({ + providers: [{provide: ɵAnimationEngine, useClass: MockAnimationEngine}], + imports: [BrowserModule, BrowserAnimationModule] + }); + }); + + function makeRenderer(animationTriggers: any[] = []) { + const type = { + 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('
'); + + 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('
'); + const element2 = el('
'); + 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('
'); + + 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 () => {}; + } +} diff --git a/modules/@angular/platform-browser/tsconfig-animations-testing.json b/modules/@angular/platform-browser/tsconfig-animations-testing.json new file mode 100644 index 0000000000000..a70bc519a59f0 --- /dev/null +++ b/modules/@angular/platform-browser/tsconfig-animations-testing.json @@ -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 + } +} diff --git a/modules/@angular/platform-browser/tsconfig-animations.json b/modules/@angular/platform-browser/tsconfig-animations.json new file mode 100644 index 0000000000000..9bbb37a4baf40 --- /dev/null +++ b/modules/@angular/platform-browser/tsconfig-animations.json @@ -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" + } +} diff --git a/modules/@angular/platform-browser/tsconfig-build.json b/modules/@angular/platform-browser/tsconfig-build.json index 4a840c74c39dd..508c08913b9ba 100644 --- a/modules/@angular/platform-browser/tsconfig-build.json +++ b/modules/@angular/platform-browser/tsconfig-build.json @@ -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": ".", diff --git a/modules/benchmarks/src/bootstrap_ng2.ts b/modules/benchmarks/src/bootstrap_ng2.ts index de71af4df411d..e7299eeb38d2d 100644 --- a/modules/benchmarks/src/bootstrap_ng2.ts +++ b/modules/benchmarks/src/bootstrap_ng2.ts @@ -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'}, diff --git a/modules/playground/src/bootstrap.ts b/modules/playground/src/bootstrap.ts index 15e6b32edc707..c95ca8fa0fb73 100644 --- a/modules/playground/src/bootstrap.ts +++ b/modules/playground/src/bootstrap.ts @@ -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'}, diff --git a/test-main.js b/test-main.js index 1c51cb2af24c2..0f988a2e677d5 100644 --- a/test-main.js +++ b/test-main.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'}, diff --git a/tools/gulp-tasks/public-api.js b/tools/gulp-tasks/public-api.js index 5aaa3b211110c..613eb89bc0b48 100644 --- a/tools/gulp-tasks/public-api.js +++ b/tools/gulp-tasks/public-api.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'; diff --git a/tools/public_api_guard/animations/typings/animations.d.ts b/tools/public_api_guard/animations/typings/animations.d.ts new file mode 100644 index 0000000000000..077ff5fb99f6b --- /dev/null +++ b/tools/public_api_guard/animations/typings/animations.d.ts @@ -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; diff --git a/tools/public_api_guard/core/typings/core.d.ts b/tools/public_api_guard/core/typings/core.d.ts index da45578d39f5c..1e4177d316ab0 100644 --- a/tools/public_api_guard/core/typings/core.d.ts +++ b/tools/public_api_guard/core/typings/core.d.ts @@ -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; templateUrl: string; - constructor(id: string, templateUrl: string, slotCount: number, encapsulation: ViewEncapsulation, styles: Array, animations: { - [key: string]: Function; - }); + constructor(id: string, templateUrl: string, slotCount: number, encapsulation: ViewEncapsulation, styles: Array, animations: any); } /** @experimental */ @@ -1031,15 +1027,6 @@ export interface TrackByFunction { /** @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; @@ -1049,12 +1036,6 @@ export declare const TRANSLATIONS_FORMAT: InjectionToken; /** @experimental */ export declare function trigger(name: string, animation: AnimationMetadata[]): AnimationEntryMetadata; -/** @experimental */ -export interface Trigger { - name: string; - transitionFactories: TransitionFactory[]; -} - /** @stable */ export declare const Type: FunctionConstructor; diff --git a/tools/public_api_guard/platform-browser/typings/animations/animations.d.ts b/tools/public_api_guard/platform-browser/typings/animations/animations.d.ts new file mode 100644 index 0000000000000..ef3ac1f4dd84e --- /dev/null +++ b/tools/public_api_guard/platform-browser/typings/animations/animations.d.ts @@ -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 { +} diff --git a/tools/public_api_guard/platform-browser/typings/animations/testing/testing.d.ts b/tools/public_api_guard/platform-browser/typings/animations/testing/testing.d.ts new file mode 100644 index 0000000000000..21d567f2ec830 --- /dev/null +++ b/tools/public_api_guard/platform-browser/typings/animations/testing/testing.d.ts @@ -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; +}