From 5e0f8cf3f06f866634ffc0ff6d73081f42ed4225 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Wed, 25 May 2016 12:46:22 -0700 Subject: [PATCH] feat(core): introduce support for animations Closes #8734 --- modules/@angular/compiler/core_private.ts | 22 + .../compiler/src/animation/animation_ast.ts | 98 ++ .../src/animation/animation_compiler.ts | 368 +++++++ .../src/animation/animation_parser.ts | 561 +++++++++++ .../src/animation/styles_collection.ts | 52 + .../@angular/compiler/src/compile_metadata.ts | 197 +++- .../compiler/src/directive_normalizer.ts | 3 +- modules/@angular/compiler/src/identifiers.ts | 60 +- .../compiler/src/metadata_resolver.ts | 46 +- .../@angular/compiler/src/offline_compiler.ts | 2 +- .../compiler/src/output/interpretive_view.ts | 8 + .../@angular/compiler/src/runtime_compiler.ts | 6 +- modules/@angular/compiler/src/template_ast.ts | 7 +- .../@angular/compiler/src/template_parser.ts | 63 +- .../src/view_compiler/compile_view.ts | 10 +- .../src/view_compiler/property_binder.ts | 81 +- .../src/view_compiler/view_builder.ts | 11 +- .../src/view_compiler/view_compiler.ts | 12 +- .../@angular/compiler/src/view_resolver.ts | 3 +- .../test/animation/animation_compiler_spec.ts | 82 ++ .../test/animation/animation_parser_spec.ts | 469 +++++++++ .../compiler/test/compile_metadata_spec.ts | 115 ++- .../compiler/test/template_parser_spec.ts | 7 + .../compiler/test/view_resolver_mock_spec.ts | 1 - .../testing/test_component_builder.ts | 12 + .../compiler/testing/view_resolver_mock.ts | 46 +- modules/@angular/core/index.ts | 4 + modules/@angular/core/private_export.ts | 60 ++ .../animation/active_animation_players_map.ts | 57 ++ .../core/src/animation/animation_constants.ts | 3 + .../core/src/animation/animation_driver.ts | 15 + .../src/animation/animation_group_player.ts | 72 ++ .../core/src/animation/animation_keyframe.ts | 5 + .../core/src/animation/animation_player.ts | 39 + .../animation/animation_sequence_player.ts | 83 ++ .../src/animation/animation_style_util.ts | 113 +++ .../core/src/animation/animation_styles.ts | 3 + .../@angular/core/src/animation/metadata.ts | 116 +++ .../@angular/core/src/debug/debug_renderer.ts | 8 + modules/@angular/core/src/linker/element.ts | 3 +- modules/@angular/core/src/linker/view.ts | 74 +- modules/@angular/core/src/metadata.dart | 13 +- modules/@angular/core/src/metadata.ts | 9 +- .../@angular/core/src/metadata/directives.ts | 7 +- modules/@angular/core/src/metadata/view.ts | 8 +- modules/@angular/core/src/render/api.ts | 5 + .../active_animations_players_map_spec.ts | 85 ++ .../animation/animation_group_player_spec.ts | 194 ++++ .../animation/animation_integration_spec.ts | 899 ++++++++++++++++++ .../test/animation/animation_player_spec.ts | 41 + .../animation_sequence_player_spec.ts | 216 +++++ .../animation/animation_style_util_spec.ts | 195 ++++ .../animation/mock_animation_driver.ts | 35 + .../animation/mock_animation_player.ts | 47 + .../integration_test/public_api_spec.ts | 21 + .../@angular/platform-browser/core_private.ts | 24 + .../platform-browser/src/animate/animation.ts | 203 ---- .../src/animate/animation_builder.ts | 19 - .../src/animate/browser_details.ts | 56 -- .../src/animate/css_animation_builder.ts | 93 -- .../src/animate/css_animation_options.ts | 22 - .../@angular/platform-browser/src/browser.ts | 15 +- .../src/browser/browser_adapter.ts | 4 +- .../platform-browser/src/dom/dom_adapter.ts | 1 + .../src/dom/dom_animate_player.ts | 9 + .../platform-browser/src/dom/dom_renderer.ts | 65 +- .../src/dom/web_animations_driver.ts | 136 +++ .../src/dom/web_animations_player.ts | 57 ++ .../src/web_workers/worker/renderer.ts | 8 + .../src/web_workers/worker/worker_adapter.ts | 3 +- .../platform-browser/src/worker_render.ts | 13 +- .../test/animate/animation_builder_spec.ts | 146 --- .../test/dom/web_animations_player_spec.ts | 154 +++ .../testing/animation_builder_mock.ts | 33 - .../testing/browser_static.ts | 5 +- .../@angular/platform-server/core_private.ts | 10 + .../src/abstract_html_adapter.dart | 9 + .../platform-server/src/parse5_adapter.ts | 1 + .../platform-server/testing/server.ts | 7 +- .../playground/src/animate/app/animate-app.ts | 78 +- .../src/animate/css/animate-app.css | 21 + modules/playground/src/animate/css/app.css | 25 - tools/public_api_guard/public_api_spec.ts | 61 +- 83 files changed, 5294 insertions(+), 756 deletions(-) create mode 100644 modules/@angular/compiler/src/animation/animation_ast.ts create mode 100644 modules/@angular/compiler/src/animation/animation_compiler.ts create mode 100644 modules/@angular/compiler/src/animation/animation_parser.ts create mode 100644 modules/@angular/compiler/src/animation/styles_collection.ts create mode 100644 modules/@angular/compiler/test/animation/animation_compiler_spec.ts create mode 100644 modules/@angular/compiler/test/animation/animation_parser_spec.ts create mode 100644 modules/@angular/core/src/animation/active_animation_players_map.ts create mode 100644 modules/@angular/core/src/animation/animation_constants.ts create mode 100644 modules/@angular/core/src/animation/animation_driver.ts create mode 100644 modules/@angular/core/src/animation/animation_group_player.ts create mode 100644 modules/@angular/core/src/animation/animation_keyframe.ts create mode 100644 modules/@angular/core/src/animation/animation_player.ts create mode 100644 modules/@angular/core/src/animation/animation_sequence_player.ts create mode 100644 modules/@angular/core/src/animation/animation_style_util.ts create mode 100644 modules/@angular/core/src/animation/animation_styles.ts create mode 100644 modules/@angular/core/src/animation/metadata.ts create mode 100644 modules/@angular/core/test/animation/active_animations_players_map_spec.ts create mode 100644 modules/@angular/core/test/animation/animation_group_player_spec.ts create mode 100644 modules/@angular/core/test/animation/animation_integration_spec.ts create mode 100644 modules/@angular/core/test/animation/animation_player_spec.ts create mode 100644 modules/@angular/core/test/animation/animation_sequence_player_spec.ts create mode 100644 modules/@angular/core/test/animation/animation_style_util_spec.ts create mode 100644 modules/@angular/core/testing/animation/mock_animation_driver.ts create mode 100644 modules/@angular/core/testing/animation/mock_animation_player.ts delete mode 100644 modules/@angular/platform-browser/src/animate/animation.ts delete mode 100644 modules/@angular/platform-browser/src/animate/animation_builder.ts delete mode 100644 modules/@angular/platform-browser/src/animate/browser_details.ts delete mode 100644 modules/@angular/platform-browser/src/animate/css_animation_builder.ts delete mode 100644 modules/@angular/platform-browser/src/animate/css_animation_options.ts create mode 100644 modules/@angular/platform-browser/src/dom/dom_animate_player.ts create mode 100644 modules/@angular/platform-browser/src/dom/web_animations_driver.ts create mode 100644 modules/@angular/platform-browser/src/dom/web_animations_player.ts delete mode 100644 modules/@angular/platform-browser/test/animate/animation_builder_spec.ts create mode 100644 modules/@angular/platform-browser/test/dom/web_animations_player_spec.ts delete mode 100644 modules/@angular/platform-browser/testing/animation_builder_mock.ts create mode 100644 modules/@angular/platform-server/core_private.ts create mode 100644 modules/playground/src/animate/css/animate-app.css delete mode 100644 modules/playground/src/animate/css/app.css diff --git a/modules/@angular/compiler/core_private.ts b/modules/@angular/compiler/core_private.ts index 7d1a09834a..de7757bd35 100644 --- a/modules/@angular/compiler/core_private.ts +++ b/modules/@angular/compiler/core_private.ts @@ -58,3 +58,25 @@ export var pureProxy10: typeof t.pureProxy10 = r.pureProxy10; export var castByValue: typeof t.castByValue = r.castByValue; export type Console = t.Console; export var Console: typeof t.Console = r.Console; + +export type NoOpAnimationPlayer = t.NoOpAnimationPlayer; +export var NoOpAnimationPlayer: typeof t.NoOpAnimationPlayer = r.NoOpAnimationPlayer; +export type AnimationPlayer = t.AnimationPlayer; +export var AnimationPlayer: typeof t.AnimationPlayer = r.AnimationPlayer; +export type NoOpAnimationDriver = t.NoOpAnimationDriver; +export var NoOpAnimationDriver: typeof t.NoOpAnimationDriver = r.NoOpAnimationDriver; +export type AnimationDriver = t.AnimationDriver; +export var AnimationDriver: typeof t.AnimationDriver = r.AnimationDriver; +export type AnimationSequencePlayer = t.AnimationSequencePlayer; +export var AnimationSequencePlayer: typeof t.AnimationSequencePlayer = r.AnimationSequencePlayer; +export type AnimationGroupPlayer = t.AnimationGroupPlayer; +export var AnimationGroupPlayer: typeof t.AnimationGroupPlayer = r.AnimationGroupPlayer; +export type AnimationKeyframe = t.AnimationKeyframe; +export var AnimationKeyframe: typeof t.AnimationKeyframe = r.AnimationKeyframe; +export type AnimationStyleUtil = t.AnimationStyleUtil; +export var AnimationStyleUtil: typeof t.AnimationStyleUtil = r.AnimationStyleUtil; +export type AnimationStylrs = t.AnimationStyles; +export var AnimationStyles: typeof t.AnimationStyles = r.AnimationStyles; +export var ANY_STATE = r.ANY_STATE; +export var EMPTY_STATE = r.EMPTY_STATE; +export var FILL_STYLE_FLAG = r.FILL_STYLE_FLAG; diff --git a/modules/@angular/compiler/src/animation/animation_ast.ts b/modules/@angular/compiler/src/animation/animation_ast.ts new file mode 100644 index 0000000000..527c362e6a --- /dev/null +++ b/modules/@angular/compiler/src/animation/animation_ast.ts @@ -0,0 +1,98 @@ +export abstract class AnimationAst { + public startTime: number = 0; + public playTime: number = 0; + abstract visit(visitor: AnimationAstVisitor, context: any): any; +} + +export abstract class AnimationStateAst extends AnimationAst { + abstract visit(visitor: AnimationAstVisitor, context: any): any; +} + +export interface AnimationAstVisitor { + visitAnimationEntry(ast: AnimationEntryAst, context: any): any; + visitAnimationStateDeclaration(ast: AnimationStateDeclarationAst, context: any): any; + visitAnimationStateTransition(ast: AnimationStateTransitionAst, context: any): any; + visitAnimationStep(ast: AnimationStepAst, context: any): any; + visitAnimationSequence(ast: AnimationSequenceAst, context: any): any; + visitAnimationGroup(ast: AnimationGroupAst, context: any): any; + visitAnimationKeyframe(ast: AnimationKeyframeAst, context: any): any; + visitAnimationStyles(ast: AnimationStylesAst, context: any): any; +} + +export class AnimationEntryAst extends AnimationAst { + constructor(public name: string, + public stateDeclarations: AnimationStateDeclarationAst[], + public stateTransitions: AnimationStateTransitionAst[]) { + super(); + } + visit(visitor: AnimationAstVisitor, context: any): any { + return visitor.visitAnimationEntry(this, context); + } +} + +export class AnimationStateDeclarationAst extends AnimationStateAst { + constructor(public stateName: string, public styles: AnimationStylesAst) { + super(); + } + visit(visitor: AnimationAstVisitor, context: any): any { + return visitor.visitAnimationStateDeclaration(this, context); + } +} + +export class AnimationStateTransitionExpression { + constructor(public fromState: string, public toState: string) {} +} + +export class AnimationStateTransitionAst extends AnimationStateAst { + constructor(public stateChanges: AnimationStateTransitionExpression[], public animation: AnimationSequenceAst) { + super(); + } + visit(visitor: AnimationAstVisitor, context: any): any { + return visitor.visitAnimationStateTransition(this, context); + } +} + +export class AnimationStepAst extends AnimationAst { + constructor(public startingStyles: AnimationStylesAst, + public keyframes: AnimationKeyframeAst[], + public duration: number, + public delay: number, + public easing: string) { + super(); + } + visit(visitor: AnimationAstVisitor, context: any): any { + return visitor.visitAnimationStep(this, context); + } +} + +export class AnimationStylesAst extends AnimationAst { + constructor(public styles: Array<{[key: string]: string | number}>) { super(); } + visit(visitor: AnimationAstVisitor, context: any): any { + return visitor.visitAnimationStyles(this, context); + } +} + +export class AnimationKeyframeAst extends AnimationAst { + constructor(public offset: number, public styles: AnimationStylesAst) { super(); } + visit(visitor: AnimationAstVisitor, context: any): any { + return visitor.visitAnimationKeyframe(this, context); + } +} + +export abstract class AnimationWithStepsAst extends AnimationAst { + constructor(public steps: AnimationAst[]) { super(); } +} + +export class AnimationGroupAst extends AnimationWithStepsAst { + constructor(steps: AnimationAst[]) { super(steps); } + visit(visitor: AnimationAstVisitor, context: any): any { + return visitor.visitAnimationGroup(this, context); + } +} + +export class AnimationSequenceAst extends AnimationWithStepsAst { + constructor(steps: AnimationAst[]) { super(steps); } + visit(visitor: AnimationAstVisitor, context: any): any { + return visitor.visitAnimationSequence(this, context); + } +} diff --git a/modules/@angular/compiler/src/animation/animation_compiler.ts b/modules/@angular/compiler/src/animation/animation_compiler.ts new file mode 100644 index 0000000000..c516fe518b --- /dev/null +++ b/modules/@angular/compiler/src/animation/animation_compiler.ts @@ -0,0 +1,368 @@ +import {BaseException} from '../facade/exceptions'; +import {ListWrapper, Map, StringMapWrapper} from '../facade/collection'; +import {isPresent, isBlank, isArray} from '../facade/lang'; + +import {Identifiers} from '../identifiers'; +import * as o from '../output/output_ast'; + +import {AUTO_STYLE} from '@angular/core'; +import {ANY_STATE, EMPTY_STATE} from '../../core_private'; + +import { + AnimationParseError, + ParsedAnimationResult, + parseAnimationEntry +} from './animation_parser'; + +import {CompileDirectiveMetadata} from "../compile_metadata"; + +import { + AnimationAst, + AnimationEntryAst, + AnimationStateAst, + AnimationStateDeclarationAst, + AnimationStateTransitionAst, + AnimationKeyframeAst, + AnimationStylesAst, + AnimationSequenceAst, + AnimationGroupAst, + AnimationStepAst, + AnimationAstVisitor +} from './animation_ast'; + +export class CompiledAnimation { + constructor(public name: string, + public statesMapStatement: o.Statement, + public statesVariableName: string, + public fnStatement: o.Statement, + public fnVariable: o.Expression) {} +} + +export class AnimationCompiler { + compileComponent(component: CompileDirectiveMetadata): CompiledAnimation[] { + var compiledAnimations: CompiledAnimation[] = []; + var index = 0; + component.template.animations.forEach(entry => { + var result = parseAnimationEntry(entry); + if (result.errors.length > 0) { + var errorMessage = ''; + result.errors.forEach((error: AnimationParseError) => { errorMessage += "\n- " + error.msg; }); + // todo (matsko): include the component name when throwing + throw new BaseException( + `Unable to parse the animation sequence for "${entry.name}" due to the following errors: ` + + errorMessage); + } + + var factoryName = `${component.type.name}_${entry.name}_${index}`; + index++; + + var visitor = new _AnimationBuilder(entry.name, factoryName); + compiledAnimations.push(visitor.build(result.ast)); + }); + return compiledAnimations; + } +} + +var _ANIMATION_FACTORY_ELEMENT_VAR = o.variable('element'); +var _ANIMATION_FACTORY_VIEW_VAR = o.variable('view'); +var _ANIMATION_FACTORY_RENDERER_VAR = _ANIMATION_FACTORY_VIEW_VAR.prop('renderer'); +var _ANIMATION_CURRENT_STATE_VAR = o.variable('currentState'); +var _ANIMATION_NEXT_STATE_VAR = o.variable('nextState'); +var _ANIMATION_PLAYER_VAR = o.variable('player'); +var _ANIMATION_START_STATE_STYLES_VAR = o.variable('startStateStyles'); +var _ANIMATION_END_STATE_STYLES_VAR = o.variable('endStateStyles'); +var _ANIMATION_COLLECTED_STYLES = o.variable('collectedStyles'); +var EMPTY_MAP = o.literalMap([]); + +class _AnimationBuilder implements AnimationAstVisitor { + private _fnVarName: string; + private _statesMapVarName: string; + private _statesMapVar: any; + + constructor(public animationName: string, factoryName: string) { + this._fnVarName = factoryName + '_factory'; + this._statesMapVarName = factoryName + '_states'; + this._statesMapVar = o.variable(this._statesMapVarName); + } + + visitAnimationStyles(ast: AnimationStylesAst, + context: _AnimationBuilderContext): o.Expression { + var stylesArr = []; + if (context.isExpectingFirstStyleStep) { + stylesArr.push(_ANIMATION_START_STATE_STYLES_VAR); + context.isExpectingFirstStyleStep = false; + } + + ast.styles.forEach(entry => { + stylesArr.push(o.literalMap(StringMapWrapper.keys(entry).map(key => [key, o.literal(entry[key])]))); + }); + + return o.importExpr(Identifiers.AnimationStyles).instantiate([ + o.importExpr(Identifiers.collectAndResolveStyles).callFn([ + _ANIMATION_COLLECTED_STYLES, + o.literalArr(stylesArr) + ]) + ]); + } + + visitAnimationKeyframe(ast: AnimationKeyframeAst, + context: _AnimationBuilderContext): o.Expression { + return o.importExpr(Identifiers.AnimationKeyframe).instantiate([ + o.literal(ast.offset), + ast.styles.visit(this, context) + ]); + } + + visitAnimationStep(ast: AnimationStepAst, context: _AnimationBuilderContext): o.Expression { + if (context.endStateAnimateStep === ast) { + return this._visitEndStateAnimation(ast, context); + } + + var startingStylesExpr = ast.startingStyles.visit(this, context); + var keyframeExpressions = ast.keyframes.map(keyframeEntry => keyframeEntry.visit(this, context)); + return this._callAnimateMethod(ast, startingStylesExpr, o.literalArr(keyframeExpressions)); + } + + _visitEndStateAnimation(ast: AnimationStepAst, + context: _AnimationBuilderContext): o.Expression { + var startingStylesExpr = ast.startingStyles.visit(this, context); + var keyframeExpressions = ast.keyframes.map(keyframe => keyframe.visit(this, context)); + var keyframesExpr = o.importExpr(Identifiers.balanceAnimationKeyframes).callFn([ + _ANIMATION_COLLECTED_STYLES, + _ANIMATION_END_STATE_STYLES_VAR, + o.literalArr(keyframeExpressions) + ]); + + return this._callAnimateMethod(ast, startingStylesExpr, keyframesExpr); + } + + _callAnimateMethod(ast: AnimationStepAst, startingStylesExpr, keyframesExpr) { + return _ANIMATION_FACTORY_RENDERER_VAR.callMethod('animate', [ + _ANIMATION_FACTORY_ELEMENT_VAR, + startingStylesExpr, + keyframesExpr, + o.literal(ast.duration), + o.literal(ast.delay), + o.literal(ast.easing) + ]); + } + + visitAnimationSequence(ast: AnimationSequenceAst, + context: _AnimationBuilderContext): o.Expression { + var playerExprs = ast.steps.map(step => step.visit(this, context)); + return o.importExpr(Identifiers.AnimationSequencePlayer).instantiate([ + o.literalArr(playerExprs)]); + } + + visitAnimationGroup(ast: AnimationGroupAst, context: _AnimationBuilderContext): o.Expression { + var playerExprs = ast.steps.map(step => step.visit(this, context)); + return o.importExpr(Identifiers.AnimationGroupPlayer).instantiate([ + o.literalArr(playerExprs)]); + } + + visitAnimationStateDeclaration(ast: AnimationStateDeclarationAst, context: _AnimationBuilderContext): void { + var flatStyles: {[key: string]: string|number} = {}; + _getStylesArray(ast).forEach(entry => { + StringMapWrapper.forEach(entry, (value, key) => { + flatStyles[key] = value; + }); + }); + context.stateMap.registerState(ast.stateName, flatStyles); + } + + visitAnimationStateTransition(ast: AnimationStateTransitionAst, context: _AnimationBuilderContext): any { + var steps = ast.animation.steps; + var lastStep = steps[steps.length - 1]; + if (_isEndStateAnimateStep(lastStep)) { + context.endStateAnimateStep = lastStep; + } + + context.isExpectingFirstStyleStep = true; + + var stateChangePreconditions = []; + + ast.stateChanges.forEach(stateChange => { + stateChangePreconditions.push( + _compareToAnimationStateExpr(_ANIMATION_CURRENT_STATE_VAR, stateChange.fromState) + .and(_compareToAnimationStateExpr(_ANIMATION_NEXT_STATE_VAR, stateChange.toState)) + ); + + if (stateChange.fromState != ANY_STATE) { + context.stateMap.registerState(stateChange.fromState); + } + + if (stateChange.toState != ANY_STATE) { + context.stateMap.registerState(stateChange.toState); + } + }); + + var animationPlayerExpr = ast.animation.visit(this, context); + + var reducedStateChangesPrecondition = stateChangePreconditions.reduce((a,b) => a.or(b)); + var precondition = _ANIMATION_PLAYER_VAR.equals(o.NULL_EXPR).and(reducedStateChangesPrecondition); + + return new o.IfStmt(precondition, [ + _ANIMATION_PLAYER_VAR.set(animationPlayerExpr).toStmt() + ]); + } + + visitAnimationEntry(ast: AnimationEntryAst, context: _AnimationBuilderContext): any { + //visit each of the declarations first to build the context state map + ast.stateDeclarations.forEach(def => def.visit(this, context)); + + var statements = []; + statements.push( + _ANIMATION_FACTORY_VIEW_VAR.callMethod('cancelActiveAnimation', [ + _ANIMATION_FACTORY_ELEMENT_VAR, + o.literal(this.animationName), + _ANIMATION_NEXT_STATE_VAR.equals(o.literal(EMPTY_STATE)) + ]).toStmt()); + + statements.push(_ANIMATION_COLLECTED_STYLES.set(EMPTY_MAP).toDeclStmt()); + statements.push(_ANIMATION_PLAYER_VAR.set(o.NULL_EXPR).toDeclStmt()); + + statements.push( + _ANIMATION_START_STATE_STYLES_VAR.set( + this._statesMapVar.key(_ANIMATION_CURRENT_STATE_VAR) + ).toDeclStmt()); + + statements.push( + new o.IfStmt(_ANIMATION_START_STATE_STYLES_VAR.equals(o.NULL_EXPR), [ + _ANIMATION_START_STATE_STYLES_VAR.set(EMPTY_MAP).toStmt() + ])); + + statements.push( + _ANIMATION_END_STATE_STYLES_VAR.set( + this._statesMapVar.key(_ANIMATION_NEXT_STATE_VAR) + ).toDeclStmt()); + + statements.push( + new o.IfStmt(_ANIMATION_END_STATE_STYLES_VAR.equals(o.NULL_EXPR), [ + _ANIMATION_END_STATE_STYLES_VAR.set(EMPTY_MAP).toStmt() + ])); + + // before we start any animation we want to clear out the starting + // styles from the element's style property (since they were placed + // there at the end of the last animation + statements.push( + _ANIMATION_FACTORY_RENDERER_VAR.callMethod('setElementStyles', [ + _ANIMATION_FACTORY_ELEMENT_VAR, + o.importExpr(Identifiers.clearAnimationStyles).callFn([_ANIMATION_START_STATE_STYLES_VAR]) + ]).toStmt()); + + ast.stateTransitions.forEach(transAst => statements.push(transAst.visit(this, context))); + + // this check ensures that the animation factory always returns a player + // so that the onDone callback can be used for tracking + statements.push( + new o.IfStmt(_ANIMATION_PLAYER_VAR.equals(o.NULL_EXPR), [ + _ANIMATION_PLAYER_VAR.set( + o.importExpr(Identifiers.NoOpAnimationPlayer).instantiate([]) + ).toStmt() + ])); + + // once complete we want to apply the styles on the element + // since the destination state's values should persist once + // the animation sequence has completed. + statements.push( + _ANIMATION_PLAYER_VAR.callMethod('onDone', [ + o.fn([], [ + _ANIMATION_FACTORY_RENDERER_VAR.callMethod('setElementStyles', [ + _ANIMATION_FACTORY_ELEMENT_VAR, + o.importExpr(Identifiers.balanceAnimationStyles).callFn([ + _ANIMATION_START_STATE_STYLES_VAR, + _ANIMATION_END_STATE_STYLES_VAR + ]) + ]).toStmt() + ]) + ]).toStmt()); + + statements.push( + _ANIMATION_FACTORY_VIEW_VAR.callMethod('registerAndStartAnimation', [ + _ANIMATION_FACTORY_ELEMENT_VAR, + o.literal(this.animationName), + _ANIMATION_PLAYER_VAR + ]).toStmt()); + + return o.fn([ + new o.FnParam(_ANIMATION_FACTORY_VIEW_VAR.name, o.importType(Identifiers.AppView)), + new o.FnParam(_ANIMATION_FACTORY_ELEMENT_VAR.name, o.DYNAMIC_TYPE), + new o.FnParam(_ANIMATION_CURRENT_STATE_VAR.name, o.DYNAMIC_TYPE), + new o.FnParam(_ANIMATION_NEXT_STATE_VAR.name, o.DYNAMIC_TYPE) + ], statements); + } + + build(ast: AnimationAst): CompiledAnimation { + var context = new _AnimationBuilderContext(); + var fnStatement = ast.visit(this, context).toDeclStmt(this._fnVarName); + var fnVariable = o.variable(this._fnVarName); + + var lookupMap = []; + StringMapWrapper.forEach(context.stateMap.states, (value, stateName) => { + var variableValue = EMPTY_MAP; + if (isPresent(value)) { + let styleMap = []; + StringMapWrapper.forEach(value, (value, key) => { + styleMap.push([key, o.literal(value)]); + }); + variableValue = o.literalMap(styleMap); + } + lookupMap.push([stateName, variableValue]); + }); + + var compiledStatesMapExpr = this._statesMapVar.set(o.literalMap(lookupMap)).toDeclStmt(); + return new CompiledAnimation(this.animationName, + compiledStatesMapExpr, + this._statesMapVarName, + fnStatement, + fnVariable); + } +} + +class _AnimationBuilderContext { + stateMap = new _AnimationBuilderStateMap(); + endStateAnimateStep: AnimationStepAst = null; + isExpectingFirstStyleStep = false; +} + +class _AnimationBuilderStateMap { + private _states: {[key: string]: {[prop: string]: string|number}} = {}; + get states() { return this._states; } + registerState(name: string, value: {[prop: string]: string|number} = null): void { + var existingEntry = this._states[name]; + if (isBlank(existingEntry)) { + this._states[name] = value; + } + } +} + +function _compareToAnimationStateExpr(value: o.Expression, animationState: string): o.Expression { + var emptyStateLiteral = o.literal(EMPTY_STATE); + switch (animationState) { + case EMPTY_STATE: + return value.equals(emptyStateLiteral); + + case ANY_STATE: + return o.literal(true); + + default: + return value.equals(o.literal(animationState)); + } +} + +function _isEndStateAnimateStep(step: AnimationAst): boolean { + // the final animation step is characterized by having only TWO + // keyframe values and it must have zero styles for both keyframes + if (step instanceof AnimationStepAst + && step.duration > 0 + && step.keyframes.length == 2) { + var styles1 = _getStylesArray(step.keyframes[0])[0]; + var styles2 = _getStylesArray(step.keyframes[1])[0]; + return StringMapWrapper.isEmpty(styles1) && StringMapWrapper.isEmpty(styles2); + } + return false; +} + +function _getStylesArray(obj: any) { + return obj.styles.styles; +} diff --git a/modules/@angular/compiler/src/animation/animation_parser.ts b/modules/@angular/compiler/src/animation/animation_parser.ts new file mode 100644 index 0000000000..b6f8ffa003 --- /dev/null +++ b/modules/@angular/compiler/src/animation/animation_parser.ts @@ -0,0 +1,561 @@ +import {ListWrapper, StringMapWrapper} from '../facade/collection'; +import {Math} from '../facade/math'; +import {ANY_STATE, EMPTY_STATE} from '../../core_private'; +import { + IS_DART, + RegExpWrapper, + isArray, + isPresent, + isBlank, + isNumber, + isString, + isStringMap, + NumberWrapper +} from '../facade/lang'; + +import {FILL_STYLE_FLAG} from '../../core_private'; + +import { + CompileAnimationEntryMetadata, + CompileAnimationStateMetadata, + CompileAnimationStateDeclarationMetadata, + CompileAnimationStateTransitionMetadata, + CompileAnimationMetadata, + CompileAnimationWithStepsMetadata, + CompileAnimationStyleMetadata, + CompileAnimationAnimateMetadata, + CompileAnimationGroupMetadata, + CompileAnimationSequenceMetadata, + CompileAnimationKeyframesSequenceMetadata +} from '../compile_metadata'; + +import { + AnimationAst, + AnimationEntryAst, + AnimationStateAst, + AnimationStateTransitionAst, + AnimationStateDeclarationAst, + AnimationKeyframeAst, + AnimationStylesAst, + AnimationWithStepsAst, + AnimationSequenceAst, + AnimationGroupAst, + AnimationStepAst, + AnimationStateTransitionExpression +} from './animation_ast'; + +import {StylesCollection} from './styles_collection'; +import {ParseError} from '../parse_util'; + +const _INITIAL_KEYFRAME = 0; +const _TERMINAL_KEYFRAME = 1; +const _ONE_SECOND = 1000; + +export class AnimationParseError extends ParseError { + constructor(message) { super(null, message); } + toString(): string { return `${this.msg}`; } +} + +export class ParsedAnimationResult { + constructor(public ast: AnimationEntryAst, public errors: AnimationParseError[]) {} +} + +export function parseAnimationEntry(entry: CompileAnimationEntryMetadata): ParsedAnimationResult { + var errors: AnimationParseError[] = []; + var stateStyles: {[key: string]: AnimationStylesAst} = {}; + var transitions: CompileAnimationStateTransitionMetadata[] = []; + + var stateDeclarationAsts = []; + entry.definitions.forEach(def => { + if (def instanceof CompileAnimationStateDeclarationMetadata) { + _parseAnimationDeclarationStates(def, errors).forEach(ast => { + stateDeclarationAsts.push(ast); + stateStyles[ast.stateName] = ast.styles; + }); + } else { + transitions.push(def); + } + }); + + var stateTransitionAsts = transitions.map(transDef => + _parseAnimationStateTransition(transDef, stateStyles, errors)); + + var ast = new AnimationEntryAst(entry.name, stateDeclarationAsts, stateTransitionAsts); + return new ParsedAnimationResult(ast, errors); +} + +function _parseAnimationDeclarationStates(stateMetadata: CompileAnimationStateDeclarationMetadata, errors: AnimationParseError[]): AnimationStateDeclarationAst[] { + var styleValues: {[key: string]: string|number}[] = []; + stateMetadata.styles.styles.forEach(stylesEntry => { + // TODO (matsko): change this when we get CSS class integration support + if (isStringMap(stylesEntry)) { + styleValues.push(<{[key: string]: string|number}>stylesEntry); + } else { + errors.push(new AnimationParseError(`State based animations cannot contain references to other states`)); + } + }); + var defStyles = new AnimationStylesAst(styleValues); + + var states = stateMetadata.stateNameExpr.split(/\s*,\s*/); + return states.map(state => new AnimationStateDeclarationAst(state, defStyles)); +} + +function _parseAnimationStateTransition(transitionStateMetadata: CompileAnimationStateTransitionMetadata, + stateStyles: {[key: string]: AnimationStylesAst}, + errors: AnimationParseError[]): AnimationStateTransitionAst { + var styles = new StylesCollection(); + var transitionExprs = []; + var transitionStates = transitionStateMetadata.stateChangeExpr.split(/\s*,\s*/); + transitionStates.forEach(expr => { + _parseAnimationTransitionExpr(expr, errors).forEach(transExpr => { + transitionExprs.push(transExpr); + }); + }); + var entry = _normalizeAnimationEntry(transitionStateMetadata.animation); + var animation = _normalizeStyleSteps(entry, stateStyles, errors); + var animationAst = _parseTransitionAnimation(animation, 0, styles, stateStyles, errors); + if (errors.length == 0) { + _fillAnimationAstStartingKeyframes(animationAst, styles, errors); + } + + var sequenceAst = (animationAst instanceof AnimationSequenceAst) + ? animationAst + : new AnimationSequenceAst([animationAst]); + + return new AnimationStateTransitionAst(transitionExprs, sequenceAst); +} + +function _parseAnimationTransitionExpr(eventStr: string, errors: AnimationParseError[]): AnimationStateTransitionExpression[] { + var expressions = []; + var match = eventStr.match(/^(\*|[-\w]+)\s*()\s*(\*|[-\w]+)$/); + if (!isPresent(match) || match.length < 4) { + errors.push(new AnimationParseError(`the provided ${eventStr} is not of a supported format`)); + return expressions; + } + + var fromState = match[1]; + var separator = match[2]; + var toState = match[3]; + expressions.push( + new AnimationStateTransitionExpression(fromState, toState)); + + var isFullAnyStateExpr = fromState == ANY_STATE && toState == ANY_STATE; + if (separator[0] == '<' && !isFullAnyStateExpr) { + expressions.push( + new AnimationStateTransitionExpression(toState, fromState)); + } + return expressions; +} + +function _fetchSylesFromState(stateName: string, + stateStyles: {[key: string]: AnimationStylesAst}): CompileAnimationStyleMetadata { + var entry = stateStyles[stateName]; + if (isPresent(entry)) { + var styles = <{[key: string]: string | number}[]>entry.styles; + return new CompileAnimationStyleMetadata(0, styles); + } + return null; +} + +function _normalizeAnimationEntry(entry: CompileAnimationMetadata | CompileAnimationMetadata[]) + :CompileAnimationMetadata { + return isArray(entry) + ? new CompileAnimationSequenceMetadata(entry) + : entry; +} + +function _normalizeStyleMetadata(entry: CompileAnimationStyleMetadata, + stateStyles: {[key: string]: AnimationStylesAst}, + errors: AnimationParseError[]): Array<{[key: string]: string|number}> { + var normalizedStyles = []; + entry.styles.forEach(styleEntry => { + if (isString(styleEntry)) { + ListWrapper.addAll(normalizedStyles, _resolveStylesFromState(styleEntry, stateStyles, errors)); + } else { + normalizedStyles.push(<{[key: string]: string | number}>styleEntry); + } + }); + return normalizedStyles; +} + +function _normalizeStyleSteps(entry: CompileAnimationMetadata, + stateStyles: {[key: string]: AnimationStylesAst}, + errors: AnimationParseError[]): CompileAnimationMetadata { + var steps = _normalizeStyleStepEntry(entry, stateStyles, errors); + return new CompileAnimationSequenceMetadata(steps); +} + +function _mergeAnimationStyles(stylesList: any[], newItem: {[key: string]: string|number}|string) { + if (isStringMap(newItem) && stylesList.length > 0) { + var lastIndex = stylesList.length - 1; + var lastItem = stylesList[lastIndex]; + if (isStringMap(lastItem)) { + stylesList[lastIndex] = StringMapWrapper.merge( + <{[key: string]: string|number}>lastItem, + <{[key: string]: string|number}>newItem + ); + return; + } + } + stylesList.push(newItem); +} + +function _normalizeStyleStepEntry(entry: CompileAnimationMetadata, + stateStyles: {[key: string]: AnimationStylesAst}, + errors: AnimationParseError[]): CompileAnimationMetadata[] { + var steps: CompileAnimationMetadata[]; + if (entry instanceof CompileAnimationWithStepsMetadata) { + steps = entry.steps; + } else { + return [entry]; + } + + var newSteps: CompileAnimationMetadata[] = []; + var combinedStyles: {[key: string]: string | number}[]; + steps.forEach(step => { + if (step instanceof CompileAnimationStyleMetadata) { + // this occurs when a style step is followed by a previous style step + // or when the first style step is run. We want to concatenate all subsequent + // style steps together into a single style step such that we have the correct + // starting keyframe data to pass into the animation player. + if (!isPresent(combinedStyles)) { + combinedStyles = []; + } + _normalizeStyleMetadata(step, stateStyles, errors).forEach(entry => { + _mergeAnimationStyles(combinedStyles, entry); + }); + } else { + // it is important that we create a metadata entry of the combined styles + // before we go on an process the animate, sequence or group metadata steps. + // This will ensure that the AST will have the previous styles painted on + // screen before any further animations that use the styles take place. + if (isPresent(combinedStyles)) { + newSteps.push(new CompileAnimationStyleMetadata(0, combinedStyles)); + combinedStyles = null; + } + + if (step instanceof CompileAnimationAnimateMetadata) { + // we do not recurse into CompileAnimationAnimateMetadata since + // those style steps are not going to be squashed + var animateStyleValue = (step).styles; + if (animateStyleValue instanceof CompileAnimationStyleMetadata) { + animateStyleValue.styles = _normalizeStyleMetadata(animateStyleValue, stateStyles, errors); + } else if (animateStyleValue instanceof CompileAnimationKeyframesSequenceMetadata) { + animateStyleValue.steps.forEach(step => { + step.styles = _normalizeStyleMetadata(step, stateStyles, errors); + }); + } + } else if (step instanceof CompileAnimationWithStepsMetadata) { + let innerSteps = _normalizeStyleStepEntry(step, stateStyles, errors); + step = step instanceof CompileAnimationGroupMetadata + ? new CompileAnimationGroupMetadata(innerSteps) + : new CompileAnimationSequenceMetadata(innerSteps); + } + + newSteps.push(step); + } + }); + + // this happens when only styles were animated within the sequence + if (isPresent(combinedStyles)) { + newSteps.push(new CompileAnimationStyleMetadata(0, combinedStyles)); + } + + return newSteps; +} + + +function _resolveStylesFromState(stateName: string, stateStyles: {[key: string]: AnimationStylesAst}, errors: AnimationParseError[]) { + var styles: {[key: string]: string|number}[] = []; + if (stateName[0] != ':') { + errors.push(new AnimationParseError(`Animation states via styles must be prefixed with a ":"`)); + } else { + var normalizedStateName = stateName.substring(1); + var value = stateStyles[normalizedStateName]; + if (!isPresent(value)) { + errors.push(new AnimationParseError(`Unable to apply styles due to missing a state: "${normalizedStateName}"`)); + } else { + value.styles.forEach(stylesEntry => { + if (isStringMap(stylesEntry)) { + styles.push(<{[key: string]: string | number}>stylesEntry); + } + }); + } + } + return styles; +} + +class _AnimationTimings { + constructor(public duration: number, public delay: number, public easing: string) {} +} + +function _parseAnimationKeyframes(keyframeSequence: CompileAnimationKeyframesSequenceMetadata, + currentTime: number, + collectedStyles: StylesCollection, + stateStyles: {[key: string]: AnimationStylesAst}, + errors: AnimationParseError[]): AnimationKeyframeAst[] { + var totalEntries = keyframeSequence.steps.length; + var totalOffsets = 0; + keyframeSequence.steps.forEach(step => totalOffsets += (isPresent(step.offset) ? 1 : 0)); + + if (totalOffsets > 0 && totalOffsets < totalEntries) { + errors.push(new AnimationParseError(`Not all style() entries contain an offset for the provided keyframe()`)); + totalOffsets = totalEntries; + } + + var limit = totalEntries - 1; + var margin = totalOffsets == 0 ? (1 / limit) : 0; + var rawKeyframes = []; + var index = 0; + var doSortKeyframes = false; + var lastOffset = 0; + keyframeSequence.steps.forEach(styleMetadata => { + var offset = styleMetadata.offset; + var keyframeStyles: {[key: string]: string|number} = {}; + styleMetadata.styles.forEach(entry => { + StringMapWrapper.forEach(<{[key: string]: string|number}>entry, (value, prop) => { + if (prop != 'offset') { + keyframeStyles[prop] = value; + } + }); + }); + + if (isPresent(offset)) { + doSortKeyframes = doSortKeyframes || (offset < lastOffset); + } else { + offset = index == limit ? _TERMINAL_KEYFRAME : (margin * index); + } + + rawKeyframes.push([offset, keyframeStyles]); + lastOffset = offset; + index++; + }); + + if (doSortKeyframes) { + ListWrapper.sort(rawKeyframes, (a,b) => a[0] <= b[0] ? -1 : 1); + } + + var i; + var firstKeyframe = rawKeyframes[0]; + if (firstKeyframe[0] != _INITIAL_KEYFRAME) { + ListWrapper.insert(rawKeyframes, 0, firstKeyframe = [_INITIAL_KEYFRAME, {}]); + } + + var firstKeyframeStyles = firstKeyframe[1]; + var limit = rawKeyframes.length - 1; + var lastKeyframe = rawKeyframes[limit]; + if (lastKeyframe[0] != _TERMINAL_KEYFRAME) { + rawKeyframes.push(lastKeyframe = [_TERMINAL_KEYFRAME, {}]); + limit++; + } + + var lastKeyframeStyles = lastKeyframe[1]; + for (i = 1; i <= limit; i++) { + let entry = rawKeyframes[i]; + let styles = entry[1]; + + StringMapWrapper.forEach(styles, (value, prop) => { + if (!isPresent(firstKeyframeStyles[prop])) { + firstKeyframeStyles[prop] = FILL_STYLE_FLAG; + } + }); + } + + for (i = limit - 1; i >= 0; i--) { + let entry = rawKeyframes[i]; + let styles = entry[1]; + + StringMapWrapper.forEach(styles, (value, prop) => { + if (!isPresent(lastKeyframeStyles[prop])) { + lastKeyframeStyles[prop] = value; + } + }); + } + + return rawKeyframes.map(entry => new AnimationKeyframeAst(entry[0], new AnimationStylesAst([entry[1]]))); +} + +function _parseTransitionAnimation(entry: CompileAnimationMetadata, + currentTime: number, + collectedStyles: StylesCollection, + stateStyles: {[key: string]: AnimationStylesAst}, + errors: AnimationParseError[]): AnimationAst { + var ast; + var playTime = 0; + var startingTime = currentTime; + if (entry instanceof CompileAnimationWithStepsMetadata) { + var maxDuration = 0; + var steps = []; + var isGroup = entry instanceof CompileAnimationGroupMetadata; + var previousStyles; + entry.steps.forEach(entry => { + // these will get picked up by the next step... + var time = isGroup ? startingTime : currentTime; + if (entry instanceof CompileAnimationStyleMetadata) { + entry.styles.forEach(stylesEntry => { + // by this point we know that we only have stringmap values + var map = <{[key: string]: string|number}>stylesEntry; + StringMapWrapper.forEach(map, (value, prop) => { + collectedStyles.insertAtTime(prop, time, value); + }); + }); + previousStyles = entry.styles; + return; + } + + var innerAst = _parseTransitionAnimation(entry, time, collectedStyles, stateStyles, errors); + if (isPresent(previousStyles)) { + if (entry instanceof CompileAnimationWithStepsMetadata) { + let startingStyles = new AnimationStylesAst(previousStyles); + steps.push(new AnimationStepAst(startingStyles, [], 0, 0, '')); + } else { + var innerStep = innerAst; + ListWrapper.addAll(innerStep.startingStyles.styles, previousStyles); + } + previousStyles = null; + } + + var astDuration = innerAst.playTime; + currentTime += astDuration; + playTime += astDuration; + maxDuration = Math.max(astDuration, maxDuration); + steps.push(innerAst); + }); + if (isPresent(previousStyles)) { + let startingStyles = new AnimationStylesAst(previousStyles); + steps.push(new AnimationStepAst(startingStyles, [], 0, 0, '')); + } + if (isGroup) { + ast = new AnimationGroupAst(steps); + playTime = maxDuration; + currentTime = startingTime + playTime; + } else { + ast = new AnimationSequenceAst(steps); + } + } else if (entry instanceof CompileAnimationAnimateMetadata) { + var timings = _parseTimeExpression(entry.timings, errors); + var styles = entry.styles; + + var keyframes; + if (styles instanceof CompileAnimationKeyframesSequenceMetadata) { + keyframes = _parseAnimationKeyframes(styles, currentTime, collectedStyles, stateStyles, errors); + } else { + let styleData = styles; + let offset = _TERMINAL_KEYFRAME; + let styleAst = new AnimationStylesAst(<{[key: string]: string|number}[]>styleData.styles); + var keyframe = new AnimationKeyframeAst(offset, styleAst); + keyframes = [keyframe]; + } + + ast = new AnimationStepAst(new AnimationStylesAst([]), keyframes, timings.duration, timings.delay, timings.easing); + playTime = timings.duration + timings.delay; + currentTime += playTime; + + keyframes.forEach(keyframe => + keyframe.styles.styles.forEach(entry => + StringMapWrapper.forEach(entry, (value, prop) => + collectedStyles.insertAtTime(prop, currentTime, value)) + ) + ); + } else { + // if the code reaches this stage then an error + // has already been populated within the _normalizeStyleSteps() + // operation... + ast = new AnimationStepAst(null, [], 0, 0, ''); + } + + ast.playTime = playTime; + ast.startTime = startingTime; + return ast; +} + +function _fillAnimationAstStartingKeyframes(ast: AnimationAst, collectedStyles: StylesCollection, + errors: AnimationParseError[]): void { + // steps that only contain style will not be filled + if ((ast instanceof AnimationStepAst) && ast.keyframes.length > 0) { + var keyframes = ast.keyframes; + if (keyframes.length == 1) { + var endKeyframe = keyframes[0]; + var startKeyframe = _createStartKeyframeFromEndKeyframe(endKeyframe, ast.startTime, + ast.playTime, collectedStyles, errors); + ast.keyframes = [startKeyframe, endKeyframe]; + } + } else if (ast instanceof AnimationWithStepsAst) { + ast.steps.forEach(entry => _fillAnimationAstStartingKeyframes(entry, collectedStyles, errors)); + } +} + +function _parseTimeExpression(exp: string | number, + errors: AnimationParseError[]): _AnimationTimings { + var regex = /^([\.\d]+)(m?s)(?:\s+([\.\d]+)(m?s))?(?:\s+([-a-z]+(?:\(.+?\))?))?/gi; + var duration: number; + var delay: number = 0; + var easing: string = null; + if (isString(exp)) { + var matches = RegExpWrapper.firstMatch(regex, exp); + if (!isPresent(matches)) { + errors.push(new AnimationParseError(`The provided timing value "${exp}" is invalid.`)); + return new _AnimationTimings(0, 0, null); + } + + var durationMatch = NumberWrapper.parseFloat(matches[1]); + var durationUnit = matches[2]; + if (durationUnit == 's') { + durationMatch *= _ONE_SECOND; + } + duration = Math.floor(durationMatch); + + var delayMatch = matches[3]; + var delayUnit = matches[4]; + if (isPresent(delayMatch)) { + var delayVal: number = NumberWrapper.parseFloat(delayMatch); + if (isPresent(delayUnit) && delayUnit == 's') { + delayVal *= _ONE_SECOND; + } + delay = Math.floor(delayVal); + } + + var easingVal = matches[5]; + if (!isBlank(easingVal)) { + easing = easingVal; + } + } else { + duration = exp; + } + + return new _AnimationTimings(duration, delay, easing); +} + +function _createStartKeyframeFromEndKeyframe(endKeyframe: AnimationKeyframeAst, startTime: number, + duration: number, collectedStyles: StylesCollection, + errors: AnimationParseError[]): AnimationKeyframeAst { + var values: {[key: string]: string | number} = {}; + var endTime = startTime + duration; + endKeyframe.styles.styles.forEach((styleData: {[key: string]: string|number}) => { + StringMapWrapper.forEach(styleData, (val, prop) => { + if (prop == 'offset') return; + + var resultIndex = collectedStyles.indexOfAtOrBeforeTime(prop, startTime); + var resultEntry, nextEntry, value; + if (isPresent(resultIndex)) { + resultEntry = collectedStyles.getByIndex(prop, resultIndex); + value = resultEntry.value; + nextEntry = collectedStyles.getByIndex(prop, resultIndex + 1); + } else { + // this is a flag that the runtime code uses to pass + // in a value either from the state declaration styles + // or using the AUTO_STYLE value (e.g. getComputedStyle) + value = FILL_STYLE_FLAG; + } + + if (isPresent(nextEntry) && !nextEntry.matches(endTime, val)) { + errors.push(new AnimationParseError( + `The animated CSS property "${prop}" unexpectedly changes between steps "${resultEntry.time}ms" and "${endTime}ms" at "${nextEntry.time}ms"`)); + } + + values[prop] = value; + }); + }); + + return new AnimationKeyframeAst(_INITIAL_KEYFRAME, new AnimationStylesAst([values])); +} diff --git a/modules/@angular/compiler/src/animation/styles_collection.ts b/modules/@angular/compiler/src/animation/styles_collection.ts new file mode 100644 index 0000000000..0ac6c83a96 --- /dev/null +++ b/modules/@angular/compiler/src/animation/styles_collection.ts @@ -0,0 +1,52 @@ +import {isPresent} from '../facade/lang'; +import {ListWrapper} from '../facade/collection'; + +export class StylesCollectionEntry { + constructor(public time: number, public value: string | number) {} + + matches(time: number, value: string | number): boolean { + return time == this.time && value == this.value; + } +} + +export class StylesCollection { + styles: {[key: string]: StylesCollectionEntry[]} = {}; + + insertAtTime(property: string, time: number, value: string | number) { + var tuple = new StylesCollectionEntry(time, value); + var entries = this.styles[property]; + if (!isPresent(entries)) { + entries = this.styles[property] = []; + } + + // insert this at the right stop in the array + // this way we can keep it sorted + var insertionIndex = 0; + for (var i = entries.length - 1; i >= 0; i--) { + if (entries[i].time <= time) { + insertionIndex = i + 1; + break; + } + } + + ListWrapper.insert(entries, insertionIndex, tuple); + } + + getByIndex(property: string, index: number): StylesCollectionEntry { + var items = this.styles[property]; + if (isPresent(items)) { + return index >= items.length ? null : items[index]; + } + return null; + } + + indexOfAtOrBeforeTime(property: string, time: number): number { + var entries = this.styles[property]; + if (isPresent(entries)) { + for (var i = entries.length - 1; i >= 0; i--) { + if (entries[i].time <= time) return i; + } + } + return null; + } +} diff --git a/modules/@angular/compiler/src/compile_metadata.ts b/modules/@angular/compiler/src/compile_metadata.ts index af1246d8de..4f961741b3 100644 --- a/modules/@angular/compiler/src/compile_metadata.ts +++ b/modules/@angular/compiler/src/compile_metadata.ts @@ -17,17 +17,16 @@ import { Type, isString, RegExpWrapper, + StringWrapper, + NumberWrapper, isArray } from '../src/facade/lang'; import {unimplemented, BaseException} from '../src/facade/exceptions'; -import { - StringMapWrapper, -} from '../src/facade/collection'; +import {StringMapWrapper, ListWrapper} from '../src/facade/collection'; import {CssSelector} from './selector'; import {splitAtColon, sanitizeIdentifier} from './util'; import {getUrlScheme} from './url_resolver'; -// group 1: "property" from "[property]" // group 2: "event" from "(event)" var HOST_REG_EXP = /^(?:(?:\[([^\]]+)\])|(?:\(([^\)]+)\)))$/g; @@ -49,6 +48,174 @@ export function metadataFromJson(data: {[key: string]: any}): any { return _COMPILE_METADATA_FROM_JSON[data['class']](data); } +export class CompileAnimationEntryMetadata { + static fromJson(data: {[key: string]: any}): CompileAnimationEntryMetadata { + var value = data['value']; + var defs = _arrayFromJson(value['definitions'], metadataFromJson); + return new CompileAnimationEntryMetadata(value['name'], defs); + } + + constructor(public name: string = null, public definitions: CompileAnimationStateMetadata[] = null) {} + + toJson(): {[key: string]: any} { + return { + 'class': 'AnimationEntryMetadata', + 'value': { + 'name' : this.name, + 'definitions': _arrayToJson(this.definitions) + } + }; + } +} + +export abstract class CompileAnimationStateMetadata {} + +export class CompileAnimationStateDeclarationMetadata extends CompileAnimationStateMetadata { + static fromJson(data: {[key: string]: any}): CompileAnimationStateDeclarationMetadata { + var value = data['value']; + var styles = _objFromJson(value['styles'], metadataFromJson); + return new CompileAnimationStateDeclarationMetadata(value['stateNameExpr'], styles); + } + + constructor(public stateNameExpr: string, public styles: CompileAnimationStyleMetadata) { super(); } + + toJson(): {[key: string]: any} { + return { + 'class': 'AnimationStateDeclarationMetadata', + 'value': { + 'stateNameExpr': this.stateNameExpr, + 'styles': this.styles.toJson() + } + }; + } +} + +export class CompileAnimationStateTransitionMetadata extends CompileAnimationStateMetadata { + static fromJson(data: {[key: string]: any}): CompileAnimationStateTransitionMetadata { + var value = data['value']; + var animation = _objFromJson(value['animation'], metadataFromJson); + return new CompileAnimationStateTransitionMetadata(value['stateChangeExpr'], animation); + } + + constructor(public stateChangeExpr: string, public animation: CompileAnimationMetadata) { super(); } + + toJson(): {[key: string]: any} { + return { + 'class': 'AnimationStateTransitionMetadata', + 'value': { + 'stateChangeExpr': this.stateChangeExpr, + 'animation': this.animation.toJson() + } + }; + } +} + +export abstract class CompileAnimationMetadata { + abstract toJson(): {[key: string]: any}; +} + +export class CompileAnimationKeyframesSequenceMetadata extends CompileAnimationMetadata { + static fromJson(data: {[key: string]: any}): CompileAnimationKeyframesSequenceMetadata { + var steps = _arrayFromJson(data['value'], metadataFromJson); + return new CompileAnimationKeyframesSequenceMetadata(steps); + } + + constructor(public steps: CompileAnimationStyleMetadata[] = []) { + super(); + } + + toJson(): {[key: string]: any} { + return { + 'class': 'AnimationKeyframesSequenceMetadata', + 'value': _arrayToJson(this.steps) + }; + } +} + +export class CompileAnimationStyleMetadata extends CompileAnimationMetadata { + static fromJson(data: {[key: string]: any}): CompileAnimationStyleMetadata { + var value = data['value']; + var offsetVal = value['offset']; + var offset = isPresent(offsetVal) ? NumberWrapper.parseFloat(offsetVal) : null; + var styles = >value['styles']; + return new CompileAnimationStyleMetadata(offset, styles); + } + + constructor(public offset: number, public styles: Array = null) { super(); } + + toJson(): {[key: string]: any} { + return { + 'class': 'AnimationStyleMetadata', + 'value': { + 'offset': this.offset, + 'styles': this.styles + } + }; + } +} + +export class CompileAnimationAnimateMetadata extends CompileAnimationMetadata { + static fromJson(data: {[key: string]: any}): CompileAnimationAnimateMetadata { + var value = data['value']; + var timings = value['timings']; + var styles = _objFromJson(value['styles'], metadataFromJson); + return new CompileAnimationAnimateMetadata(timings, styles); + } + + constructor(public timings: string|number = 0, + public styles: CompileAnimationStyleMetadata|CompileAnimationKeyframesSequenceMetadata = null) { super(); } + + toJson(): {[key: string]: any} { + return { + 'class': 'AnimationAnimateMetadata', + 'value': { + 'timings': this.timings, + 'styles': _objToJson(this.styles) + } + }; + } +} + +export abstract class CompileAnimationWithStepsMetadata extends CompileAnimationMetadata { + constructor(public steps: CompileAnimationMetadata[] = null) { super(); } +} + +export class CompileAnimationSequenceMetadata extends CompileAnimationWithStepsMetadata { + static fromJson(data: {[key: string]: any}): CompileAnimationSequenceMetadata { + var steps = _arrayFromJson(data['value'], metadataFromJson); + return new CompileAnimationSequenceMetadata(steps); + } + + constructor(steps: CompileAnimationMetadata[] = null) { + super(steps); + } + + toJson(): {[key: string]: any} { + return { + 'class': 'AnimationSequenceMetadata', + 'value': _arrayToJson(this.steps) + }; + } +} + +export class CompileAnimationGroupMetadata extends CompileAnimationWithStepsMetadata { + static fromJson(data: {[key: string]: any}): CompileAnimationGroupMetadata { + var steps = _arrayFromJson(data["value"], metadataFromJson); + return new CompileAnimationGroupMetadata(steps); + } + + constructor(steps: CompileAnimationMetadata[] = null) { + super(steps); + } + + toJson(): {[key: string]: any} { + return { + 'class': 'AnimationGroupMetadata', + 'value': _arrayToJson(this.steps) + }; + } +} + export class CompileIdentifierMetadata implements CompileMetadataWithIdentifier { runtime: any; name: string; @@ -477,24 +644,28 @@ export class CompileTemplateMetadata { templateUrl: string; styles: string[]; styleUrls: string[]; + animations: CompileAnimationEntryMetadata[]; ngContentSelectors: string[]; - constructor({encapsulation, template, templateUrl, styles, styleUrls, ngContentSelectors}: { + constructor({encapsulation, template, templateUrl, styles, styleUrls, animations, ngContentSelectors}: { encapsulation?: ViewEncapsulation, template?: string, templateUrl?: string, styles?: string[], styleUrls?: string[], - ngContentSelectors?: string[] + ngContentSelectors?: string[], + animations?: CompileAnimationEntryMetadata[] } = {}) { this.encapsulation = isPresent(encapsulation) ? encapsulation : ViewEncapsulation.Emulated; this.template = template; this.templateUrl = templateUrl; this.styles = isPresent(styles) ? styles : []; this.styleUrls = isPresent(styleUrls) ? styleUrls : []; + this.animations = isPresent(animations) ? ListWrapper.flatten(animations) : []; this.ngContentSelectors = isPresent(ngContentSelectors) ? ngContentSelectors : []; } static fromJson(data: {[key: string]: any}): CompileTemplateMetadata { + var animations = _arrayFromJson(data['animations'], metadataFromJson); return new CompileTemplateMetadata({ encapsulation: isPresent(data['encapsulation']) ? VIEW_ENCAPSULATION_VALUES[data['encapsulation']] : @@ -503,6 +674,7 @@ export class CompileTemplateMetadata { templateUrl: data['templateUrl'], styles: data['styles'], styleUrls: data['styleUrls'], + animations: animations, ngContentSelectors: data['ngContentSelectors'] }); } @@ -515,6 +687,7 @@ export class CompileTemplateMetadata { 'templateUrl': this.templateUrl, 'styles': this.styles, 'styleUrls': this.styleUrls, + 'animations': _objToJson(this.animations), 'ngContentSelectors': this.ngContentSelectors }; } @@ -718,7 +891,7 @@ export function createHostComponentMeta(componentType: CompileTypeMetadata, isHost: true }), template: new CompileTemplateMetadata( - {template: template, templateUrl: '', styles: [], styleUrls: [], ngContentSelectors: []}), + {template: template, templateUrl: '', styles: [], styleUrls: [], ngContentSelectors: [], animations:[]}), changeDetection: ChangeDetectionStrategy.Default, inputs: [], outputs: [], @@ -777,7 +950,15 @@ var _COMPILE_METADATA_FROM_JSON = { 'Type': CompileTypeMetadata.fromJson, 'Provider': CompileProviderMetadata.fromJson, 'Identifier': CompileIdentifierMetadata.fromJson, - 'Factory': CompileFactoryMetadata.fromJson + 'Factory': CompileFactoryMetadata.fromJson, + 'AnimationEntryMetadata': CompileAnimationEntryMetadata.fromJson, + 'AnimationStateDeclarationMetadata': CompileAnimationStateDeclarationMetadata.fromJson, + 'AnimationStateTransitionMetadata': CompileAnimationStateTransitionMetadata.fromJson, + 'AnimationSequenceMetadata': CompileAnimationSequenceMetadata.fromJson, + 'AnimationGroupMetadata': CompileAnimationGroupMetadata.fromJson, + 'AnimationAnimateMetadata': CompileAnimationAnimateMetadata.fromJson, + 'AnimationStyleMetadata': CompileAnimationStyleMetadata.fromJson, + 'AnimationKeyframesSequenceMetadata': CompileAnimationKeyframesSequenceMetadata.fromJson }; function _arrayFromJson(obj: any[], fn: (a: {[key: string]: any}) => any): any { diff --git a/modules/@angular/compiler/src/directive_normalizer.ts b/modules/@angular/compiler/src/directive_normalizer.ts index d6d1981b3d..4100dd77bc 100644 --- a/modules/@angular/compiler/src/directive_normalizer.ts +++ b/modules/@angular/compiler/src/directive_normalizer.ts @@ -109,7 +109,8 @@ export class DirectiveNormalizer { templateUrl: templateAbsUrl, styles: allResolvedStyles, styleUrls: allStyleAbsUrls, - ngContentSelectors: visitor.ngContentSelectors + ngContentSelectors: visitor.ngContentSelectors, + animations: templateMeta.animations }); } } diff --git a/modules/@angular/compiler/src/identifiers.ts b/modules/@angular/compiler/src/identifiers.ts index fd9c71e8eb..bfee5374d0 100644 --- a/modules/@angular/compiler/src/identifiers.ts +++ b/modules/@angular/compiler/src/identifiers.ts @@ -41,7 +41,13 @@ import { pureProxy7, pureProxy8, pureProxy9, - pureProxy10 + pureProxy10, + AnimationKeyframe as AnimationKeyframe_, + AnimationStyles as AnimationStyles_, + NoOpAnimationPlayer as NoOpAnimationPlayer_, + AnimationGroupPlayer as AnimationGroupPlayer_, + AnimationSequencePlayer as AnimationSequencePlayer_, + AnimationStyleUtil } from '../core_private'; import {CompileIdentifierMetadata, CompileTokenMetadata} from './compile_metadata'; @@ -83,6 +89,13 @@ var impCheckBinding = checkBinding; var impCastByValue = castByValue; var impEMPTY_ARRAY = EMPTY_ARRAY; var impEMPTY_MAP = EMPTY_MAP; +var impAnimationGroupPlayer = AnimationGroupPlayer_; +var impAnimationSequencePlayer = AnimationSequencePlayer_; +var impAnimationKeyframe = AnimationKeyframe_; +var impAnimationStyles = AnimationStyles_; +var impNoOpAnimationPlayer = NoOpAnimationPlayer_; + +var ANIMATION_STYLE_UTIL_ASSET_URL = assetUrl('core','animation/animation_style_util'); export class Identifiers { static ViewUtils = new CompileIdentifierMetadata( @@ -205,6 +218,51 @@ export class Identifiers { moduleUrl: assetUrl('core', 'security'), runtime: SecurityContext, }); + static AnimationKeyframe = new CompileIdentifierMetadata({ + name: 'AnimationKeyframe', + moduleUrl: assetUrl('core','animation/animation_keyframe'), + runtime: impAnimationKeyframe + }); + static AnimationStyles = new CompileIdentifierMetadata({ + name: 'AnimationStyles', + moduleUrl: assetUrl('core','animation/animation_styles'), + runtime: impAnimationStyles + }); + static NoOpAnimationPlayer = new CompileIdentifierMetadata({ + name: 'NoOpAnimationPlayer', + moduleUrl: assetUrl('core','animation/animation_player'), + runtime: impNoOpAnimationPlayer + }); + static AnimationGroupPlayer = new CompileIdentifierMetadata({ + name: 'AnimationGroupPlayer', + moduleUrl: assetUrl('core','animation/animation_group_player'), + runtime: impAnimationGroupPlayer + }); + static AnimationSequencePlayer = new CompileIdentifierMetadata({ + name: 'AnimationSequencePlayer', + moduleUrl: assetUrl('core','animation/animation_sequence_player'), + runtime: impAnimationSequencePlayer + }); + static balanceAnimationStyles = new CompileIdentifierMetadata({ + name: 'balanceAnimationStyles', + moduleUrl: ANIMATION_STYLE_UTIL_ASSET_URL, + runtime: AnimationStyleUtil.balanceStyles + }); + static balanceAnimationKeyframes = new CompileIdentifierMetadata({ + name: 'balanceAnimationKeyframes', + moduleUrl: ANIMATION_STYLE_UTIL_ASSET_URL, + runtime: AnimationStyleUtil.balanceKeyframes + }); + static clearAnimationStyles = new CompileIdentifierMetadata({ + name: 'clearAnimationStyles', + moduleUrl: ANIMATION_STYLE_UTIL_ASSET_URL, + runtime: AnimationStyleUtil.clearStyles + }); + static collectAndResolveStyles = new CompileIdentifierMetadata({ + name: 'collectAndResolveStyles', + moduleUrl: ANIMATION_STYLE_UTIL_ASSET_URL, + runtime: AnimationStyleUtil.collectAndResolveStyles + }); } export function identifierToken(identifier: CompileIdentifierMetadata): CompileTokenMetadata { diff --git a/modules/@angular/compiler/src/metadata_resolver.ts b/modules/@angular/compiler/src/metadata_resolver.ts index b3e73317a8..aa4de38fb2 100644 --- a/modules/@angular/compiler/src/metadata_resolver.ts +++ b/modules/@angular/compiler/src/metadata_resolver.ts @@ -33,6 +33,7 @@ import { import {StringMapWrapper} from '../src/facade/collection'; import {BaseException} from '../src/facade/exceptions'; import * as cpl from './compile_metadata'; +import * as anmd from '@angular/core'; import {DirectiveResolver} from './directive_resolver'; import {PipeResolver} from './pipe_resolver'; import {ViewResolver} from './view_resolver'; @@ -77,6 +78,44 @@ export class CompileMetadataResolver { return sanitizeIdentifier(identifier); } + getAnimationEntryMetadata(entry: anmd.AnimationEntryMetadata): cpl.CompileAnimationEntryMetadata { + var defs = entry.definitions.map(def => this.getAnimationStateMetadata(def)); + return new cpl.CompileAnimationEntryMetadata(entry.name, defs); + } + + getAnimationStateMetadata(value: anmd.AnimationStateMetadata): cpl.CompileAnimationStateMetadata { + if (value instanceof anmd.AnimationStateDeclarationMetadata) { + var styles = this.getAnimationStyleMetadata(value.styles); + return new cpl.CompileAnimationStateDeclarationMetadata(value.stateNameExpr, styles); + } else if (value instanceof anmd.AnimationStateTransitionMetadata) { + return new cpl.CompileAnimationStateTransitionMetadata(value.stateChangeExpr, this.getAnimationMetadata(value.animation)); + } + return null; + } + + getAnimationStyleMetadata(value: anmd.AnimationStyleMetadata): cpl.CompileAnimationStyleMetadata { + return new cpl.CompileAnimationStyleMetadata(value.offset, value.styles); + } + + getAnimationMetadata(value: anmd.AnimationMetadata): cpl.CompileAnimationMetadata { + if (value instanceof anmd.AnimationStyleMetadata) { + return this.getAnimationStyleMetadata(value); + } else if (value instanceof anmd.AnimationKeyframesSequenceMetadata) { + return new cpl.CompileAnimationKeyframesSequenceMetadata(value.steps.map(entry => this.getAnimationStyleMetadata(entry))); + } else if (value instanceof anmd.AnimationAnimateMetadata) { + let animateData = this.getAnimationMetadata(value.styles); + return new cpl.CompileAnimationAnimateMetadata(value.timings, animateData); + } else if (value instanceof anmd.AnimationWithStepsMetadata) { + var steps = value.steps.map(step => this.getAnimationMetadata(step)); + if (value instanceof anmd.AnimationGroupMetadata) { + return new cpl.CompileAnimationGroupMetadata(steps); + } else { + return new cpl.CompileAnimationSequenceMetadata(steps); + } + } + return null; + } + getDirectiveMetadata(directiveType: Type): cpl.CompileDirectiveMetadata { var meta = this._directiveCache.get(directiveType); if (isBlank(meta)) { @@ -90,12 +129,17 @@ export class CompileMetadataResolver { var cmpMeta = dirMeta; var viewMeta = this._viewResolver.resolve(directiveType); assertArrayOfStrings('styles', viewMeta.styles); + var animations = isPresent(viewMeta.animations) + ? viewMeta.animations.map(e => this.getAnimationEntryMetadata(e)) + : null; + templateMeta = new cpl.CompileTemplateMetadata({ encapsulation: viewMeta.encapsulation, template: viewMeta.template, templateUrl: viewMeta.templateUrl, styles: viewMeta.styles, - styleUrls: viewMeta.styleUrls + styleUrls: viewMeta.styleUrls, + animations: animations }); changeDetectionStrategy = cmpMeta.changeDetection; if (isPresent(dirMeta.viewProviders)) { diff --git a/modules/@angular/compiler/src/offline_compiler.ts b/modules/@angular/compiler/src/offline_compiler.ts index 234ab986c1..61639cd0ee 100644 --- a/modules/@angular/compiler/src/offline_compiler.ts +++ b/modules/@angular/compiler/src/offline_compiler.ts @@ -168,4 +168,4 @@ function _splitSuffix(path: string): string[] { } else { return [path, '']; } -} \ No newline at end of file +} diff --git a/modules/@angular/compiler/src/output/interpretive_view.ts b/modules/@angular/compiler/src/output/interpretive_view.ts index 4108411826..58b05d3ff0 100644 --- a/modules/@angular/compiler/src/output/interpretive_view.ts +++ b/modules/@angular/compiler/src/output/interpretive_view.ts @@ -41,6 +41,14 @@ class _InterpretiveAppView extends DebugAppView implements DynamicInstance return super.injectorGet(token, nodeIndex, notFoundResult); } } + detachInternal(): void { + var m = this.methods.get('detachInternal'); + if (isPresent(m)) { + return m(); + } else { + return super.detachInternal(); + } + } destroyInternal(): void { var m = this.methods.get('destroyInternal'); if (isPresent(m)) { diff --git a/modules/@angular/compiler/src/runtime_compiler.ts b/modules/@angular/compiler/src/runtime_compiler.ts index 2766302859..8325878b13 100644 --- a/modules/@angular/compiler/src/runtime_compiler.ts +++ b/modules/@angular/compiler/src/runtime_compiler.ts @@ -103,8 +103,7 @@ export class RuntimeCompiler implements ComponentResolver { var childPromises = []; compiledTemplate.init(this._compileComponent(compMeta, parsedTemplate, styles, - pipes, compilingComponentsPath, - childPromises)); + pipes, compilingComponentsPath, childPromises)); return PromiseWrapper.all(childPromises).then((_) => { return compiledTemplate; }); }); this._compiledTemplateDone.set(cacheKey, done); @@ -113,7 +112,8 @@ export class RuntimeCompiler implements ComponentResolver { } private _compileComponent(compMeta: CompileDirectiveMetadata, parsedTemplate: TemplateAst[], - styles: string[], pipes: CompilePipeMetadata[], + styles: string[], + pipes: CompilePipeMetadata[], compilingComponentsPath: any[], childPromises: Promise[]): Function { var compileResult = this._viewCompiler.compileComponent( diff --git a/modules/@angular/compiler/src/template_ast.ts b/modules/@angular/compiler/src/template_ast.ts index 8d0e149c2d..d4e5e20a82 100644 --- a/modules/@angular/compiler/src/template_ast.ts +++ b/modules/@angular/compiler/src/template_ast.ts @@ -213,7 +213,12 @@ export enum PropertyBindingType { /** * A binding to a style rule (e.g. `[style.rule]="expression"`). */ - Style + Style, + + /** + * A binding to an animation reference (e.g. `[animate.key]="expression"`). + */ + Animation } /** diff --git a/modules/@angular/compiler/src/template_parser.ts b/modules/@angular/compiler/src/template_parser.ts index 92663ae8b6..3f395c70c7 100644 --- a/modules/@angular/compiler/src/template_parser.ts +++ b/modules/@angular/compiler/src/template_parser.ts @@ -75,12 +75,13 @@ import {ProviderElementContext, ProviderViewContext} from './provider_parser'; // Group 4 = "ref-/#" // Group 5 = "on-" // Group 6 = "bindon-" -// Group 7 = the identifier after "bind-", "var-/#", or "on-" -// Group 8 = identifier inside [()] -// Group 9 = identifier inside [] -// Group 10 = identifier inside () +// Group 7 = "animate-/@" +// Group 8 = the identifier after "bind-", "var-/#", or "on-" +// Group 9 = identifier inside [()] +// Group 10 = identifier inside [] +// Group 11 = identifier inside () var BIND_NAME_REGEXP = - /^(?:(?:(?:(bind-)|(var-)|(let-)|(ref-|#)|(on-)|(bindon-))(.+))|\[\(([^\)]+)\)\]|\[([^\]]+)\]|\(([^\)]+)\))$/g; + /^(?:(?:(?:(bind-)|(var-)|(let-)|(ref-|#)|(on-)|(bindon-)|(animate-|@))(.+))|\[\(([^\)]+)\)\]|\[([^\]]+)\]|\(([^\)]+)\))$/g; const TEMPLATE_ELEMENT = 'template'; const TEMPLATE_ATTR = 'template'; @@ -303,6 +304,7 @@ class TemplateParseVisitor implements HtmlAstVisitor { var elementOrDirectiveProps: BoundElementOrDirectiveProperty[] = []; var elementOrDirectiveRefs: ElementOrDirectiveRef[] = []; var elementVars: VariableAst[] = []; + var animationProps: BoundElementPropertyAst[] = []; var events: BoundEventAst[] = []; var templateElementOrDirectiveProps: BoundElementOrDirectiveProperty[] = []; @@ -316,7 +318,7 @@ class TemplateParseVisitor implements HtmlAstVisitor { element.attrs.forEach(attr => { var hasBinding = - this._parseAttr(isTemplateElement, attr, matchableAttrs, elementOrDirectiveProps, events, + this._parseAttr(isTemplateElement, attr, matchableAttrs, elementOrDirectiveProps, animationProps, events, elementOrDirectiveRefs, elementVars); var hasTemplateBinding = this._parseInlineTemplateBinding( attr, templateMatchableAttrs, templateElementOrDirectiveProps, templateElementVars); @@ -337,7 +339,7 @@ class TemplateParseVisitor implements HtmlAstVisitor { elementOrDirectiveProps, elementOrDirectiveRefs, element.sourceSpan, references); var elementProps: BoundElementPropertyAst[] = - this._createElementPropertyAsts(element.name, elementOrDirectiveProps, directiveAsts); + this._createElementPropertyAsts(element.name, elementOrDirectiveProps, directiveAsts).concat(animationProps); var isViewRoot = parent.isTemplateElement || hasInlineTemplates; var providerContext = new ProviderElementContext(this.providerViewContext, parent.providerContext, isViewRoot, @@ -439,7 +441,9 @@ class TemplateParseVisitor implements HtmlAstVisitor { private _parseAttr(isTemplateElement: boolean, attr: HtmlAttrAst, targetMatchableAttrs: string[][], - targetProps: BoundElementOrDirectiveProperty[], targetEvents: BoundEventAst[], + targetProps: BoundElementOrDirectiveProperty[], + targetAnimationProps: BoundElementPropertyAst[], + targetEvents: BoundEventAst[], targetRefs: ElementOrDirectiveRef[], targetVars: VariableAst[]): boolean { var attrName = this._normalizeAttributeName(attr.name); var attrValue = attr.value; @@ -448,11 +452,11 @@ class TemplateParseVisitor implements HtmlAstVisitor { if (isPresent(bindParts)) { hasBinding = true; if (isPresent(bindParts[1])) { // match: bind-prop - this._parseProperty(bindParts[7], attrValue, attr.sourceSpan, targetMatchableAttrs, + this._parseProperty(bindParts[8], attrValue, attr.sourceSpan, targetMatchableAttrs, targetProps); } else if (isPresent(bindParts[2])) { // match: var-name / var-name="iden" - var identifier = bindParts[7]; + var identifier = bindParts[8]; if (isTemplateElement) { this._reportError(`"var-" on