From f1b6c6efa1f25b725a8da6a3b91865698a7cafb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Fri, 23 Sep 2016 16:37:04 -0400 Subject: [PATCH] refactor(animations): ensure animation input/outputs are managed within the template parser (#11782) Closes #11782 Closes #11601 Related #11707 --- .../src/animation/animation_compiler.ts | 174 ++---------------- .../src/animation/animation_parser.ts | 104 ++++++----- modules/@angular/compiler/src/identifiers.ts | 7 +- .../@angular/compiler/src/offline_compiler.ts | 14 +- .../compiler/src/private_import_core.ts | 2 - .../compiler/src/provider_analyzer.ts | 16 +- .../@angular/compiler/src/runtime_compiler.ts | 14 +- .../src/template_parser/template_ast.ts | 13 +- .../src/template_parser/template_parser.ts | 119 +++++++++--- modules/@angular/compiler/src/util.ts | 14 +- .../src/view_compiler/compile_view.ts | 4 +- .../src/view_compiler/event_binder.ts | 39 ++-- .../src/view_compiler/property_binder.ts | 2 - .../compiler/src/view_compiler/view_binder.ts | 31 +--- .../src/view_compiler/view_builder.ts | 10 +- .../src/view_compiler/view_compiler.ts | 22 +-- .../test/animation/animation_compiler_spec.ts | 21 +-- .../test/animation/animation_parser_spec.ts | 10 +- .../template_parser/template_parser_spec.ts | 44 +++-- .../core/src/animation/animation_output.ts | 10 - .../@angular/core/src/core_private_export.ts | 3 - modules/@angular/core/src/linker/view.ts | 16 +- .../animation/animation_integration_spec.ts | 35 +++- 23 files changed, 325 insertions(+), 399 deletions(-) delete mode 100644 modules/@angular/core/src/animation/animation_output.ts diff --git a/modules/@angular/compiler/src/animation/animation_compiler.ts b/modules/@angular/compiler/src/animation/animation_compiler.ts index f80ac3cc3e..3399e8e800 100644 --- a/modules/@angular/compiler/src/animation/animation_compiler.ts +++ b/modules/@angular/compiler/src/animation/animation_compiler.ts @@ -6,75 +6,26 @@ * found in the LICENSE file at https://angular.io/license */ -import {CompileDirectiveMetadata} from '../compile_metadata'; import {StringMapWrapper} from '../facade/collection'; import {isBlank, isPresent} from '../facade/lang'; import {Identifiers, resolveIdentifier} from '../identifiers'; import * as o from '../output/output_ast'; -import {ANY_STATE, AnimationOutput, DEFAULT_STATE, EMPTY_STATE} from '../private_import_core'; -import * as t from '../template_parser/template_ast'; +import {ANY_STATE, DEFAULT_STATE, EMPTY_STATE} from '../private_import_core'; -import {AnimationAst, AnimationAstVisitor, AnimationEntryAst, AnimationGroupAst, AnimationKeyframeAst, AnimationSequenceAst, AnimationStateAst, AnimationStateDeclarationAst, AnimationStateTransitionAst, AnimationStepAst, AnimationStylesAst} from './animation_ast'; -import {AnimationParseError, ParsedAnimationResult, parseAnimationEntry, parseAnimationOutputName} from './animation_parser'; +import {AnimationAst, AnimationAstVisitor, AnimationEntryAst, AnimationGroupAst, AnimationKeyframeAst, AnimationSequenceAst, AnimationStateDeclarationAst, AnimationStateTransitionAst, AnimationStepAst, AnimationStylesAst} from './animation_ast'; -const animationCompilationCache = - new Map(); - -export class CompiledAnimationTriggerResult { - constructor( - public name: string, public statesMapStatement: o.Statement, - public statesVariableName: string, public fnStatement: o.Statement, - public fnVariable: o.Expression) {} -} - -export class CompiledComponentAnimationResult { - constructor( - public outputs: AnimationOutput[], public triggers: CompiledAnimationTriggerResult[]) {} +export class AnimationEntryCompileResult { + constructor(public name: string, public statements: o.Statement[], public fnExp: o.Expression) {} } export class AnimationCompiler { - compileComponent(component: CompileDirectiveMetadata, template: t.TemplateAst[]): - CompiledComponentAnimationResult { - var compiledAnimations: CompiledAnimationTriggerResult[] = []; - var groupedErrors: string[] = []; - var triggerLookup: {[key: string]: CompiledAnimationTriggerResult} = {}; - var componentName = component.type.name; - - component.template.animations.forEach(entry => { - var result = parseAnimationEntry(entry); - var triggerName = entry.name; - if (result.errors.length > 0) { - var errorMessage = - `Unable to parse the animation sequence for "${triggerName}" due to the following errors:`; - result.errors.forEach( - (error: AnimationParseError) => { errorMessage += '\n-- ' + error.msg; }); - groupedErrors.push(errorMessage); - } - - if (triggerLookup[triggerName]) { - groupedErrors.push( - `The animation trigger "${triggerName}" has already been registered on "${componentName}"`); - } else { - var factoryName = `${componentName}_${entry.name}`; - var visitor = new _AnimationBuilder(triggerName, factoryName); - var compileResult = visitor.build(result.ast); - compiledAnimations.push(compileResult); - triggerLookup[entry.name] = compileResult; - } + compile(factoryNamePrefix: string, parsedAnimations: AnimationEntryAst[]): + AnimationEntryCompileResult[] { + return parsedAnimations.map(entry => { + const factoryName = `${factoryNamePrefix}_${entry.name}`; + const visitor = new _AnimationBuilder(entry.name, factoryName); + return visitor.build(entry); }); - - var validatedProperties = _validateAnimationProperties(compiledAnimations, template); - validatedProperties.errors.forEach(error => { groupedErrors.push(error.msg); }); - - if (groupedErrors.length > 0) { - var errorMessageStr = - `Animation parsing for ${component.type.name} has failed due to the following errors:`; - groupedErrors.forEach(error => errorMessageStr += `\n- ${error}`); - throw new Error(errorMessageStr); - } - - animationCompilationCache.set(component, compiledAnimations); - return new CompiledComponentAnimationResult(validatedProperties.outputs, compiledAnimations); } } @@ -334,7 +285,7 @@ class _AnimationBuilder implements AnimationAstVisitor { statements); } - build(ast: AnimationAst): CompiledAnimationTriggerResult { + build(ast: AnimationAst): AnimationEntryCompileResult { var context = new _AnimationBuilderContext(); var fnStatement = ast.visit(this, context).toDeclStmt(this._fnVarName); var fnVariable = o.variable(this._fnVarName); @@ -353,9 +304,10 @@ class _AnimationBuilder implements AnimationAstVisitor { lookupMap.push([stateName, variableValue]); }); - var compiledStatesMapExpr = this._statesMapVar.set(o.literalMap(lookupMap)).toDeclStmt(); - return new CompiledAnimationTriggerResult( - this.animationName, compiledStatesMapExpr, this._statesMapVarName, fnStatement, fnVariable); + const compiledStatesMapStmt = this._statesMapVar.set(o.literalMap(lookupMap)).toDeclStmt(); + const statements: o.Statement[] = [compiledStatesMapStmt, fnStatement]; + + return new AnimationEntryCompileResult(this.animationName, statements, fnVariable); } } @@ -405,99 +357,3 @@ function _isEndStateAnimateStep(step: AnimationAst): boolean { function _getStylesArray(obj: any): {[key: string]: any}[] { return obj.styles.styles; } - -function _validateAnimationProperties( - compiledAnimations: CompiledAnimationTriggerResult[], - template: t.TemplateAst[]): AnimationPropertyValidationOutput { - var visitor = new _AnimationTemplatePropertyVisitor(compiledAnimations); - t.templateVisitAll(visitor, template); - return new AnimationPropertyValidationOutput(visitor.outputs, visitor.errors); -} - -export class AnimationPropertyValidationOutput { - constructor(public outputs: AnimationOutput[], public errors: AnimationParseError[]) {} -} - -class _AnimationTemplatePropertyVisitor implements t.TemplateAstVisitor { - private _animationRegistry: {[key: string]: boolean}; - public errors: AnimationParseError[] = []; - public outputs: AnimationOutput[] = []; - - constructor(animations: CompiledAnimationTriggerResult[]) { - this._animationRegistry = this._buildCompileAnimationLookup(animations); - } - - private _buildCompileAnimationLookup(animations: CompiledAnimationTriggerResult[]): - {[key: string]: boolean} { - var map: {[key: string]: boolean} = {}; - animations.forEach(entry => { map[entry.name] = true; }); - return map; - } - - private _validateAnimationInputOutputPairs( - inputAsts: t.BoundElementPropertyAst[], outputAsts: t.BoundEventAst[], - animationRegistry: {[key: string]: any}, isHostLevel: boolean): void { - var detectedAnimationInputs: {[key: string]: boolean} = {}; - inputAsts.forEach(input => { - if (input.type == t.PropertyBindingType.Animation) { - var triggerName = input.name; - if (isPresent(animationRegistry[triggerName])) { - detectedAnimationInputs[triggerName] = true; - } else { - this.errors.push( - new AnimationParseError(`Couldn't find an animation entry for ${triggerName}`)); - } - } - }); - - outputAsts.forEach(output => { - if (output.name[0] == '@') { - var normalizedOutputData = parseAnimationOutputName(output.name.substr(1), this.errors); - let triggerName = normalizedOutputData.name; - let triggerEventPhase = normalizedOutputData.phase; - if (!animationRegistry[triggerName]) { - this.errors.push(new AnimationParseError( - `Couldn't find the corresponding ${isHostLevel ? 'host-level ' : '' }animation trigger definition for (@${triggerName})`)); - } else if (!detectedAnimationInputs[triggerName]) { - this.errors.push(new AnimationParseError( - `Unable to listen on (@${triggerName}.${triggerEventPhase}) because the animation trigger [@${triggerName}] isn't being used on the same element`)); - } else { - this.outputs.push(normalizedOutputData); - } - } - }); - } - - visitElement(ast: t.ElementAst, ctx: any): any { - this._validateAnimationInputOutputPairs( - ast.inputs, ast.outputs, this._animationRegistry, false); - - var componentOnElement: t.DirectiveAst = - ast.directives.find(directive => directive.directive.isComponent); - if (componentOnElement) { - let cachedComponentAnimations = animationCompilationCache.get(componentOnElement.directive); - if (cachedComponentAnimations) { - this._validateAnimationInputOutputPairs( - componentOnElement.hostProperties, componentOnElement.hostEvents, - this._buildCompileAnimationLookup(cachedComponentAnimations), true); - } - } - - t.templateVisitAll(this, ast.children); - } - - visitEmbeddedTemplate(ast: t.EmbeddedTemplateAst, ctx: any): any { - t.templateVisitAll(this, ast.children); - } - - visitEvent(ast: t.BoundEventAst, ctx: any): any {} - visitBoundText(ast: t.BoundTextAst, ctx: any): any {} - visitText(ast: t.TextAst, ctx: any): any {} - visitNgContent(ast: t.NgContentAst, ctx: any): any {} - visitAttr(ast: t.AttrAst, ctx: any): any {} - visitDirective(ast: t.DirectiveAst, ctx: any): any {} - visitReference(ast: t.ReferenceAst, ctx: any): any {} - visitVariable(ast: t.VariableAst, ctx: any): any {} - visitDirectiveProperty(ast: t.BoundDirectivePropertyAst, ctx: any): any {} - visitElementProperty(ast: t.BoundElementPropertyAst, ctx: any): any {} -} diff --git a/modules/@angular/compiler/src/animation/animation_parser.ts b/modules/@angular/compiler/src/animation/animation_parser.ts index 4870b2804d..bf5b7177e3 100644 --- a/modules/@angular/compiler/src/animation/animation_parser.ts +++ b/modules/@angular/compiler/src/animation/animation_parser.ts @@ -6,13 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ -import {CompileAnimationAnimateMetadata, CompileAnimationEntryMetadata, CompileAnimationGroupMetadata, CompileAnimationKeyframesSequenceMetadata, CompileAnimationMetadata, CompileAnimationSequenceMetadata, CompileAnimationStateDeclarationMetadata, CompileAnimationStateTransitionMetadata, CompileAnimationStyleMetadata, CompileAnimationWithStepsMetadata} from '../compile_metadata'; +import {CompileAnimationAnimateMetadata, CompileAnimationEntryMetadata, CompileAnimationGroupMetadata, CompileAnimationKeyframesSequenceMetadata, CompileAnimationMetadata, CompileAnimationSequenceMetadata, CompileAnimationStateDeclarationMetadata, CompileAnimationStateTransitionMetadata, CompileAnimationStyleMetadata, CompileAnimationWithStepsMetadata, CompileDirectiveMetadata} from '../compile_metadata'; import {ListWrapper, StringMapWrapper} from '../facade/collection'; import {isArray, isBlank, isPresent, isString, isStringMap} from '../facade/lang'; import {Math} from '../facade/math'; import {ParseError} from '../parse_util'; +import {ANY_STATE, FILL_STYLE_FLAG} from '../private_import_core'; -import {ANY_STATE, AnimationOutput, FILL_STYLE_FLAG} from '../private_import_core'; import {AnimationAst, AnimationEntryAst, AnimationGroupAst, AnimationKeyframeAst, AnimationSequenceAst, AnimationStateDeclarationAst, AnimationStateTransitionAst, AnimationStateTransitionExpression, AnimationStepAst, AnimationStylesAst, AnimationWithStepsAst} from './animation_ast'; import {StylesCollection} from './styles_collection'; @@ -21,62 +21,70 @@ const _TERMINAL_KEYFRAME = 1; const _ONE_SECOND = 1000; export class AnimationParseError extends ParseError { - constructor(message: any /** TODO #9100 */) { super(null, message); } + constructor(message: string) { super(null, message); } toString(): string { return `${this.msg}`; } } -export class ParsedAnimationResult { +export class AnimationEntryParseResult { constructor(public ast: AnimationEntryAst, public errors: AnimationParseError[]) {} } -export function parseAnimationEntry(entry: CompileAnimationEntryMetadata): ParsedAnimationResult { - var errors: AnimationParseError[] = []; - var stateStyles: {[key: string]: AnimationStylesAst} = {}; - var transitions: CompileAnimationStateTransitionMetadata[] = []; +export class AnimationParser { + parseComponent(component: CompileDirectiveMetadata): AnimationEntryAst[] { + const errors: string[] = []; + const componentName = component.type.name; + const animationTriggerNames = new Set(); + const asts = component.template.animations.map(entry => { + const result = this.parseEntry(entry); + const ast = result.ast; + const triggerName = ast.name; + if (animationTriggerNames.has(triggerName)) { + result.errors.push(new AnimationParseError( + `The animation trigger "${triggerName}" has already been registered for the ${componentName} component`)); + } else { + animationTriggerNames.add(triggerName); + } + if (result.errors.length > 0) { + let errorMessage = + `- Unable to parse the animation sequence for "${triggerName}" on the ${componentName} component due to the following errors:`; + result.errors.forEach( + (error: AnimationParseError) => { errorMessage += '\n-- ' + error.msg; }); + errors.push(errorMessage); + } + return ast; + }); - var stateDeclarationAsts: any[] /** TODO #9100 */ = []; - 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); + if (errors.length > 0) { + const errorString = errors.join('\n'); + throw new Error(`Animation parse errors:\n${errorString}`); } - }); - var stateTransitionAsts = - transitions.map(transDef => _parseAnimationStateTransition(transDef, stateStyles, errors)); - - var ast = new AnimationEntryAst(entry.name, stateDeclarationAsts, stateTransitionAsts); - return new ParsedAnimationResult(ast, errors); -} - -export function parseAnimationOutputName( - outputName: string, errors: AnimationParseError[]): AnimationOutput { - var values = outputName.split('.'); - var name: string; - var phase: string = ''; - if (values.length > 1) { - name = values[0]; - let parsedPhase = values[1]; - switch (parsedPhase) { - case 'start': - case 'done': - phase = parsedPhase; - break; - - default: - errors.push(new AnimationParseError( - `The provided animation output phase value "${parsedPhase}" for "@${name}" is not supported (use start or done)`)); - } - } else { - name = outputName; - errors.push(new AnimationParseError( - `The animation trigger output event (@${name}) is missing its phase value name (start or done are currently supported)`)); + return asts; + } + + parseEntry(entry: CompileAnimationEntryMetadata): AnimationEntryParseResult { + var errors: AnimationParseError[] = []; + var stateStyles: {[key: string]: AnimationStylesAst} = {}; + var transitions: CompileAnimationStateTransitionMetadata[] = []; + + var stateDeclarationAsts: AnimationStateDeclarationAst[] = []; + 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 AnimationEntryParseResult(ast, errors); } - return new AnimationOutput(name, phase, outputName); } function _parseAnimationDeclarationStates( diff --git a/modules/@angular/compiler/src/identifiers.ts b/modules/@angular/compiler/src/identifiers.ts index a81eeb81c9..f1008b45c7 100644 --- a/modules/@angular/compiler/src/identifiers.ts +++ b/modules/@angular/compiler/src/identifiers.ts @@ -9,7 +9,7 @@ import {ANALYZE_FOR_ENTRY_COMPONENTS, ChangeDetectionStrategy, ChangeDetectorRef, ComponentFactory, ComponentFactoryResolver, ElementRef, Injector, LOCALE_ID as LOCALE_ID_, NgModuleFactory, QueryList, RenderComponentType, Renderer, SecurityContext, SimpleChange, TRANSLATIONS_FORMAT as TRANSLATIONS_FORMAT_, TemplateRef, ViewContainerRef, ViewEncapsulation} from '@angular/core'; import {CompileIdentifierMetadata, CompileTokenMetadata} from './compile_metadata'; -import {AnimationGroupPlayer, AnimationKeyframe, AnimationOutput, AnimationSequencePlayer, AnimationStyles, AppElement, AppView, ChangeDetectorStatus, CodegenComponentFactoryResolver, DebugAppView, DebugContext, EMPTY_ARRAY, EMPTY_MAP, NgModuleInjector, NoOpAnimationPlayer, StaticNodeDebugInfo, TemplateRef_, UNINITIALIZED, ValueUnwrapper, ViewType, ViewUtils, balanceAnimationKeyframes, castByValue, checkBinding, clearStyles, collectAndResolveStyles, devModeEqual, flattenNestedViewRenderNodes, interpolate, prepareFinalAnimationStyles, pureProxy1, pureProxy10, pureProxy2, pureProxy3, pureProxy4, pureProxy5, pureProxy6, pureProxy7, pureProxy8, pureProxy9, reflector, registerModuleFactory, renderStyles} from './private_import_core'; +import {AnimationGroupPlayer, AnimationKeyframe, AnimationSequencePlayer, AnimationStyles, AppElement, AppView, ChangeDetectorStatus, CodegenComponentFactoryResolver, DebugAppView, DebugContext, EMPTY_ARRAY, EMPTY_MAP, NgModuleInjector, NoOpAnimationPlayer, StaticNodeDebugInfo, TemplateRef_, UNINITIALIZED, ValueUnwrapper, ViewType, ViewUtils, balanceAnimationKeyframes, castByValue, checkBinding, clearStyles, collectAndResolveStyles, devModeEqual, flattenNestedViewRenderNodes, interpolate, prepareFinalAnimationStyles, pureProxy1, pureProxy10, pureProxy2, pureProxy3, pureProxy4, pureProxy5, pureProxy6, pureProxy7, pureProxy8, pureProxy9, reflector, registerModuleFactory, renderStyles} from './private_import_core'; import {assetUrl} from './util'; var APP_VIEW_MODULE_URL = assetUrl('core', 'linker/view'); @@ -266,11 +266,6 @@ export class Identifiers { moduleUrl: assetUrl('core', 'i18n/tokens'), runtime: TRANSLATIONS_FORMAT_ }; - static AnimationOutput: IdentifierSpec = { - name: 'AnimationOutput', - moduleUrl: assetUrl('core', 'animation/animation_output'), - runtime: AnimationOutput - }; } export function resolveIdentifier(identifier: IdentifierSpec) { diff --git a/modules/@angular/compiler/src/offline_compiler.ts b/modules/@angular/compiler/src/offline_compiler.ts index 90bd832096..65a9bb2856 100644 --- a/modules/@angular/compiler/src/offline_compiler.ts +++ b/modules/@angular/compiler/src/offline_compiler.ts @@ -8,6 +8,8 @@ import {SchemaMetadata} from '@angular/core'; +import {AnimationCompiler} from './animation/animation_compiler'; +import {AnimationParser} from './animation/animation_parser'; import {CompileDirectiveMetadata, CompileIdentifierMetadata, CompileNgModuleMetadata, CompilePipeMetadata, CompileProviderMetadata, CompileTokenMetadata, StaticSymbol, createHostComponentMeta} from './compile_metadata'; import {DirectiveNormalizer} from './directive_normalizer'; import {Identifiers, resolveIdentifier, resolveIdentifierToken} from './identifiers'; @@ -28,6 +30,9 @@ export class NgModulesSummary { } export class OfflineCompiler { + private _animationParser = new AnimationParser(); + private _animationCompiler = new AnimationCompiler(); + constructor( private _metadataResolver: CompileMetadataResolver, private _directiveNormalizer: DirectiveNormalizer, private _templateParser: TemplateParser, @@ -162,14 +167,19 @@ export class OfflineCompiler { compMeta: CompileDirectiveMetadata, directives: CompileDirectiveMetadata[], pipes: CompilePipeMetadata[], schemas: SchemaMetadata[], componentStyles: CompiledStylesheet, fileSuffix: string, targetStatements: o.Statement[]): string { + const parsedAnimations = this._animationParser.parseComponent(compMeta); const parsedTemplate = this._templateParser.parse( compMeta, compMeta.template.template, directives, pipes, schemas, compMeta.type.name); const stylesExpr = componentStyles ? o.variable(componentStyles.stylesVar) : o.literalArr([]); - const viewResult = - this._viewCompiler.compileComponent(compMeta, parsedTemplate, stylesExpr, pipes); + const compiledAnimations = + this._animationCompiler.compile(compMeta.type.name, parsedAnimations); + const viewResult = this._viewCompiler.compileComponent( + compMeta, parsedTemplate, stylesExpr, pipes, compiledAnimations); if (componentStyles) { targetStatements.push(..._resolveStyleStatements(componentStyles, fileSuffix)); } + compiledAnimations.forEach( + entry => { entry.statements.forEach(statement => { targetStatements.push(statement); }); }); targetStatements.push(..._resolveViewStatements(viewResult)); return viewResult.viewFactoryVar; } diff --git a/modules/@angular/compiler/src/private_import_core.ts b/modules/@angular/compiler/src/private_import_core.ts index 5bd4cd4df1..86e66c6730 100644 --- a/modules/@angular/compiler/src/private_import_core.ts +++ b/modules/@angular/compiler/src/private_import_core.ts @@ -75,8 +75,6 @@ export type AnimationKeyframe = typeof r._AnimationKeyframe; export const AnimationKeyframe: typeof r.AnimationKeyframe = r.AnimationKeyframe; export type AnimationStyles = typeof r._AnimationStyles; export const AnimationStyles: typeof r.AnimationStyles = r.AnimationStyles; -export type AnimationOutput = typeof r._AnimationOutput; -export const AnimationOutput: typeof r.AnimationOutput = r.AnimationOutput; export const ANY_STATE = r.ANY_STATE; export const DEFAULT_STATE = r.DEFAULT_STATE; export const EMPTY_STATE = r.EMPTY_STATE; diff --git a/modules/@angular/compiler/src/provider_analyzer.ts b/modules/@angular/compiler/src/provider_analyzer.ts index 52f7c71142..b6160788da 100644 --- a/modules/@angular/compiler/src/provider_analyzer.ts +++ b/modules/@angular/compiler/src/provider_analyzer.ts @@ -50,14 +50,14 @@ export class ProviderElementContext { private _hasViewContainer: boolean = false; constructor( - private _viewContext: ProviderViewContext, private _parent: ProviderElementContext, + public viewContext: ProviderViewContext, private _parent: ProviderElementContext, private _isViewRoot: boolean, private _directiveAsts: DirectiveAst[], attrs: AttrAst[], refs: ReferenceAst[], private _sourceSpan: ParseSourceSpan) { this._attrs = {}; attrs.forEach((attrAst) => this._attrs[attrAst.name] = attrAst.value); var directivesMeta = _directiveAsts.map(directiveAst => directiveAst.directive); this._allProviders = - _resolveProvidersFromDirectives(directivesMeta, _sourceSpan, _viewContext.errors); + _resolveProvidersFromDirectives(directivesMeta, _sourceSpan, viewContext.errors); this._contentQueries = _getContentQueries(directivesMeta); var queriedTokens = new Map(); MapWrapper.values(this._allProviders).forEach((provider) => { @@ -124,7 +124,7 @@ export class ProviderElementContext { } currentEl = currentEl._parent; } - queries = this._viewContext.viewQueries.get(token.reference); + queries = this.viewContext.viewQueries.get(token.reference); if (isPresent(queries)) { ListWrapper.addAll(result, queries); } @@ -150,7 +150,7 @@ export class ProviderElementContext { return transformedProviderAst; } if (isPresent(this._seenProviders.get(token.reference))) { - this._viewContext.errors.push(new ProviderError( + this.viewContext.errors.push(new ProviderError( `Cannot instantiate cyclic dependency! ${token.name}`, this._sourceSpan)); return null; } @@ -254,9 +254,9 @@ export class ProviderElementContext { } // check @Host restriction if (isBlank(result)) { - if (!dep.isHost || this._viewContext.component.type.isHost || - this._viewContext.component.type.reference === dep.token.reference || - isPresent(this._viewContext.viewProviders.get(dep.token.reference))) { + if (!dep.isHost || this.viewContext.component.type.isHost || + this.viewContext.component.type.reference === dep.token.reference || + isPresent(this.viewContext.viewProviders.get(dep.token.reference))) { result = dep; } else { result = dep.isOptional ? @@ -266,7 +266,7 @@ export class ProviderElementContext { } } if (isBlank(result)) { - this._viewContext.errors.push( + this.viewContext.errors.push( new ProviderError(`No provider for ${dep.token.name}`, this._sourceSpan)); } return result; diff --git a/modules/@angular/compiler/src/runtime_compiler.ts b/modules/@angular/compiler/src/runtime_compiler.ts index 768380339e..1ca88871c3 100644 --- a/modules/@angular/compiler/src/runtime_compiler.ts +++ b/modules/@angular/compiler/src/runtime_compiler.ts @@ -7,7 +7,8 @@ */ import {Compiler, ComponentFactory, Injectable, Injector, ModuleWithComponentFactories, NgModuleFactory, Optional, Provider, SchemaMetadata, SkipSelf, Type} from '@angular/core'; - +import {AnimationCompiler} from './animation/animation_compiler'; +import {AnimationParser} from './animation/animation_parser'; import {CompileDirectiveMetadata, CompileIdentifierMetadata, CompileNgModuleMetadata, CompilePipeMetadata, ProviderMeta, createHostComponentMeta} from './compile_metadata'; import {CompilerConfig} from './config'; import {DirectiveNormalizer} from './directive_normalizer'; @@ -23,8 +24,6 @@ import {TemplateParser} from './template_parser/template_parser'; import {SyncAsyncResult} from './util'; import {ComponentFactoryDependency, ViewCompiler, ViewFactoryDependency} from './view_compiler/view_compiler'; - - /** * An internal module of the Angular compiler that begins with component types, * extracts templates, and eventually produces a compiled version of the component @@ -39,6 +38,8 @@ export class RuntimeCompiler implements Compiler { private _compiledTemplateCache = new Map, CompiledTemplate>(); private _compiledHostTemplateCache = new Map, CompiledTemplate>(); private _compiledNgModuleCache = new Map, NgModuleFactory>(); + private _animationParser = new AnimationParser(); + private _animationCompiler = new AnimationCompiler(); constructor( private _injector: Injector, private _metadataResolver: CompileMetadataResolver, @@ -253,12 +254,15 @@ export class RuntimeCompiler implements Compiler { stylesCompileResult.componentStylesheet, externalStylesheetsByModuleUrl); const viewCompMetas = template.viewComponentTypes.map( (compType) => this._assertComponentLoaded(compType, false).normalizedCompMeta); + const parsedAnimations = this._animationParser.parseComponent(compMeta); const parsedTemplate = this._templateParser.parse( compMeta, compMeta.template.template, template.viewDirectives.concat(viewCompMetas), template.viewPipes, template.schemas, compMeta.type.name); + const compiledAnimations = + this._animationCompiler.compile(compMeta.type.name, parsedAnimations); const compileResult = this._viewCompiler.compileComponent( compMeta, parsedTemplate, ir.variable(stylesCompileResult.componentStylesheet.stylesVar), - template.viewPipes); + template.viewPipes, compiledAnimations); compileResult.dependencies.forEach((dep) => { let depTemplate: CompiledTemplate; if (dep instanceof ViewFactoryDependency) { @@ -275,6 +279,8 @@ export class RuntimeCompiler implements Compiler { }); const statements = stylesCompileResult.componentStylesheet.statements.concat(compileResult.statements); + compiledAnimations.forEach( + entry => { entry.statements.forEach(statement => { statements.push(statement); }); }); let factory: any; if (!this._compilerConfig.useJit) { factory = interpretStatements(statements, compileResult.viewFactoryVar); diff --git a/modules/@angular/compiler/src/template_parser/template_ast.ts b/modules/@angular/compiler/src/template_parser/template_ast.ts index 11f57a0e43..d8d916e173 100644 --- a/modules/@angular/compiler/src/template_parser/template_ast.ts +++ b/modules/@angular/compiler/src/template_parser/template_ast.ts @@ -12,11 +12,8 @@ import {CompileDirectiveMetadata, CompileProviderMetadata, CompileTokenMetadata} import {AST} from '../expression_parser/ast'; import {isPresent} from '../facade/lang'; import {ParseSourceSpan} from '../parse_util'; - import {LifecycleHooks} from '../private_import_core'; - - /** * An Abstract Syntax Tree node representing part of a parsed Angular template. */ @@ -61,7 +58,8 @@ export class AttrAst implements TemplateAst { } /** - * A binding for an element property (e.g. `[property]="expression"`). + * A binding for an element property (e.g. `[property]="expression"`) or an animation trigger (e.g. + * `[@trigger]="stateExp"`) */ export class BoundElementPropertyAst implements TemplateAst { constructor( @@ -71,14 +69,16 @@ export class BoundElementPropertyAst implements TemplateAst { visit(visitor: TemplateAstVisitor, context: any): any { return visitor.visitElementProperty(this, context); } + get isAnimation(): boolean { return this.type === PropertyBindingType.Animation; } } /** - * A binding for an element event (e.g. `(event)="handler()"`). + * A binding for an element event (e.g. `(event)="handler()"`) or an animation trigger event (e.g. + * `(@trigger.phase)="callback($event)"`). */ export class BoundEventAst implements TemplateAst { constructor( - public name: string, public target: string, public handler: AST, + public name: string, public target: string, public phase: string, public handler: AST, public sourceSpan: ParseSourceSpan) {} visit(visitor: TemplateAstVisitor, context: any): any { return visitor.visitEvent(this, context); @@ -90,6 +90,7 @@ export class BoundEventAst implements TemplateAst { return this.name; } } + get isAnimation(): boolean { return !!this.phase; } } /** diff --git a/modules/@angular/compiler/src/template_parser/template_parser.ts b/modules/@angular/compiler/src/template_parser/template_parser.ts index 776ba51ae6..62adb99bcd 100644 --- a/modules/@angular/compiler/src/template_parser/template_parser.ts +++ b/modules/@angular/compiler/src/template_parser/template_parser.ts @@ -8,7 +8,7 @@ import {Inject, Injectable, OpaqueToken, Optional, SchemaMetadata, SecurityContext} from '@angular/core'; -import {CompileDirectiveMetadata, CompilePipeMetadata, CompileTokenMetadata, removeIdentifierDuplicates} from '../compile_metadata'; +import {CompileDirectiveMetadata, CompilePipeMetadata, CompileTemplateMetadata, CompileTokenMetadata, removeIdentifierDuplicates} from '../compile_metadata'; import {AST, ASTWithSource, BindingPipe, EmptyExpr, Interpolation, ParserError, RecursiveAstVisitor, TemplateBinding} from '../expression_parser/ast'; import {Parser} from '../expression_parser/parser'; import {StringMapWrapper} from '../facade/collection'; @@ -26,12 +26,13 @@ import {ProviderElementContext, ProviderViewContext} from '../provider_analyzer' import {ElementSchemaRegistry} from '../schema/element_schema_registry'; import {CssSelector, SelectorMatcher} from '../selector'; import {isStyleUrlResolvable} from '../style_url_resolver'; -import {splitAtColon} from '../util'; +import {splitAtColon, splitAtPeriod} from '../util'; import {AttrAst, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, DirectiveAst, ElementAst, EmbeddedTemplateAst, NgContentAst, PropertyBindingType, ReferenceAst, TemplateAst, TemplateAstVisitor, TextAst, VariableAst, templateVisitAll} from './template_ast'; import {PreparsedElementType, preparseElement} from './template_preparser'; + // Group 1 = "bind-" // Group 2 = "let-" // Group 3 = "ref-/#" @@ -142,7 +143,6 @@ export class TemplateParser { const parseVisitor = new TemplateParseVisitor( providerViewContext, uniqDirectives, uniqPipes, schemas, this._exprParser, this._schemaRegistry); - result = html.visitAll(parseVisitor, htmlAstWithErrors.rootNodes, EMPTY_ELEMENT_CONTEXT); errors.push(...parseVisitor.errors, ...providerViewContext.errors); } else { @@ -444,6 +444,15 @@ class TemplateParseVisitor implements html.Visitor { providerContext.transformedDirectiveAsts, providerContext.transformProviders, providerContext.transformedHasViewContainer, children, hasInlineTemplates ? null : ngContentIndex, element.sourceSpan); + + this._findComponentDirectives(directiveAsts) + .forEach( + componentDirectiveAst => this._validateElementAnimationInputOutputs( + componentDirectiveAst.hostProperties, componentDirectiveAst.hostEvents, + componentDirectiveAst.directive.template)); + + const componentTemplate = providerContext.viewContext.component.template; + this._validateElementAnimationInputOutputs(elementProps, events, componentTemplate); } if (hasInlineTemplates) { @@ -469,9 +478,36 @@ class TemplateParseVisitor implements html.Visitor { templateProviderContext.transformedHasViewContainer, [parsedElement], ngContentIndex, element.sourceSpan); } + return parsedElement; } + private _validateElementAnimationInputOutputs( + inputs: BoundElementPropertyAst[], outputs: BoundEventAst[], + template: CompileTemplateMetadata) { + const triggerLookup = new Set(); + template.animations.forEach(entry => { triggerLookup.add(entry.name); }); + + const animationInputs = inputs.filter(input => input.isAnimation); + animationInputs.forEach(input => { + const name = input.name; + if (!triggerLookup.has(name)) { + this._reportError(`Couldn't find an animation entry for "${name}"`, input.sourceSpan); + } + }); + + outputs.forEach(output => { + if (output.isAnimation) { + const found = animationInputs.find(input => input.name == output.name); + if (!found) { + this._reportError( + `Unable to listen on (@${output.name}.${output.phase}) because the animation trigger [@${output.name}] isn't being used on the same element`, + output.sourceSpan); + } + } + }); + } + private _parseInlineTemplateBinding( attr: html.Attribute, targetMatchableAttrs: string[][], targetProps: BoundElementOrDirectiveProperty[], targetVars: VariableAst[]): boolean { @@ -533,7 +569,7 @@ class TemplateParseVisitor implements html.Visitor { this._parseReference(identifier, value, srcSpan, targetRefs); } else if (bindParts[KW_ON_IDX]) { - this._parseEvent( + this._parseEventOrAnimationEvent( bindParts[IDENT_KW_IDX], value, srcSpan, targetMatchableAttrs, targetEvents); } else if (bindParts[KW_BINDON_IDX]) { @@ -544,7 +580,7 @@ class TemplateParseVisitor implements html.Visitor { bindParts[IDENT_KW_IDX], value, srcSpan, targetMatchableAttrs, targetEvents); } else if (bindParts[KW_AT_IDX]) { - if (name[0] == '@' && isPresent(value) && value.length > 0) { + if (_isAnimationLabel(name) && isPresent(value) && value.length > 0) { this._reportError( `Assigning animation triggers via @prop="exp" attributes with an expression is invalid.` + ` Use property bindings (e.g. [@prop]="exp") or use an attribute without a value (e.g. @prop) instead.`, @@ -565,7 +601,7 @@ class TemplateParseVisitor implements html.Visitor { targetAnimationProps); } else if (bindParts[IDENT_EVENT_IDX]) { - this._parseEvent( + this._parseEventOrAnimationEvent( bindParts[IDENT_EVENT_IDX], value, srcSpan, targetMatchableAttrs, targetEvents); } } else { @@ -608,7 +644,7 @@ class TemplateParseVisitor implements html.Visitor { targetMatchableAttrs: string[][], targetProps: BoundElementOrDirectiveProperty[], targetAnimationProps: BoundElementPropertyAst[]) { const animatePropLength = ANIMATE_PROP_PREFIX.length; - var isAnimationProp = name[0] == '@'; + var isAnimationProp = _isAnimationLabel(name); var animationPrefixLength = 1; if (name.substring(0, animatePropLength) == ANIMATE_PROP_PREFIX) { isAnimationProp = true; @@ -635,6 +671,7 @@ class TemplateParseVisitor implements html.Visitor { if (!isPresent(expression) || expression.length == 0) { expression = 'null'; } + const ast = this._parseBinding(expression, sourceSpan); targetMatchableAttrs.push([name, ast.source]); targetAnimationProps.push(new BoundElementPropertyAst( @@ -662,20 +699,56 @@ class TemplateParseVisitor implements html.Visitor { private _parseAssignmentEvent( name: string, expression: string, sourceSpan: ParseSourceSpan, targetMatchableAttrs: string[][], targetEvents: BoundEventAst[]) { - this._parseEvent( + this._parseEventOrAnimationEvent( `${name}Change`, `${expression}=$event`, sourceSpan, targetMatchableAttrs, targetEvents); } + private _parseEventOrAnimationEvent( + name: string, expression: string, sourceSpan: ParseSourceSpan, + targetMatchableAttrs: string[][], targetEvents: BoundEventAst[]) { + if (_isAnimationLabel(name)) { + name = name.substr(1); + this._parseAnimationEvent(name, expression, sourceSpan, targetEvents); + } else { + this._parseEvent(name, expression, sourceSpan, targetMatchableAttrs, targetEvents); + } + } + + private _parseAnimationEvent( + name: string, expression: string, sourceSpan: ParseSourceSpan, + targetEvents: BoundEventAst[]) { + const matches = splitAtPeriod(name, [name, '']); + const eventName = matches[0]; + const phase = matches[1].toLowerCase(); + if (phase) { + switch (phase) { + case 'start': + case 'done': + const ast = this._parseAction(expression, sourceSpan); + targetEvents.push(new BoundEventAst(eventName, null, phase, ast, sourceSpan)); + break; + + default: + this._reportError( + `The provided animation output phase value "${phase}" for "@${eventName}" is not supported (use start or done)`, + sourceSpan); + break; + } + } else { + this._reportError( + `The animation trigger output event (@${eventName}) is missing its phase value name (start or done are currently supported)`, + sourceSpan); + } + } + private _parseEvent( name: string, expression: string, sourceSpan: ParseSourceSpan, targetMatchableAttrs: string[][], targetEvents: BoundEventAst[]) { // long format: 'target: eventName' - const parts = splitAtColon(name, [null, name]); - const target = parts[0]; - const eventName = parts[1]; + const [target, eventName] = splitAtColon(name, [null, name]); const ast = this._parseAction(expression, sourceSpan); targetMatchableAttrs.push([name, ast.source]); - targetEvents.push(new BoundEventAst(eventName, target, ast, sourceSpan)); + targetEvents.push(new BoundEventAst(eventName, target, null, ast, sourceSpan)); // Don't detect directives for event names for now, // so don't add the event name to the matchableAttrs } @@ -779,7 +852,7 @@ class TemplateParseVisitor implements html.Visitor { if (hostListeners) { StringMapWrapper.forEach(hostListeners, (expression: string, propName: string) => { if (isString(expression)) { - this._parseEvent(propName, expression, sourceSpan, [], targetEventAsts); + this._parseEventOrAnimationEvent(propName, expression, sourceSpan, [], targetEventAsts); } else { this._reportError( `Value of the host listener "${propName}" needs to be a string representing an expression but got "${expression}" (${typeof expression})`, @@ -846,7 +919,7 @@ class TemplateParseVisitor implements html.Visitor { if (parts.length === 1) { var partValue = parts[0]; - if (partValue[0] == '@') { + if (_isAnimationLabel(partValue)) { boundPropertyName = partValue.substr(1); bindingType = PropertyBindingType.Animation; securityContext = SecurityContext.NONE; @@ -922,15 +995,13 @@ class TemplateParseVisitor implements html.Visitor { } } + private _findComponentDirectives(directives: DirectiveAst[]): DirectiveAst[] { + return directives.filter(directive => directive.directive.isComponent); + } + private _findComponentDirectiveNames(directives: DirectiveAst[]): string[] { - const componentTypeNames: string[] = []; - directives.forEach(directive => { - const typeName = directive.directive.type.name; - if (directive.directive.isComponent) { - componentTypeNames.push(typeName); - } - }); - return componentTypeNames; + return this._findComponentDirectives(directives) + .map(directive => directive.directive.type.name); } private _assertOnlyOneComponent(directives: DirectiveAst[], sourceSpan: ParseSourceSpan) { @@ -1114,3 +1185,7 @@ export class PipeCollector extends RecursiveAstVisitor { return null; } } + +function _isAnimationLabel(name: string): boolean { + return name[0] == '@'; +} diff --git a/modules/@angular/compiler/src/util.ts b/modules/@angular/compiler/src/util.ts index fd8cf945f3..f69eb0c63c 100644 --- a/modules/@angular/compiler/src/util.ts +++ b/modules/@angular/compiler/src/util.ts @@ -21,9 +21,17 @@ export function camelCaseToDashCase(input: string): string { } export function splitAtColon(input: string, defaultValues: string[]): string[] { - const colonIndex = input.indexOf(':'); - if (colonIndex == -1) return defaultValues; - return [input.slice(0, colonIndex).trim(), input.slice(colonIndex + 1).trim()]; + return _splitAt(input, ':', defaultValues); +} + +export function splitAtPeriod(input: string, defaultValues: string[]): string[] { + return _splitAt(input, '.', defaultValues); +} + +function _splitAt(input: string, character: string, defaultValues: string[]): string[] { + const characterIndex = input.indexOf(character); + if (characterIndex == -1) return defaultValues; + return [input.slice(0, characterIndex).trim(), input.slice(characterIndex + 1).trim()]; } export function sanitizeIdentifier(name: string): string { diff --git a/modules/@angular/compiler/src/view_compiler/compile_view.ts b/modules/@angular/compiler/src/view_compiler/compile_view.ts index 9f78ba78b5..7d429211cc 100644 --- a/modules/@angular/compiler/src/view_compiler/compile_view.ts +++ b/modules/@angular/compiler/src/view_compiler/compile_view.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {CompiledAnimationTriggerResult} from '../animation/animation_compiler'; +import {AnimationEntryCompileResult} from '../animation/animation_compiler'; import {CompileDirectiveMetadata, CompileIdentifierMetadata, CompilePipeMetadata, CompileTokenMetadata} from '../compile_metadata'; import {CompilerConfig} from '../config'; import {ListWrapper, MapWrapper} from '../facade/collection'; @@ -72,7 +72,7 @@ export class CompileView implements NameResolver { constructor( public component: CompileDirectiveMetadata, public genConfig: CompilerConfig, public pipeMetas: CompilePipeMetadata[], public styles: o.Expression, - public animations: CompiledAnimationTriggerResult[], public viewIndex: number, + public animations: AnimationEntryCompileResult[], public viewIndex: number, public declarationElement: CompileElement, public templateVariableBindings: string[][]) { this.createMethod = new CompileMethod(this); this.animationBindingsMethod = new CompileMethod(this); diff --git a/modules/@angular/compiler/src/view_compiler/event_binder.ts b/modules/@angular/compiler/src/view_compiler/event_binder.ts index 947e6241f4..72a8a3de2b 100644 --- a/modules/@angular/compiler/src/view_compiler/event_binder.ts +++ b/modules/@angular/compiler/src/view_compiler/event_binder.ts @@ -11,7 +11,6 @@ import {ListWrapper, StringMapWrapper} from '../facade/collection'; import {StringWrapper, isBlank, isPresent} from '../facade/lang'; import {Identifiers, identifierToken, resolveIdentifier} from '../identifiers'; import * as o from '../output/output_ast'; -import {AnimationOutput} from '../private_import_core'; import {BoundEventAst, DirectiveAst} from '../template_parser/template_ast'; import {CompileBinding} from './compile_binding'; @@ -20,10 +19,6 @@ import {CompileMethod} from './compile_method'; import {EventHandlerVars, ViewProperties} from './constants'; import {convertCdStatementToIr} from './expression_converter'; -export class CompileElementAnimationOutput { - constructor(public listener: CompileEventListener, public output: AnimationOutput) {} -} - export class CompileEventListener { private _method: CompileMethod; private _hasComponentHostListener: boolean = false; @@ -32,13 +27,14 @@ export class CompileEventListener { private _actionResultExprs: o.Expression[] = []; static getOrCreate( - compileElement: CompileElement, eventTarget: string, eventName: string, + compileElement: CompileElement, eventTarget: string, eventName: string, eventPhase: string, targetEventListeners: CompileEventListener[]): CompileEventListener { var listener = targetEventListeners.find( - listener => listener.eventTarget == eventTarget && listener.eventName == eventName); + listener => listener.eventTarget == eventTarget && listener.eventName == eventName && + listener.eventPhase == eventPhase); if (isBlank(listener)) { listener = new CompileEventListener( - compileElement, eventTarget, eventName, targetEventListeners.length); + compileElement, eventTarget, eventName, eventPhase, targetEventListeners.length); targetEventListeners.push(listener); } return listener; @@ -48,7 +44,7 @@ export class CompileEventListener { constructor( public compileElement: CompileElement, public eventTarget: string, public eventName: string, - listenerIndex: number) { + public eventPhase: string, listenerIndex: number) { this._method = new CompileMethod(compileElement.view); this._methodName = `_handle_${santitizeEventName(eventName)}_${compileElement.nodeIndex}_${listenerIndex}`; @@ -119,7 +115,7 @@ export class CompileEventListener { disposable.set(listenExpr).toDeclStmt(o.FUNCTION_TYPE, [o.StmtModifier.Private])); } - listenToAnimation(output: AnimationOutput) { + listenToAnimation() { var outputListener = o.THIS_EXPR.callMethod( 'eventHandler', [o.THIS_EXPR.prop(this._methodName).callMethod(o.BuiltinMethod.Bind, [o.THIS_EXPR])]); @@ -129,11 +125,8 @@ export class CompileEventListener { .callMethod( 'registerAnimationOutput', [ - this.compileElement.renderNode, - o.importExpr(resolveIdentifier(Identifiers.AnimationOutput)).instantiate([ - o.literal(output.name), o.literal(output.phase) - ]), - outputListener + this.compileElement.renderNode, o.literal(this.eventName), + o.literal(this.eventPhase), outputListener ]) .toStmt(); this.compileElement.view.createMethod.addStmt(stmt); @@ -160,7 +153,7 @@ export function collectEventListeners( hostEvents.forEach((hostEvent) => { compileElement.view.bindings.push(new CompileBinding(compileElement, hostEvent)); var listener = CompileEventListener.getOrCreate( - compileElement, hostEvent.target, hostEvent.name, eventListeners); + compileElement, hostEvent.target, hostEvent.name, hostEvent.phase, eventListeners); listener.addAction(hostEvent, null, null); }); dirs.forEach((directiveAst) => { @@ -169,7 +162,7 @@ export function collectEventListeners( directiveAst.hostEvents.forEach((hostEvent) => { compileElement.view.bindings.push(new CompileBinding(compileElement, hostEvent)); var listener = CompileEventListener.getOrCreate( - compileElement, hostEvent.target, hostEvent.name, eventListeners); + compileElement, hostEvent.target, hostEvent.name, hostEvent.phase, eventListeners); listener.addAction(hostEvent, directiveAst.directive, directiveInstance); }); }); @@ -190,11 +183,13 @@ export function bindDirectiveOutputs( } export function bindRenderOutputs(eventListeners: CompileEventListener[]) { - eventListeners.forEach(listener => listener.listenToRenderer()); -} - -export function bindAnimationOutputs(eventListeners: CompileElementAnimationOutput[]) { - eventListeners.forEach(entry => { entry.listener.listenToAnimation(entry.output); }); + eventListeners.forEach(listener => { + if (listener.eventPhase) { + listener.listenToAnimation(); + } else { + listener.listenToRenderer(); + } + }); } function convertStmtIntoExpression(stmt: o.Statement): o.Expression { diff --git a/modules/@angular/compiler/src/view_compiler/property_binder.ts b/modules/@angular/compiler/src/view_compiler/property_binder.ts index 026efece67..19e5caaaa7 100644 --- a/modules/@angular/compiler/src/view_compiler/property_binder.ts +++ b/modules/@angular/compiler/src/view_compiler/property_binder.ts @@ -31,8 +31,6 @@ function createCurrValueExpr(exprIndex: number): o.ReadVarExpr { return o.variable(`currVal_${exprIndex}`); // fix syntax highlighting: ` } -const _animationViewCheckedFlagMap = new Map(); - function bind( view: CompileView, currValExpr: o.ReadVarExpr, fieldExpr: o.ReadPropExpr, parsedExpression: cdAst.AST, context: o.Expression, actions: o.Statement[], diff --git a/modules/@angular/compiler/src/view_compiler/view_binder.ts b/modules/@angular/compiler/src/view_compiler/view_binder.ts index 403e6cfe51..2f98f0f151 100644 --- a/modules/@angular/compiler/src/view_compiler/view_binder.ts +++ b/modules/@angular/compiler/src/view_compiler/view_binder.ts @@ -7,18 +7,16 @@ */ import {ListWrapper} from '../facade/collection'; import {identifierToken} from '../identifiers'; -import {AnimationOutput} from '../private_import_core'; import {AttrAst, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, DirectiveAst, ElementAst, EmbeddedTemplateAst, NgContentAst, ReferenceAst, TemplateAst, TemplateAstVisitor, TextAst, VariableAst, templateVisitAll} from '../template_parser/template_ast'; import {CompileElement, CompileNode} from './compile_element'; import {CompileView} from './compile_view'; -import {CompileElementAnimationOutput, CompileEventListener, bindAnimationOutputs, bindDirectiveOutputs, bindRenderOutputs, collectEventListeners} from './event_binder'; +import {CompileEventListener, bindDirectiveOutputs, bindRenderOutputs, collectEventListeners} from './event_binder'; import {bindDirectiveAfterContentLifecycleCallbacks, bindDirectiveAfterViewLifecycleCallbacks, bindDirectiveDetectChangesLifecycleCallbacks, bindInjectableDestroyLifecycleCallbacks, bindPipeDestroyLifecycleCallbacks} from './lifecycle_binder'; import {bindDirectiveHostProps, bindDirectiveInputs, bindRenderInputs, bindRenderText} from './property_binder'; -export function bindView( - view: CompileView, parsedTemplate: TemplateAst[], animationOutputs: AnimationOutput[]): void { - var visitor = new ViewBinderVisitor(view, animationOutputs); +export function bindView(view: CompileView, parsedTemplate: TemplateAst[]): void { + var visitor = new ViewBinderVisitor(view); templateVisitAll(visitor, parsedTemplate); view.pipes.forEach( (pipe) => { bindPipeDestroyLifecycleCallbacks(pipe.meta, pipe.instance, pipe.view); }); @@ -26,12 +24,8 @@ export function bindView( class ViewBinderVisitor implements TemplateAstVisitor { private _nodeIndex: number = 0; - private _animationOutputsMap: {[key: string]: AnimationOutput} = {}; - constructor(public view: CompileView, public animationOutputs: AnimationOutput[]) { - animationOutputs.forEach( - entry => { this._animationOutputsMap[entry.fullPropertyName] = entry; }); - } + constructor(public view: CompileView) {} visitBoundText(ast: BoundTextAst, parent: CompileElement): any { var node = this.view.nodes[this._nodeIndex++]; @@ -48,22 +42,9 @@ class ViewBinderVisitor implements TemplateAstVisitor { visitElement(ast: ElementAst, parent: CompileElement): any { var compileElement = this.view.nodes[this._nodeIndex++]; var eventListeners: CompileEventListener[] = []; - var animationEventListeners: CompileElementAnimationOutput[] = []; collectEventListeners(ast.outputs, ast.directives, compileElement).forEach(entry => { - // TODO: figure out how to abstract this `if` statement elsewhere - if (entry.eventName[0] == '@') { - let animationOutputName = entry.eventName.substr(1); - let output = this._animationOutputsMap[animationOutputName]; - // no need to report an error here since the parser will - // have caught the missing animation trigger definition - if (output) { - animationEventListeners.push(new CompileElementAnimationOutput(entry, output)); - } - } else { - eventListeners.push(entry); - } + eventListeners.push(entry); }); - bindAnimationOutputs(animationEventListeners); bindRenderInputs(ast.inputs, compileElement); bindRenderOutputs(eventListeners); ast.directives.forEach((directiveAst) => { @@ -108,7 +89,7 @@ class ViewBinderVisitor implements TemplateAstVisitor { var providerInstance = compileElement.instances.get(providerAst.token.reference); bindInjectableDestroyLifecycleCallbacks(providerAst, providerInstance, compileElement); }); - bindView(compileElement.embeddedView, ast.children, this.animationOutputs); + bindView(compileElement.embeddedView, ast.children); return null; } diff --git a/modules/@angular/compiler/src/view_compiler/view_builder.ts b/modules/@angular/compiler/src/view_compiler/view_builder.ts index ace3e903cb..166b160252 100644 --- a/modules/@angular/compiler/src/view_compiler/view_builder.ts +++ b/modules/@angular/compiler/src/view_compiler/view_builder.ts @@ -7,8 +7,6 @@ */ import {ChangeDetectionStrategy, ViewEncapsulation} from '@angular/core'; - -import {AnimationCompiler} from '../animation/animation_compiler'; import {CompileDirectiveMetadata, CompileIdentifierMetadata, CompileTokenMetadata, CompileTypeMetadata} from '../compile_metadata'; import {ListWrapper, SetWrapper, StringMapWrapper} from '../facade/collection'; import {StringWrapper, isPresent} from '../facade/lang'; @@ -65,8 +63,6 @@ export function finishView(view: CompileView, targetStatements: o.Statement[]) { class ViewBuilderVisitor implements TemplateAstVisitor { nestedViewCount: number = 0; - private _animationCompiler = new AnimationCompiler(); - constructor( public view: CompileView, public targetDependencies: Array) {} @@ -279,12 +275,10 @@ class ViewBuilderVisitor implements TemplateAstVisitor { ast.hasViewContainer, true, ast.references); this.view.nodes.push(compileElement); - var compiledAnimations = this._animationCompiler.compileComponent(this.view.component, [ast]); - this.nestedViewCount++; var embeddedView = new CompileView( this.view.component, this.view.genConfig, this.view.pipeMetas, o.NULL_EXPR, - compiledAnimations.triggers, this.view.viewIndex + this.nestedViewCount, compileElement, + this.view.animations, this.view.viewIndex + this.nestedViewCount, compileElement, templateVariableBindings); this.nestedViewCount += buildView(embeddedView, ast.children, this.targetDependencies); @@ -518,7 +512,7 @@ function createViewFactory( templateUrlInfo = view.component.template.templateUrl; } if (view.viewIndex === 0) { - var animationsExpr = o.literalMap(view.animations.map(entry => [entry.name, entry.fnVariable])); + var animationsExpr = o.literalMap(view.animations.map(entry => [entry.name, entry.fnExp])); initRenderCompTypeStmts = [new o.IfStmt(renderCompTypeVar.identical(o.NULL_EXPR), [ renderCompTypeVar .set(ViewConstructorVars.viewUtils.callMethod( diff --git a/modules/@angular/compiler/src/view_compiler/view_compiler.ts b/modules/@angular/compiler/src/view_compiler/view_compiler.ts index 073c92cb41..2a67f7bdbd 100644 --- a/modules/@angular/compiler/src/view_compiler/view_compiler.ts +++ b/modules/@angular/compiler/src/view_compiler/view_compiler.ts @@ -8,7 +8,7 @@ import {Injectable} from '@angular/core'; -import {AnimationCompiler} from '../animation/animation_compiler'; +import {AnimationCompiler, AnimationEntryCompileResult} from '../animation/animation_compiler'; import {CompileDirectiveMetadata, CompilePipeMetadata} from '../compile_metadata'; import {CompilerConfig} from '../config'; import * as o from '../output/output_ast'; @@ -34,22 +34,18 @@ export class ViewCompiler { compileComponent( component: CompileDirectiveMetadata, template: TemplateAst[], styles: o.Expression, - pipes: CompilePipeMetadata[]): ViewCompileResult { - var dependencies: Array = []; - var compiledAnimations = this._animationCompiler.compileComponent(component, template); - var statements: o.Statement[] = []; - var animationTriggers = compiledAnimations.triggers; - animationTriggers.forEach(entry => { - statements.push(entry.statesMapStatement); - statements.push(entry.fnStatement); - }); - var view = new CompileView( - component, this._genConfig, pipes, styles, animationTriggers, 0, + pipes: CompilePipeMetadata[], + compiledAnimations: AnimationEntryCompileResult[]): ViewCompileResult { + const dependencies: Array = []; + const view = new CompileView( + component, this._genConfig, pipes, styles, compiledAnimations, 0, CompileElement.createNull(), []); + + const statements: o.Statement[] = []; buildView(view, template, dependencies); // Need to separate binding from creation to be able to refer to // variables that have been declared after usage. - bindView(view, template, compiledAnimations.outputs); + bindView(view, template); finishView(view, statements); return new ViewCompileResult(statements, view.viewFactory.name, dependencies); diff --git a/modules/@angular/compiler/test/animation/animation_compiler_spec.ts b/modules/@angular/compiler/test/animation/animation_compiler_spec.ts index 094a3da956..bdb6b4f221 100644 --- a/modules/@angular/compiler/test/animation/animation_compiler_spec.ts +++ b/modules/@angular/compiler/test/animation/animation_compiler_spec.ts @@ -10,7 +10,8 @@ import {AnimationMetadata, animate, group, sequence, style, transition, trigger} import {AsyncTestCompleter, beforeEach, beforeEachProviders, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '@angular/core/testing/testing_internal'; import {StringMapWrapper} from '../../../platform-browser-dynamic/src/facade/collection'; -import {AnimationCompiler, CompiledAnimationTriggerResult} from '../../src/animation/animation_compiler'; +import {AnimationCompiler, AnimationEntryCompileResult} from '../../src/animation/animation_compiler'; +import {AnimationParser} from '../../src/animation/animation_parser'; import {CompileAnimationEntryMetadata, CompileDirectiveMetadata, CompileTemplateMetadata, CompileTypeMetadata} from '../../src/compile_metadata'; import {CompileMetadataResolver} from '../../src/metadata_resolver'; @@ -20,12 +21,13 @@ export function main() { beforeEach( inject([CompileMetadataResolver], (res: CompileMetadataResolver) => { resolver = res; })); - var compiler = new AnimationCompiler(); + const parser = new AnimationParser(); + const compiler = new AnimationCompiler(); var compileAnimations = - (component: CompileDirectiveMetadata): CompiledAnimationTriggerResult => { - var result = compiler.compileComponent(component, []); - return result.triggers[0]; + (component: CompileDirectiveMetadata): AnimationEntryCompileResult[] => { + const parsedAnimations = parser.parseComponent(component); + return compiler.compile(component.type.name, parsedAnimations); }; var compileTriggers = (input: any[]) => { @@ -66,14 +68,5 @@ export function main() { expect(capturedErrorMessage) .toMatch(/Animation states via styles must be prefixed with a ":"/); }); - - it('should throw an error when two or more animation triggers contain the same name', () => { - var t1Data: any[] = []; - var t2Data: any[] = []; - - expect(() => { - compileTriggers([['myTrigger', t1Data], ['myTrigger', t2Data]]); - }).toThrowError(/The animation trigger "myTrigger" has already been registered on "myCmp"/); - }); }); } diff --git a/modules/@angular/compiler/test/animation/animation_parser_spec.ts b/modules/@angular/compiler/test/animation/animation_parser_spec.ts index b568871520..1e0fd72455 100644 --- a/modules/@angular/compiler/test/animation/animation_parser_spec.ts +++ b/modules/@angular/compiler/test/animation/animation_parser_spec.ts @@ -11,7 +11,8 @@ import {AsyncTestCompleter, beforeEach, beforeEachProviders, ddescribe, describe import {expect} from '@angular/platform-browser/testing/matchers'; import {AnimationAst, AnimationEntryAst, AnimationGroupAst, AnimationKeyframeAst, AnimationSequenceAst, AnimationStateTransitionAst, AnimationStepAst, AnimationStylesAst} from '../../src/animation/animation_ast'; -import {parseAnimationEntry} from '../../src/animation/animation_parser'; +import {AnimationParser} from '../../src/animation/animation_parser'; +import {CompileDirectiveMetadata, CompileTemplateMetadata, CompileTypeMetadata} from '../../src/compile_metadata'; import {StringMapWrapper} from '../../src/facade/collection'; import {CompileMetadataResolver} from '../../src/metadata_resolver'; import {FILL_STYLE_FLAG, flattenStyles} from '../private_import_core'; @@ -46,9 +47,10 @@ export function main() { inject([CompileMetadataResolver], (res: CompileMetadataResolver) => { resolver = res; })); var parseAnimation = (data: AnimationMetadata[]) => { - var entry = trigger('myAnimation', [transition('state1 => state2', sequence(data))]); - var compiledAnimationEntry = resolver.getAnimationEntryMetadata(entry); - return parseAnimationEntry(compiledAnimationEntry); + const entry = trigger('myAnimation', [transition('state1 => state2', sequence(data))]); + const compiledAnimationEntry = resolver.getAnimationEntryMetadata(entry); + const parser = new AnimationParser(); + return parser.parseEntry(compiledAnimationEntry); }; var getAnimationAstFromEntryAst = diff --git a/modules/@angular/compiler/test/template_parser/template_parser_spec.ts b/modules/@angular/compiler/test/template_parser/template_parser_spec.ts index 00a4842d46..dceedebcd4 100644 --- a/modules/@angular/compiler/test/template_parser/template_parser_spec.ts +++ b/modules/@angular/compiler/test/template_parser/template_parser_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {CompileDiDependencyMetadata, CompileDirectiveMetadata, CompilePipeMetadata, CompileProviderMetadata, CompileQueryMetadata, CompileTemplateMetadata, CompileTokenMetadata, CompileTypeMetadata} from '@angular/compiler/src/compile_metadata'; +import {CompileAnimationEntryMetadata, CompileDiDependencyMetadata, CompileDirectiveMetadata, CompilePipeMetadata, CompileProviderMetadata, CompileQueryMetadata, CompileTemplateMetadata, CompileTokenMetadata, CompileTypeMetadata} from '@angular/compiler/src/compile_metadata'; import {DomElementSchemaRegistry} from '@angular/compiler/src/schema/dom_element_schema_registry'; import {ElementSchemaRegistry} from '@angular/compiler/src/schema/element_schema_registry'; import {AttrAst, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, DirectiveAst, ElementAst, EmbeddedTemplateAst, NgContentAst, PropertyBindingType, ProviderAstType, ReferenceAst, TemplateAst, TemplateAstVisitor, TextAst, VariableAst, templateVisitAll} from '@angular/compiler/src/template_parser/template_ast'; @@ -16,7 +16,6 @@ import {SchemaMetadata, SecurityContext, Type} from '@angular/core'; import {Console} from '@angular/core/src/console'; import {TestBed} from '@angular/core/testing'; import {afterEach, beforeEach, beforeEachProviders, ddescribe, describe, expect, iit, inject, it, xit} from '@angular/core/testing/testing_internal'; - import {Identifiers, identifierToken, resolveIdentifierToken} from '../../src/identifiers'; import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../../src/ml_parser/interpolation_config'; import {MockSchemaRegistry} from '../../testing/index'; @@ -32,9 +31,9 @@ const MOCK_SCHEMA_REGISTRY = [{ export function main() { var ngIf: CompileDirectiveMetadata; - var parse: - (template: string, directives: CompileDirectiveMetadata[], pipes?: CompilePipeMetadata[]) => - TemplateAst[]; + var parse: ( + template: string, directives: CompileDirectiveMetadata[], pipes?: CompilePipeMetadata[], + schemas?: SchemaMetadata[]) => TemplateAst[]; var console: ArrayConsole; function commonBeforeEach() { @@ -43,14 +42,18 @@ export function main() { TestBed.configureCompiler({providers: [{provide: Console, useValue: console}]}); }); beforeEach(inject([TemplateParser], (parser: TemplateParser) => { + var someAnimation = new CompileAnimationEntryMetadata('someAnimation', []); + var someTemplate = new CompileTemplateMetadata({animations: [someAnimation]}); var component = CompileDirectiveMetadata.create({ selector: 'root', + template: someTemplate, type: new CompileTypeMetadata( {moduleUrl: someModuleUrl, name: 'Root', reference: {} as Type}), isComponent: true }); ngIf = CompileDirectiveMetadata.create({ selector: '[ngIf]', + template: someTemplate, type: new CompileTypeMetadata( {moduleUrl: someModuleUrl, name: 'NgIf', reference: {} as Type}), inputs: ['ngIf'] @@ -302,27 +305,31 @@ Can't bind to 'invalidProp' since it isn't a known property of 'my-component'. ]); }); - it('should parse bound properties via bind-animate- and not report them as animation properties', + it('should parse bound properties via bind-animate- and not report them as attributes', () => { - expect(humanizeTplAst(parse('
', []))).toEqual([ - [ElementAst, 'div'], - [ - BoundElementPropertyAst, PropertyBindingType.Animation, 'something', 'value2', null - ] - ]); + expect(humanizeTplAst(parse('
', [], [], []))) + .toEqual([ + [ElementAst, 'div'], + [ + BoundElementPropertyAst, PropertyBindingType.Animation, 'someAnimation', + 'value2', null + ] + ]); }); it('should throw an error when parsing detects non-bound properties via @ that contain a value', () => { - expect(() => { parse('
', []); }) + expect(() => { parse('
', [], [], []); }) .toThrowError( - /Assigning animation triggers via @prop="exp" attributes with an expression is invalid. Use property bindings \(e.g. \[@prop\]="exp"\) or use an attribute without a value \(e.g. @prop\) instead. \("
\]@something="value2">"\): TestComp@0:5/); + /Assigning animation triggers via @prop="exp" attributes with an expression is invalid. Use property bindings \(e.g. \[@prop\]="exp"\) or use an attribute without a value \(e.g. @prop\) instead. \("
\]@someAnimation="value2">"\): TestComp@0:5/); }); it('should not issue a warning when host attributes contain a valid property-bound animation trigger', () => { + const animationEntries = [new CompileAnimationEntryMetadata('prop', [])]; var dirA = CompileDirectiveMetadata.create({ selector: 'div', + template: new CompileTemplateMetadata({animations: animationEntries}), type: new CompileTypeMetadata( {moduleUrl: someModuleUrl, name: 'DirA', reference: {} as Type}), host: {'[@prop]': 'expr'} @@ -360,14 +367,17 @@ Can't bind to 'invalidProp' since it isn't a known property of 'my-component'. it('should not issue a warning when an animation property is bound without an expression', () => { - humanizeTplAst(parse('
', [])); + humanizeTplAst(parse('
', [], [], [])); expect(console.warnings.length).toEqual(0); }); it('should parse bound properties via [@] and not report them as attributes', () => { - expect(humanizeTplAst(parse('
', []))).toEqual([ + expect(humanizeTplAst(parse('
', [], [], []))).toEqual([ [ElementAst, 'div'], - [BoundElementPropertyAst, PropertyBindingType.Animation, 'something', 'value2', null] + [ + BoundElementPropertyAst, PropertyBindingType.Animation, 'someAnimation', 'value2', + null + ] ]); }); }); diff --git a/modules/@angular/core/src/animation/animation_output.ts b/modules/@angular/core/src/animation/animation_output.ts deleted file mode 100644 index bde629b3c9..0000000000 --- a/modules/@angular/core/src/animation/animation_output.ts +++ /dev/null @@ -1,10 +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 class AnimationOutput { - constructor(public name: string, public phase: string, public fullPropertyName: string) {} -} diff --git a/modules/@angular/core/src/core_private_export.ts b/modules/@angular/core/src/core_private_export.ts index 6a356841da..980ec38066 100644 --- a/modules/@angular/core/src/core_private_export.ts +++ b/modules/@angular/core/src/core_private_export.ts @@ -9,7 +9,6 @@ import {ANY_STATE as ANY_STATE_, DEFAULT_STATE as DEFAULT_STATE_, EMPTY_STATE as EMPTY_STATE_, FILL_STYLE_FLAG as FILL_STYLE_FLAG_} from './animation/animation_constants'; import {AnimationGroupPlayer as AnimationGroupPlayer_} from './animation/animation_group_player'; import {AnimationKeyframe as AnimationKeyframe_} from './animation/animation_keyframe'; -import {AnimationOutput as AnimationOutput_} from './animation/animation_output'; import {AnimationPlayer as AnimationPlayer_, NoOpAnimationPlayer as NoOpAnimationPlayer_} from './animation/animation_player'; import {AnimationSequencePlayer as AnimationSequencePlayer_} from './animation/animation_sequence_player'; import * as animationUtils from './animation/animation_style_util'; @@ -115,7 +114,6 @@ export var __core_private__: { renderStyles: typeof animationUtils.renderStyles, collectAndResolveStyles: typeof animationUtils.collectAndResolveStyles, AnimationStyles: typeof AnimationStyles_, _AnimationStyles?: AnimationStyles_, - AnimationOutput: typeof AnimationOutput_, _AnimationOutput?: AnimationOutput_, ANY_STATE: typeof ANY_STATE_, DEFAULT_STATE: typeof DEFAULT_STATE_, EMPTY_STATE: typeof EMPTY_STATE_, @@ -183,7 +181,6 @@ export var __core_private__: { renderStyles: animationUtils.renderStyles, collectAndResolveStyles: animationUtils.collectAndResolveStyles, AnimationStyles: AnimationStyles_, - AnimationOutput: AnimationOutput_, ANY_STATE: ANY_STATE_, DEFAULT_STATE: DEFAULT_STATE_, EMPTY_STATE: EMPTY_STATE_, diff --git a/modules/@angular/core/src/linker/view.ts b/modules/@angular/core/src/linker/view.ts index 198157628e..23cae11b16 100644 --- a/modules/@angular/core/src/linker/view.ts +++ b/modules/@angular/core/src/linker/view.ts @@ -7,7 +7,6 @@ */ import {AnimationGroupPlayer} from '../animation/animation_group_player'; -import {AnimationOutput} from '../animation/animation_output'; import {AnimationPlayer, NoOpAnimationPlayer} from '../animation/animation_player'; import {queueAnimation} from '../animation/animation_queue'; import {AnimationTransitionEvent} from '../animation/animation_transition_event'; @@ -53,7 +52,7 @@ export abstract class AppView { public animationPlayers = new ViewAnimationMap(); - private _animationListeners = new Map(); + private _animationListeners = new Map(); public context: T; @@ -107,7 +106,7 @@ export abstract class AppView { let listener = listeners[i]; // we check for both the name in addition to the phase in the event // that there may be more than one @trigger on the same element - if (listener.output.name == animationName && listener.output.phase == phase) { + if (listener.eventName === animationName && listener.eventPhase === phase) { listener.handler(event); break; } @@ -115,14 +114,13 @@ export abstract class AppView { } } - registerAnimationOutput(element: any, outputEvent: AnimationOutput, eventHandler: Function): - void { - var entry = new _AnimationOutputWithHandler(outputEvent, eventHandler); + registerAnimationOutput( + element: any, eventName: string, eventPhase: string, eventHandler: Function): void { var animations = this._animationListeners.get(element); if (!isPresent(animations)) { this._animationListeners.set(element, animations = []); } - animations.push(entry); + animations.push(new _AnimationOutputHandler(eventName, eventPhase, eventHandler)); } create(context: T, givenProjectableNodes: Array, rootSelectorOrNode: string|any): @@ -469,6 +467,6 @@ function _findLastRenderNode(node: any): any { return lastNode; } -class _AnimationOutputWithHandler { - constructor(public output: AnimationOutput, public handler: Function) {} +class _AnimationOutputHandler { + constructor(public eventName: string, public eventPhase: string, public handler: Function) {} } diff --git a/modules/@angular/core/test/animation/animation_integration_spec.ts b/modules/@angular/core/test/animation/animation_integration_spec.ts index 88a1f56cbe..622c0b3bc5 100644 --- a/modules/@angular/core/test/animation/animation_integration_spec.ts +++ b/modules/@angular/core/test/animation/animation_integration_spec.ts @@ -997,7 +997,7 @@ function declareTests({useJit}: {useJit: boolean}) {
outer
- inner + inner <
<
`, @@ -1234,8 +1234,7 @@ function declareTests({useJit}: {useJit: boolean}) { message = e.message; } - expect(message).toMatch( - /- Couldn't find the corresponding animation trigger definition for \(@something\)/); + expect(message).toMatch(/Couldn't find an animation entry for "something"/); }); it('should throw an error if an animation output is referenced that is not bound to as a property on the same element', @@ -1258,7 +1257,7 @@ function declareTests({useJit}: {useJit: boolean}) { } expect(message).toMatch( - /- Unable to listen on \(@trigger.done\) because the animation trigger \[@trigger\] isn't being used on the same element/); + /Unable to listen on \(@trigger.done\) because the animation trigger \[@trigger\] isn't being used on the same element/); }); it('should throw an error if an unsupported animation output phase name is used', () => { @@ -1287,7 +1286,7 @@ function declareTests({useJit}: {useJit: boolean}) { TestBed.overrideComponent(DummyIfCmp, { set: { template: ` -
+
`, animations: [trigger('trigger', [transition('one => two', [animate(1000)])])] } @@ -1319,7 +1318,7 @@ function declareTests({useJit}: {useJit: boolean}) { } expect(message).toMatch( - /Couldn't find the corresponding host-level animation trigger definition for \(@trigger\)/); + /Unable to listen on \(@trigger.done\) because the animation trigger \[@trigger\] isn't being used on the same element/); }); it('should allow host and element-level animation bindings to be defined on the same tag/component', @@ -1480,11 +1479,27 @@ function declareTests({useJit}: {useJit: boolean}) { failureMessage = e.message; } - expect(failureMessage) - .toMatch(/Animation parsing for DummyIfCmp has failed due to the following errors:/); - expect(failureMessage).toMatch(/- Couldn't find an animation entry for status/); + expect(failureMessage).toMatch(/Template parse errors:/); + expect(failureMessage).toMatch(/Couldn't find an animation entry for "status"/); }); + it('should throw an error if an animation trigger is registered but is already in use', () => { + TestBed.overrideComponent( + DummyIfCmp, {set: {animations: [trigger('matias', []), trigger('matias', [])]}}); + + var failureMessage = ''; + try { + const fixture = TestBed.createComponent(DummyLoadingCmp); + } catch (e) { + failureMessage = e.message; + } + + expect(failureMessage).toMatch(/Animation parse errors:/); + expect(failureMessage) + .toMatch( + /The animation trigger "matias" has already been registered for the DummyIfCmp component/); + }); + it('should be permitted to be registered on the host element', fakeAsync(() => { TestBed.overrideComponent(DummyLoadingCmp, { set: { @@ -1521,7 +1536,7 @@ function declareTests({useJit}: {useJit: boolean}) { failureMessage = e.message; } - expect(failureMessage).toMatch(/- Couldn't find an animation entry for loading/); + expect(failureMessage).toMatch(/Couldn't find an animation entry for "loading"/); }); it('should retain the destination animation state styles once the animation is complete',