From 45e8e73670b96387fc109921fad299742d3f7cbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Mon, 22 Aug 2016 17:18:25 -0700 Subject: [PATCH] refactor(animations): deport TCB away from animation-land forever (#10892) * feat(animations): support animation trigger template callbacks * refactor(animations): deport TCB away from animation-land forever --- modules/@angular/compiler/core_private.ts | 2 + .../src/animation/animation_compiler.ts | 124 +- .../src/animation/animation_parser.ts | 28 +- modules/@angular/compiler/src/identifiers.ts | 9 +- .../src/view_compiler/compile_view.ts | 4 +- .../src/view_compiler/event_binder.ts | 33 +- .../compiler/src/view_compiler/view_binder.ts | 35 +- .../src/view_compiler/view_builder.ts | 2 +- .../src/view_compiler/view_compiler.ts | 7 +- .../test/animation/animation_compiler_spec.ts | 10 +- modules/@angular/core/private_export.ts | 4 + .../core/src/animation/animation_output.ts | 10 + modules/@angular/core/src/linker/view.ts | 53 +- .../animation/animation_integration_spec.ts | 2394 +++++++++-------- .../playground/src/animate/app/animate-app.ts | 8 +- 15 files changed, 1598 insertions(+), 1125 deletions(-) create mode 100644 modules/@angular/core/src/animation/animation_output.ts diff --git a/modules/@angular/compiler/core_private.ts b/modules/@angular/compiler/core_private.ts index 27fd52c3a2..baf44a2b4e 100644 --- a/modules/@angular/compiler/core_private.ts +++ b/modules/@angular/compiler/core_private.ts @@ -78,6 +78,8 @@ export type AnimationKeyframe = t.AnimationKeyframe; export var AnimationKeyframe: typeof t.AnimationKeyframe = r.AnimationKeyframe; export type AnimationStyles = t.AnimationStyles; export var AnimationStyles: typeof t.AnimationStyles = r.AnimationStyles; +export type AnimationOutput = t.AnimationOutput; +export var AnimationOutput: typeof t.AnimationOutput = r.AnimationOutput; export var ANY_STATE = r.ANY_STATE; export var DEFAULT_STATE = r.DEFAULT_STATE; export var EMPTY_STATE = r.EMPTY_STATE; diff --git a/modules/@angular/compiler/src/animation/animation_compiler.ts b/modules/@angular/compiler/src/animation/animation_compiler.ts index 4786a98e3b..d7e6ec6cbc 100644 --- a/modules/@angular/compiler/src/animation/animation_compiler.ts +++ b/modules/@angular/compiler/src/animation/animation_compiler.ts @@ -8,7 +8,7 @@ import {AUTO_STYLE, BaseException} from '@angular/core'; -import {ANY_STATE, DEFAULT_STATE, EMPTY_STATE} from '../../core_private'; +import {ANY_STATE, AnimationOutput, DEFAULT_STATE, EMPTY_STATE} from '../../core_private'; import {CompileDirectiveMetadata} from '../compile_metadata'; import {StringMapWrapper} from '../facade/collection'; import {isBlank, isPresent} from '../facade/lang'; @@ -17,23 +17,29 @@ import * as o from '../output/output_ast'; import * as t from '../template_parser/template_ast'; import {AnimationAst, AnimationAstVisitor, AnimationEntryAst, AnimationGroupAst, AnimationKeyframeAst, AnimationSequenceAst, AnimationStateAst, AnimationStateDeclarationAst, AnimationStateTransitionAst, AnimationStepAst, AnimationStylesAst} from './animation_ast'; -import {AnimationParseError, ParsedAnimationResult, parseAnimationEntry} from './animation_parser'; +import {AnimationParseError, ParsedAnimationResult, parseAnimationEntry, parseAnimationOutputName} from './animation_parser'; -const animationCompilationCache = new Map(); +const animationCompilationCache = + new Map(); -export class CompiledAnimation { +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 AnimationCompiler { compileComponent(component: CompileDirectiveMetadata, template: t.TemplateAst[]): - CompiledAnimation[] { - var compiledAnimations: CompiledAnimation[] = []; + CompiledComponentAnimationResult { + var compiledAnimations: CompiledAnimationTriggerResult[] = []; var groupedErrors: string[] = []; - var triggerLookup: {[key: string]: CompiledAnimation} = {}; + var triggerLookup: {[key: string]: CompiledAnimationTriggerResult} = {}; var componentName = component.type.name; component.template.animations.forEach(entry => { @@ -59,9 +65,8 @@ export class AnimationCompiler { } }); - _validateAnimationProperties(compiledAnimations, template).forEach(entry => { - groupedErrors.push(entry.msg); - }); + var validatedProperties = _validateAnimationProperties(compiledAnimations, template); + validatedProperties.errors.forEach(error => { groupedErrors.push(error.msg); }); if (groupedErrors.length > 0) { var errorMessageStr = @@ -71,7 +76,7 @@ export class AnimationCompiler { } animationCompilationCache.set(component, compiledAnimations); - return compiledAnimations; + return new CompiledComponentAnimationResult(validatedProperties.outputs, compiledAnimations); } } @@ -292,14 +297,15 @@ class _AnimationBuilder implements AnimationAstVisitor { .toStmt()])]) .toStmt()); - statements.push(_ANIMATION_FACTORY_VIEW_VAR - .callMethod( - 'queueAnimation', - [ - _ANIMATION_FACTORY_ELEMENT_VAR, o.literal(this.animationName), - _ANIMATION_PLAYER_VAR - ]) - .toStmt()); + statements.push( + _ANIMATION_FACTORY_VIEW_VAR + .callMethod( + 'queueAnimation', + [ + _ANIMATION_FACTORY_ELEMENT_VAR, o.literal(this.animationName), + _ANIMATION_PLAYER_VAR, _ANIMATION_CURRENT_STATE_VAR, _ANIMATION_NEXT_STATE_VAR + ]) + .toStmt()); return o.fn( [ @@ -313,7 +319,7 @@ class _AnimationBuilder implements AnimationAstVisitor { statements); } - build(ast: AnimationAst): CompiledAnimation { + build(ast: AnimationAst): CompiledAnimationTriggerResult { var context = new _AnimationBuilderContext(); var fnStatement = ast.visit(this, context).toDeclStmt(this._fnVarName); var fnVariable = o.variable(this._fnVarName); @@ -333,7 +339,7 @@ class _AnimationBuilder implements AnimationAstVisitor { }); var compiledStatesMapExpr = this._statesMapVar.set(o.literalMap(lookupMap)).toDeclStmt(); - return new CompiledAnimation( + return new CompiledAnimationTriggerResult( this.animationName, compiledStatesMapExpr, this._statesMapVarName, fnStatement, fnVariable); } } @@ -385,60 +391,92 @@ function _getStylesArray(obj: any): {[key: string]: any}[] { } function _validateAnimationProperties( - compiledAnimations: CompiledAnimation[], template: t.TemplateAst[]): AnimationParseError[] { + compiledAnimations: CompiledAnimationTriggerResult[], + template: t.TemplateAst[]): AnimationPropertyValidationOutput { var visitor = new _AnimationTemplatePropertyVisitor(compiledAnimations); t.templateVisitAll(visitor, template); - return visitor.errors; + 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: CompiledAnimation[]) { + constructor(animations: CompiledAnimationTriggerResult[]) { this._animationRegistry = this._buildCompileAnimationLookup(animations); } - private _buildCompileAnimationLookup(animations: CompiledAnimation[]): {[key: string]: boolean} { + private _buildCompileAnimationLookup(animations: CompiledAnimationTriggerResult[]): + {[key: string]: boolean} { var map: {[key: string]: boolean} = {}; animations.forEach(entry => { map[entry.name] = true; }); return map; } - visitElement(ast: t.ElementAst, ctx: any): any { - var inputAsts: t.BoundElementPropertyAst[] = ast.inputs; - var componentAnimationRegistry = this._animationRegistry; - - var componentOnElement: t.DirectiveAst = - ast.directives.find(directive => directive.directive.isComponent); - if (componentOnElement) { - inputAsts = componentOnElement.hostProperties; - let cachedComponentAnimations = animationCompilationCache.get(componentOnElement.directive); - if (cachedComponentAnimations) { - componentAnimationRegistry = this._buildCompileAnimationLookup(cachedComponentAnimations); - } - } - + 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 animationName = input.name; - if (!isPresent(componentAnimationRegistry[animationName])) { + var triggerName = input.name; + if (isPresent(animationRegistry[triggerName])) { + detectedAnimationInputs[triggerName] = true; + } else { this.errors.push( - new AnimationParseError(`couldn't find an animation entry for ${animationName}`)); + 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); } + visitEvent(ast: t.BoundEventAst, ctx: any): any {} visitBoundText(ast: t.BoundTextAst, ctx: any): any {} visitText(ast: t.TextAst, ctx: any): any {} visitEmbeddedTemplate(ast: t.EmbeddedTemplateAst, ctx: any): any {} visitNgContent(ast: t.NgContentAst, ctx: any): any {} visitAttr(ast: t.AttrAst, ctx: any): any {} visitDirective(ast: t.DirectiveAst, ctx: any): any {} - visitEvent(ast: t.BoundEventAst, ctx: any): any {} visitReference(ast: t.ReferenceAst, ctx: any): any {} visitVariable(ast: t.VariableAst, ctx: any): any {} visitDirectiveProperty(ast: t.BoundDirectivePropertyAst, ctx: any): any {} diff --git a/modules/@angular/compiler/src/animation/animation_parser.ts b/modules/@angular/compiler/src/animation/animation_parser.ts index 6177f54dd2..88ed61c8af 100644 --- a/modules/@angular/compiler/src/animation/animation_parser.ts +++ b/modules/@angular/compiler/src/animation/animation_parser.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {ANY_STATE, FILL_STYLE_FLAG} from '../../core_private'; +import {ANY_STATE, AnimationOutput, FILL_STYLE_FLAG} from '../../core_private'; import {CompileAnimationAnimateMetadata, CompileAnimationEntryMetadata, CompileAnimationGroupMetadata, CompileAnimationKeyframesSequenceMetadata, CompileAnimationMetadata, CompileAnimationSequenceMetadata, CompileAnimationStateDeclarationMetadata, CompileAnimationStateTransitionMetadata, CompileAnimationStyleMetadata, CompileAnimationWithStepsMetadata} from '../compile_metadata'; import {ListWrapper, StringMapWrapper} from '../facade/collection'; import {NumberWrapper, isArray, isBlank, isPresent, isString, isStringMap} from '../facade/lang'; @@ -53,6 +53,32 @@ export function parseAnimationEntry(entry: CompileAnimationEntryMetadata): Parse 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 new AnimationOutput(name, phase, outputName); +} + function _parseAnimationDeclarationStates( stateMetadata: CompileAnimationStateDeclarationMetadata, errors: AnimationParseError[]): AnimationStateDeclarationAst[] { diff --git a/modules/@angular/compiler/src/identifiers.ts b/modules/@angular/compiler/src/identifiers.ts index df77099d85..3240021222 100644 --- a/modules/@angular/compiler/src/identifiers.ts +++ b/modules/@angular/compiler/src/identifiers.ts @@ -7,7 +7,8 @@ */ 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 {AnimationGroupPlayer as AnimationGroupPlayer_, AnimationKeyframe as AnimationKeyframe_, AnimationSequencePlayer as AnimationSequencePlayer_, AnimationStyles as AnimationStyles_, AppElement, AppView, ChangeDetectorStatus, CodegenComponentFactoryResolver, DebugAppView, DebugContext, EMPTY_ARRAY, EMPTY_MAP, NgModuleInjector, NoOpAnimationPlayer as NoOpAnimationPlayer_, StaticNodeDebugInfo, TemplateRef_, UNINITIALIZED, ValueUnwrapper, ViewType, ViewUtils, balanceAnimationKeyframes as impBalanceAnimationKeyframes, castByValue, checkBinding, clearStyles as impClearStyles, collectAndResolveStyles as impCollectAndResolveStyles, devModeEqual, flattenNestedViewRenderNodes, interpolate, prepareFinalAnimationStyles as impBalanceAnimationStyles, pureProxy1, pureProxy10, pureProxy2, pureProxy3, pureProxy4, pureProxy5, pureProxy6, pureProxy7, pureProxy8, pureProxy9, renderStyles as impRenderStyles} from '../core_private'; + +import {AnimationGroupPlayer as AnimationGroupPlayer_, AnimationKeyframe as AnimationKeyframe_, AnimationOutput as AnimationOutput_, AnimationSequencePlayer as AnimationSequencePlayer_, AnimationStyles as AnimationStyles_, AppElement, AppView, ChangeDetectorStatus, CodegenComponentFactoryResolver, DebugAppView, DebugContext, EMPTY_ARRAY, EMPTY_MAP, NgModuleInjector, NoOpAnimationPlayer as NoOpAnimationPlayer_, StaticNodeDebugInfo, TemplateRef_, UNINITIALIZED, ValueUnwrapper, ViewType, ViewUtils, balanceAnimationKeyframes as impBalanceAnimationKeyframes, castByValue, checkBinding, clearStyles as impClearStyles, collectAndResolveStyles as impCollectAndResolveStyles, devModeEqual, flattenNestedViewRenderNodes, interpolate, prepareFinalAnimationStyles as impBalanceAnimationStyles, pureProxy1, pureProxy10, pureProxy2, pureProxy3, pureProxy4, pureProxy5, pureProxy6, pureProxy7, pureProxy8, pureProxy9, renderStyles as impRenderStyles} from '../core_private'; import {CompileIdentifierMetadata, CompileTokenMetadata} from './compile_metadata'; import {assetUrl} from './util'; @@ -53,6 +54,7 @@ var impAnimationSequencePlayer = AnimationSequencePlayer_; var impAnimationKeyframe = AnimationKeyframe_; var impAnimationStyles = AnimationStyles_; var impNoOpAnimationPlayer = NoOpAnimationPlayer_; +var impAnimationOutput = AnimationOutput_; var ANIMATION_STYLE_UTIL_ASSET_URL = assetUrl('core', 'animation/animation_style_util'); @@ -258,6 +260,11 @@ export class Identifiers { moduleUrl: assetUrl('core', 'i18n/tokens'), runtime: TRANSLATIONS_FORMAT_ }); + static AnimationOutput = new CompileIdentifierMetadata({ + name: 'AnimationOutput', + moduleUrl: assetUrl('core', 'animation/animation_output'), + runtime: impAnimationOutput + }); } export function identifierToken(identifier: CompileIdentifierMetadata): CompileTokenMetadata { diff --git a/modules/@angular/compiler/src/view_compiler/compile_view.ts b/modules/@angular/compiler/src/view_compiler/compile_view.ts index 0a46a2f66c..b34e22bcf6 100644 --- a/modules/@angular/compiler/src/view_compiler/compile_view.ts +++ b/modules/@angular/compiler/src/view_compiler/compile_view.ts @@ -7,7 +7,7 @@ */ import {ViewType} from '../../core_private'; -import {CompiledAnimation} from '../animation/animation_compiler'; +import {CompiledAnimationTriggerResult} from '../animation/animation_compiler'; import {CompileDirectiveMetadata, CompileIdentifierMap, CompileIdentifierMetadata, CompilePipeMetadata, CompileTokenMetadata} from '../compile_metadata'; import {CompilerConfig} from '../config'; import {ListWrapper} from '../facade/collection'; @@ -71,7 +71,7 @@ export class CompileView implements NameResolver { constructor( public component: CompileDirectiveMetadata, public genConfig: CompilerConfig, public pipeMetas: CompilePipeMetadata[], public styles: o.Expression, - public animations: CompiledAnimation[], public viewIndex: number, + public animations: CompiledAnimationTriggerResult[], public viewIndex: number, public declarationElement: CompileElement, public templateVariableBindings: string[][]) { this.createMethod = new CompileMethod(this); this.injectorGetMethod = 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 d2631ed1c4..1ee6ac7b59 100644 --- a/modules/@angular/compiler/src/view_compiler/event_binder.ts +++ b/modules/@angular/compiler/src/view_compiler/event_binder.ts @@ -6,10 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ +import {AnimationOutput} from '../../core_private'; import {CompileDirectiveMetadata} from '../compile_metadata'; import {ListWrapper, StringMapWrapper} from '../facade/collection'; import {StringWrapper, isBlank, isPresent} from '../facade/lang'; -import {identifierToken} from '../identifiers'; +import {Identifiers, identifierToken} from '../identifiers'; import * as o from '../output/output_ast'; import {BoundEventAst, DirectiveAst} from '../template_parser/template_ast'; @@ -19,6 +20,10 @@ 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; @@ -39,6 +44,8 @@ export class CompileEventListener { return listener; } + get methodName() { return this._methodName; } + constructor( public compileElement: CompileElement, public eventTarget: string, public eventName: string, listenerIndex: number) { @@ -112,6 +119,26 @@ export class CompileEventListener { disposable.set(listenExpr).toDeclStmt(o.FUNCTION_TYPE, [o.StmtModifier.Private])); } + listenToAnimation(output: AnimationOutput) { + var outputListener = o.THIS_EXPR.callMethod( + 'eventHandler', + [o.THIS_EXPR.prop(this._methodName).callMethod(o.BuiltinMethod.Bind, [o.THIS_EXPR])]); + + // tie the property callback method to the view animations map + var stmt = o.THIS_EXPR + .callMethod( + 'registerAnimationOutput', + [ + this.compileElement.renderNode, + o.importExpr(Identifiers.AnimationOutput).instantiate([ + o.literal(output.name), o.literal(output.phase) + ]), + outputListener + ]) + .toStmt(); + this.compileElement.view.createMethod.addStmt(stmt); + } + listenToDirective(directiveInstance: o.Expression, observablePropName: string) { var subscription = o.variable(`subscription_${this.compileElement.view.subscriptions.length}`); this.compileElement.view.subscriptions.push(subscription); @@ -166,6 +193,10 @@ export function bindRenderOutputs(eventListeners: CompileEventListener[]) { eventListeners.forEach(listener => listener.listenToRenderer()); } +export function bindAnimationOutputs(eventListeners: CompileElementAnimationOutput[]) { + eventListeners.forEach(entry => { entry.listener.listenToAnimation(entry.output); }); +} + function convertStmtIntoExpression(stmt: o.Statement): o.Expression { if (stmt instanceof o.ExpressionStatement) { return stmt.expr; diff --git a/modules/@angular/compiler/src/view_compiler/view_binder.ts b/modules/@angular/compiler/src/view_compiler/view_binder.ts index a099eb3733..dfa46be2f0 100644 --- a/modules/@angular/compiler/src/view_compiler/view_binder.ts +++ b/modules/@angular/compiler/src/view_compiler/view_binder.ts @@ -5,19 +5,20 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ - +import {AnimationOutput} from '../../core_private'; import {ListWrapper} from '../facade/collection'; import {identifierToken} from '../identifiers'; 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 {bindDirectiveOutputs, bindRenderOutputs, collectEventListeners} from './event_binder'; +import {CompileElementAnimationOutput, CompileEventListener, bindAnimationOutputs, 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[]): void { - var visitor = new ViewBinderVisitor(view); +export function bindView( + view: CompileView, parsedTemplate: TemplateAst[], animationOutputs: AnimationOutput[]): void { + var visitor = new ViewBinderVisitor(view, animationOutputs); templateVisitAll(visitor, parsedTemplate); view.pipes.forEach( (pipe) => { bindPipeDestroyLifecycleCallbacks(pipe.meta, pipe.instance, pipe.view); }); @@ -25,8 +26,12 @@ export function bindView(view: CompileView, parsedTemplate: TemplateAst[]): void class ViewBinderVisitor implements TemplateAstVisitor { private _nodeIndex: number = 0; + private _animationOutputsMap: {[key: string]: AnimationOutput} = {}; - constructor(public view: CompileView) {} + constructor(public view: CompileView, animationOutputs: AnimationOutput[]) { + animationOutputs.forEach( + entry => { this._animationOutputsMap[entry.fullPropertyName] = entry; }); + } visitBoundText(ast: BoundTextAst, parent: CompileElement): any { var node = this.view.nodes[this._nodeIndex++]; @@ -42,7 +47,23 @@ class ViewBinderVisitor implements TemplateAstVisitor { visitElement(ast: ElementAst, parent: CompileElement): any { var compileElement = this.view.nodes[this._nodeIndex++]; - var eventListeners = collectEventListeners(ast.outputs, ast.directives, compileElement); + 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); + } + }); + bindAnimationOutputs(animationEventListeners); bindRenderInputs(ast.inputs, compileElement); bindRenderOutputs(eventListeners); ast.directives.forEach((directiveAst) => { @@ -90,7 +111,7 @@ class ViewBinderVisitor implements TemplateAstVisitor { var providerInstance = compileElement.instances.get(providerAst.token); bindInjectableDestroyLifecycleCallbacks(providerAst, providerInstance, compileElement); }); - bindView(compileElement.embeddedView, ast.children); + 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 bbe8aee3d5..7025428d29 100644 --- a/modules/@angular/compiler/src/view_compiler/view_builder.ts +++ b/modules/@angular/compiler/src/view_compiler/view_builder.ts @@ -283,7 +283,7 @@ class ViewBuilderVisitor implements TemplateAstVisitor { this.nestedViewCount++; var embeddedView = new CompileView( this.view.component, this.view.genConfig, this.view.pipeMetas, o.NULL_EXPR, - compiledAnimations, this.view.viewIndex + this.nestedViewCount, compileElement, + compiledAnimations.triggers, this.view.viewIndex + this.nestedViewCount, compileElement, templateVariableBindings); this.nestedViewCount += buildView(embeddedView, ast.children, this.targetDependencies); diff --git a/modules/@angular/compiler/src/view_compiler/view_compiler.ts b/modules/@angular/compiler/src/view_compiler/view_compiler.ts index 6331819fbb..073c92cb41 100644 --- a/modules/@angular/compiler/src/view_compiler/view_compiler.ts +++ b/modules/@angular/compiler/src/view_compiler/view_compiler.ts @@ -38,17 +38,18 @@ export class ViewCompiler { var dependencies: Array = []; var compiledAnimations = this._animationCompiler.compileComponent(component, template); var statements: o.Statement[] = []; - compiledAnimations.map(entry => { + var animationTriggers = compiledAnimations.triggers; + animationTriggers.forEach(entry => { statements.push(entry.statesMapStatement); statements.push(entry.fnStatement); }); var view = new CompileView( - component, this._genConfig, pipes, styles, compiledAnimations, 0, + component, this._genConfig, pipes, styles, animationTriggers, 0, CompileElement.createNull(), []); 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); + bindView(view, template, compiledAnimations.outputs); 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 168e8828a5..094a3da956 100644 --- a/modules/@angular/compiler/test/animation/animation_compiler_spec.ts +++ b/modules/@angular/compiler/test/animation/animation_compiler_spec.ts @@ -10,7 +10,7 @@ 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, CompiledAnimation} from '../../src/animation/animation_compiler'; +import {AnimationCompiler, CompiledAnimationTriggerResult} from '../../src/animation/animation_compiler'; import {CompileAnimationEntryMetadata, CompileDirectiveMetadata, CompileTemplateMetadata, CompileTypeMetadata} from '../../src/compile_metadata'; import {CompileMetadataResolver} from '../../src/metadata_resolver'; @@ -22,9 +22,11 @@ export function main() { var compiler = new AnimationCompiler(); - var compileAnimations = (component: CompileDirectiveMetadata): CompiledAnimation => { - return compiler.compileComponent(component, [])[0]; - }; + var compileAnimations = + (component: CompileDirectiveMetadata): CompiledAnimationTriggerResult => { + var result = compiler.compileComponent(component, []); + return result.triggers[0]; + }; var compileTriggers = (input: any[]) => { var entries: CompileAnimationEntryMetadata[] = input.map(entry => { diff --git a/modules/@angular/core/private_export.ts b/modules/@angular/core/private_export.ts index a1a9bb20aa..4498690100 100644 --- a/modules/@angular/core/private_export.ts +++ b/modules/@angular/core/private_export.ts @@ -9,6 +9,7 @@ import {ANY_STATE as ANY_STATE_, DEFAULT_STATE as DEFAULT_STATE_, EMPTY_STATE as EMPTY_STATE_, FILL_STYLE_FLAG as FILL_STYLE_FLAG_} from './src/animation/animation_constants'; import {AnimationGroupPlayer as AnimationGroupPlayer_} from './src/animation/animation_group_player'; import {AnimationKeyframe as AnimationKeyframe_} from './src/animation/animation_keyframe'; +import {AnimationOutput as AnimationOutput_} from './src/animation/animation_output'; import {AnimationPlayer as AnimationPlayer_, NoOpAnimationPlayer as NoOpAnimationPlayer_} from './src/animation/animation_player'; import {AnimationSequencePlayer as AnimationSequencePlayer_} from './src/animation/animation_sequence_player'; import * as animationUtils from './src/animation/animation_style_util'; @@ -119,6 +120,8 @@ export declare namespace __core_private_types__ { export var collectAndResolveStyles: typeof animationUtils.collectAndResolveStyles; export type AnimationStyles = AnimationStyles_; export var AnimationStyles: typeof AnimationStyles_; + export type AnimationOutput = AnimationOutput_; + export var AnimationOutput: typeof AnimationOutput_; export var ANY_STATE: typeof ANY_STATE_; export var DEFAULT_STATE: typeof DEFAULT_STATE_; export var EMPTY_STATE: typeof EMPTY_STATE_; @@ -185,6 +188,7 @@ 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/animation/animation_output.ts b/modules/@angular/core/src/animation/animation_output.ts new file mode 100644 index 0000000000..bde629b3c9 --- /dev/null +++ b/modules/@angular/core/src/animation/animation_output.ts @@ -0,0 +1,10 @@ +/** + * @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/linker/view.ts b/modules/@angular/core/src/linker/view.ts index 9f2574e656..fc3672e353 100644 --- a/modules/@angular/core/src/linker/view.ts +++ b/modules/@angular/core/src/linker/view.ts @@ -7,7 +7,8 @@ */ import {AnimationGroupPlayer} from '../animation/animation_group_player'; -import {AnimationPlayer} from '../animation/animation_player'; +import {AnimationOutput} from '../animation/animation_output'; +import {AnimationPlayer, NoOpAnimationPlayer} from '../animation/animation_player'; import {ViewAnimationMap} from '../animation/view_animation_map'; import {ChangeDetectorRef, ChangeDetectorStatus} from '../change_detection/change_detection'; import {Injector} from '../di/injector'; @@ -50,6 +51,8 @@ export abstract class AppView { public animationPlayers = new ViewAnimationMap(); + private _animationListeners = new Map(); + public context: T; constructor( @@ -77,9 +80,23 @@ export abstract class AppView { } } - queueAnimation(element: any, animationName: string, player: AnimationPlayer): void { + queueAnimation( + element: any, animationName: string, player: AnimationPlayer, fromState: string, + toState: string): void { + var actualAnimationDetected = !(player instanceof NoOpAnimationPlayer); + var animationData = { + 'fromState': fromState, + 'toState': toState, + 'running': actualAnimationDetected + }; this.animationPlayers.set(element, animationName, player); - player.onDone(() => { this.animationPlayers.remove(element, animationName); }); + player.onDone(() => { + // TODO: make this into a datastructure for done|start + this.triggerAnimationOutput(element, animationName, 'done', animationData); + this.animationPlayers.remove(element, animationName); + }); + player.onStart( + () => { this.triggerAnimationOutput(element, animationName, 'start', animationData); }); } triggerQueuedAnimations() { @@ -90,6 +107,32 @@ export abstract class AppView { }); } + triggerAnimationOutput( + element: any, animationName: string, phase: string, animationData: {[key: string]: any}) { + var listeners = this._animationListeners.get(element); + if (isPresent(listeners) && listeners.length) { + for (let i = 0; i < listeners.length; i++) { + 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) { + listener.handler(animationData); + break; + } + } + } + } + + registerAnimationOutput(element: any, outputEvent: AnimationOutput, eventHandler: Function): + void { + var entry = new _AnimationOutputWithHandler(outputEvent, eventHandler); + var animations = this._animationListeners.get(element); + if (!isPresent(animations)) { + this._animationListeners.set(element, animations = []); + } + animations.push(entry); + } + create(context: T, givenProjectableNodes: Array, rootSelectorOrNode: string|any): AppElement { this.context = context; @@ -433,3 +476,7 @@ function _findLastRenderNode(node: any): any { } return lastNode; } + +class _AnimationOutputWithHandler { + constructor(public output: AnimationOutput, 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 7c5f112932..17c7d2b63c 100644 --- a/modules/@angular/core/test/animation/animation_integration_spec.ts +++ b/modules/@angular/core/test/animation/animation_integration_spec.ts @@ -10,18 +10,16 @@ import {NgIf} from '@angular/common'; import {AnimationDriver} from '@angular/platform-browser/src/dom/animation_driver'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {MockAnimationDriver} from '@angular/platform-browser/testing/mock_animation_driver'; - -import {BaseException} from '../../../compiler/src/facade/exceptions'; import {Component} from '../../index'; import {DEFAULT_STATE} from '../../src/animation/animation_constants'; import {AnimationKeyframe} from '../../src/animation/animation_keyframe'; import {AnimationPlayer} from '../../src/animation/animation_player'; import {AnimationStyles} from '../../src/animation/animation_styles'; import {AUTO_STYLE, AnimationEntryMetadata, animate, group, keyframes, sequence, state, style, transition, trigger} from '../../src/animation/metadata'; -import {isArray, isPresent} from '../../src/facade/lang'; -import {TestBed, async, fakeAsync, flushMicrotasks, tick} from '../../testing'; +import {isPresent} from '../../src/facade/lang'; +import {TestBed, fakeAsync, flushMicrotasks} from '../../testing'; import {MockAnimationPlayer} from '../../testing/mock_animation_player'; -import {AsyncTestCompleter, TestComponentBuilder, beforeEach, beforeEachProviders, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '../../testing/testing_internal'; +import {beforeEach, ddescribe, describe, expect, iit, it, xdescribe, xit} from '../../testing/testing_internal'; export function main() { describe('jit', () => { declareTests({useJit: true}); }); @@ -30,7 +28,7 @@ export function main() { function declareTests({useJit}: {useJit: boolean}) { describe('animation tests', function() { - beforeEachProviders(() => { + beforeEach(() => { TestBed.configureCompiler({useJit: useJit}); TestBed.configureTestingModule({ declarations: [DummyLoadingCmp, DummyIfCmp], @@ -38,216 +36,207 @@ function declareTests({useJit}: {useJit: boolean}) { }); }); - var makeAnimationCmp = - (tcb: TestComponentBuilder, tpl: string, - animationEntry: AnimationEntryMetadata | AnimationEntryMetadata[], - callback: (fixture: any) => void = null, failure: (fixture: any) => void = null) => { - var entries = isArray(animationEntry) ? animationEntry : - [animationEntry]; - tcb = tcb.overrideTemplate(DummyIfCmp, tpl); - tcb = tcb.overrideAnimations(DummyIfCmp, entries); - var promise = tcb.createAsync(DummyIfCmp).then((root) => { callback(root); }); - if (isPresent(failure)) { - promise.catch(failure); - } - tick(); - }; - describe('animation triggers', () => { - it('should trigger a state change animation from void => state', - inject( - [TestComponentBuilder, AnimationDriver], - fakeAsync((tcb: TestComponentBuilder, driver: MockAnimationDriver) => { - makeAnimationCmp( - tcb, '
', - trigger( - 'myAnimation', - [transition( - 'void => *', - [style({'opacity': 0}), animate(500, style({'opacity': 1}))])]), - (fixture: any /** TODO #9100 */) => { - var cmp = fixture.debugElement.componentInstance; - cmp.exp = true; - fixture.detectChanges(); - flushMicrotasks(); + it('should trigger a state change animation from void => state', fakeAsync(() => { + TestBed.overrideComponent(DummyIfCmp, { + set: { + template: ` +
+ `, + animations: [trigger( + 'myAnimation', + [transition( + 'void => *', + [style({'opacity': 0}), animate(500, style({'opacity': 1}))])])] + } + }); - expect(driver.log.length).toEqual(1); + const driver = TestBed.get(AnimationDriver) as MockAnimationDriver; - var keyframes2 = driver.log[0]['keyframeLookup']; - expect(keyframes2.length).toEqual(2); - expect(keyframes2[0]).toEqual([0, {'opacity': 0}]); - expect(keyframes2[1]).toEqual([1, {'opacity': 1}]); - }); - }))); + let fixture = TestBed.createComponent(DummyIfCmp); + var cmp = fixture.debugElement.componentInstance; + cmp.exp = true; + fixture.detectChanges(); + flushMicrotasks(); - it('should trigger a state change animation from state => void', - inject( - [TestComponentBuilder, AnimationDriver], - fakeAsync((tcb: TestComponentBuilder, driver: MockAnimationDriver) => { - makeAnimationCmp( - tcb, '
', - trigger( - 'myAnimation', - [transition( - '* => void', - [style({'opacity': 1}), animate(500, style({'opacity': 0}))])]), - (fixture: any /** TODO #9100 */) => { - var cmp = fixture.debugElement.componentInstance; - cmp.exp = true; - fixture.detectChanges(); - flushMicrotasks(); + expect(driver.log.length).toEqual(1); - cmp.exp = false; - fixture.detectChanges(); - flushMicrotasks(); + var keyframes2 = driver.log[0]['keyframeLookup']; + expect(keyframes2.length).toEqual(2); + expect(keyframes2[0]).toEqual([0, {'opacity': 0}]); + expect(keyframes2[1]).toEqual([1, {'opacity': 1}]); + })); - expect(driver.log.length).toEqual(1); + it('should trigger a state change animation from state => void', fakeAsync(() => { + TestBed.overrideComponent(DummyIfCmp, { + set: { + template: ` +
+ `, + animations: [trigger( + 'myAnimation', + [transition( + '* => void', + [style({'opacity': 1}), animate(500, style({'opacity': 0}))])])] + } + }); - var keyframes2 = driver.log[0]['keyframeLookup']; - expect(keyframes2.length).toEqual(2); - expect(keyframes2[0]).toEqual([0, {'opacity': 1}]); - expect(keyframes2[1]).toEqual([1, {'opacity': 0}]); - }); - }))); + const driver = TestBed.get(AnimationDriver) as MockAnimationDriver; + let fixture = TestBed.createComponent(DummyIfCmp); + var cmp = fixture.debugElement.componentInstance; - it('should animate the element when the expression changes between states', - inject( - [TestComponentBuilder, AnimationDriver], - fakeAsync((tcb: TestComponentBuilder, driver: MockAnimationDriver) => { - tcb.overrideAnimations( - DummyIfCmp, - [trigger( - 'myAnimation', - [transition( - '* => state1', - [ - style({'background': 'red'}), - animate('0.5s 1s ease-out', style({'background': 'blue'})) - ])])]) - .createAsync(DummyIfCmp) - .then((fixture) => { - tick(); + cmp.exp = true; + fixture.detectChanges(); + flushMicrotasks(); - var cmp = fixture.debugElement.componentInstance; - cmp.exp = 'state1'; - fixture.detectChanges(); + cmp.exp = false; + fixture.detectChanges(); + flushMicrotasks(); - flushMicrotasks(); + expect(driver.log.length).toEqual(1); - expect(driver.log.length).toEqual(1); + var keyframes2 = driver.log[0]['keyframeLookup']; + expect(keyframes2.length).toEqual(2); + expect(keyframes2[0]).toEqual([0, {'opacity': 1}]); + expect(keyframes2[1]).toEqual([1, {'opacity': 0}]); + })); - var animation1 = driver.log[0]; - expect(animation1['duration']).toEqual(500); - expect(animation1['delay']).toEqual(1000); - expect(animation1['easing']).toEqual('ease-out'); + it('should animate the element when the expression changes between states', fakeAsync(() => { + TestBed.overrideComponent(DummyIfCmp, { + set: { + template: ` +
+ `, + animations: [ + trigger('myAnimation', [ + transition('* => state1', [ + style({'background': 'red'}), + animate('0.5s 1s ease-out', style({'background': 'blue'})) + ]) + ]) + ] + } + }); - var startingStyles = animation1['startingStyles']; - expect(startingStyles).toEqual({'background': 'red'}); + const driver = TestBed.get(AnimationDriver) as MockAnimationDriver; + let fixture = TestBed.createComponent(DummyIfCmp); + var cmp = fixture.debugElement.componentInstance; + cmp.exp = 'state1'; + fixture.detectChanges(); - var keyframes = animation1['keyframeLookup']; - expect(keyframes[0]).toEqual([0, {'background': 'red'}]); - expect(keyframes[1]).toEqual([1, {'background': 'blue'}]); - }); - }))); + flushMicrotasks(); + + expect(driver.log.length).toEqual(1); + + var animation1 = driver.log[0]; + expect(animation1['duration']).toEqual(500); + expect(animation1['delay']).toEqual(1000); + expect(animation1['easing']).toEqual('ease-out'); + + var startingStyles = animation1['startingStyles']; + expect(startingStyles).toEqual({'background': 'red'}); + + var kf = animation1['keyframeLookup']; + expect(kf[0]).toEqual([0, {'background': 'red'}]); + expect(kf[1]).toEqual([1, {'background': 'blue'}]); + })); it('should animate between * and void and back even when no expression is assigned', - inject( - [TestComponentBuilder, AnimationDriver], - fakeAsync((tcb: TestComponentBuilder, driver: MockAnimationDriver) => { - tcb = tcb.overrideTemplate(DummyIfCmp, ` -
- `); - tcb.overrideAnimations(DummyIfCmp, [trigger( - 'myAnimation', - [ - state('*', style({'opacity': '1'})), - state('void', style({'opacity': '0'})), - transition('* => *', [animate('500ms')]) - ])]) - .createAsync(DummyIfCmp) - .then((fixture) => { - tick(); + fakeAsync(() => { + TestBed.overrideComponent(DummyIfCmp, { + set: { + template: ` +
+ `, + animations: [trigger( + 'myAnimation', + [ + state('*', style({'opacity': '1'})), state('void', style({'opacity': '0'})), + transition('* => *', [animate('500ms')]) + ])] + } + }); - var cmp = fixture.debugElement.componentInstance; - cmp.exp = true; - fixture.detectChanges(); - flushMicrotasks(); + const driver = TestBed.get(AnimationDriver) as MockAnimationDriver; + let fixture = TestBed.createComponent(DummyIfCmp); + var cmp = fixture.debugElement.componentInstance; + cmp.exp = true; + fixture.detectChanges(); + flushMicrotasks(); - var result = driver.log.pop(); - expect(result['duration']).toEqual(500); - expect(result['startingStyles']).toEqual({'opacity': '0'}); - expect(result['keyframeLookup']).toEqual([ - [0, {'opacity': '0'}], [1, {'opacity': '1'}] - ]); + var result = driver.log.pop(); + expect(result['duration']).toEqual(500); + expect(result['startingStyles']).toEqual({'opacity': '0'}); + expect(result['keyframeLookup']).toEqual([[0, {'opacity': '0'}], [1, {'opacity': '1'}]]); - cmp.exp = false; - fixture.detectChanges(); - flushMicrotasks(); + cmp.exp = false; + fixture.detectChanges(); + flushMicrotasks(); - result = driver.log.pop(); - expect(result['duration']).toEqual(500); - expect(result['startingStyles']).toEqual({'opacity': '1'}); - expect(result['keyframeLookup']).toEqual([ - [0, {'opacity': '1'}], [1, {'opacity': '0'}] - ]); - }); - }))); + result = driver.log.pop(); + expect(result['duration']).toEqual(500); + expect(result['startingStyles']).toEqual({'opacity': '1'}); + expect(result['keyframeLookup']).toEqual([[0, {'opacity': '1'}], [1, {'opacity': '0'}]]); + })); - it('should combine repeated style steps into a single step', - inject( - [TestComponentBuilder, AnimationDriver], - fakeAsync((tcb: TestComponentBuilder, driver: MockAnimationDriver) => { - tcb.overrideAnimations(DummyIfCmp, [ - trigger('myAnimation', [ - transition('void => *', [ - style({'background': 'red'}), - style({'width': '100px'}), - style({'background': 'gold'}), - style({'height': 111}), - animate('999ms', style({'width': '200px', 'background': 'blue'})), - style({'opacity': '1'}), - style({'border-width': '100px'}), - animate('999ms', style({'opacity': '0', 'height': '200px', 'border-width': '10px'})) - ]) - ]) - ]) - .createAsync(DummyIfCmp) - .then((fixture) => { - tick(); + it('should combine repeated style steps into a single step', fakeAsync(() => { + TestBed.overrideComponent(DummyIfCmp, { + set: { + template: ` +
+ `, + animations: [ + trigger('myAnimation', [ + transition('void => *', [ + style({'background': 'red'}), + style({'width': '100px'}), + style({'background': 'gold'}), + style({'height': 111}), + animate('999ms', style({'width': '200px', 'background': 'blue'})), + style({'opacity': '1'}), + style({'border-width': '100px'}), + animate('999ms', style({'opacity': '0', 'height': '200px', 'border-width': '10px'})) + ]) + ]) + ] + } + }); - var cmp = fixture.debugElement.componentInstance; - cmp.exp = true; - fixture.detectChanges(); + const driver = TestBed.get(AnimationDriver) as MockAnimationDriver; + let fixture = TestBed.createComponent(DummyIfCmp); + var cmp = fixture.debugElement.componentInstance; + cmp.exp = true; + fixture.detectChanges(); - flushMicrotasks(); + flushMicrotasks(); - expect(driver.log.length).toEqual(2); + expect(driver.log.length).toEqual(2); - var animation1 = driver.log[0]; - expect(animation1['duration']).toEqual(999); - expect(animation1['delay']).toEqual(0); - expect(animation1['easing']).toEqual(null); - expect(animation1['startingStyles']) - .toEqual({'background': 'gold', 'width': '100px', 'height': 111}); + var animation1 = driver.log[0]; + expect(animation1['duration']).toEqual(999); + expect(animation1['delay']).toEqual(0); + expect(animation1['easing']).toEqual(null); + expect(animation1['startingStyles']) + .toEqual({'background': 'gold', 'width': '100px', 'height': 111}); - var keyframes1 = animation1['keyframeLookup']; - expect(keyframes1[0]).toEqual([0, {'background': 'gold', 'width': '100px'}]); - expect(keyframes1[1]).toEqual([1, {'background': 'blue', 'width': '200px'}]); + var keyframes1 = animation1['keyframeLookup']; + expect(keyframes1[0]).toEqual([0, {'background': 'gold', 'width': '100px'}]); + expect(keyframes1[1]).toEqual([1, {'background': 'blue', 'width': '200px'}]); - var animation2 = driver.log[1]; - expect(animation2['duration']).toEqual(999); - expect(animation2['delay']).toEqual(0); - expect(animation2['easing']).toEqual(null); - expect(animation2['startingStyles']) - .toEqual({'opacity': '1', 'border-width': '100px'}); + var animation2 = driver.log[1]; + expect(animation2['duration']).toEqual(999); + expect(animation2['delay']).toEqual(0); + expect(animation2['easing']).toEqual(null); + expect(animation2['startingStyles']).toEqual({'opacity': '1', 'border-width': '100px'}); - var keyframes2 = animation2['keyframeLookup']; - expect(keyframes2[0]) - .toEqual([0, {'opacity': '1', 'height': 111, 'border-width': '100px'}]); - expect(keyframes2[1]) - .toEqual([1, {'opacity': '0', 'height': '200px', 'border-width': '10px'}]); - }); - }))); + var keyframes2 = animation2['keyframeLookup']; + expect(keyframes2[0]).toEqual([ + 0, {'opacity': '1', 'height': 111, 'border-width': '100px'} + ]); + expect(keyframes2[1]).toEqual([ + 1, {'opacity': '0', 'height': '200px', 'border-width': '10px'} + ]); + })); describe('groups/sequences', () => { var assertPlaying = (player: MockAnimationDriver, isPlaying: any /** TODO #9100 */) => { @@ -263,491 +252,477 @@ function declareTests({useJit}: {useJit: boolean}) { }; it('should run animations in sequence one by one if a top-level array is used', - inject( - [TestComponentBuilder, AnimationDriver], - fakeAsync((tcb: TestComponentBuilder, driver: MockAnimationDriver) => { - tcb.overrideAnimations( - DummyIfCmp, - [trigger('myAnimation', [transition( - 'void => *', - [ - style({'opacity': '0'}), - animate(1000, style({'opacity': '0.5'})), - animate('1000ms', style({'opacity': '0.8'})), - animate('1s', style({'opacity': '1'})), - ])])]) - .createAsync(DummyIfCmp) - .then((fixture) => { + fakeAsync(() => { + TestBed.overrideComponent(DummyIfCmp, { + set: { + template: ` +
+ `, + animations: [ + trigger('myAnimation', [transition( + 'void => *', + [ + style({'opacity': '0'}), + animate(1000, style({'opacity': '0.5'})), + animate('1000ms', style({'opacity': '0.8'})), + animate('1s', style({'opacity': '1'})), + ])]) + ] + } + }); - tick(); + const driver = TestBed.get(AnimationDriver) as MockAnimationDriver; + let fixture = TestBed.createComponent(DummyIfCmp); + var cmp = fixture.debugElement.componentInstance; - var cmp = fixture.debugElement.componentInstance; - cmp.exp = true; - fixture.detectChanges(); + cmp.exp = true; + fixture.detectChanges(); - flushMicrotasks(); + flushMicrotasks(); - expect(driver.log.length).toEqual(3); + expect(driver.log.length).toEqual(3); - var player1 = driver.log[0]['player']; - var player2 = driver.log[1]['player']; - var player3 = driver.log[2]['player']; + var player1 = driver.log[0]['player']; + var player2 = driver.log[1]['player']; + var player3 = driver.log[2]['player']; - assertPlaying(player1, true); - assertPlaying(player2, false); - assertPlaying(player3, false); + assertPlaying(player1, true); + assertPlaying(player2, false); + assertPlaying(player3, false); - player1.finish(); + player1.finish(); - assertPlaying(player1, false); - assertPlaying(player2, true); - assertPlaying(player3, false); + assertPlaying(player1, false); + assertPlaying(player2, true); + assertPlaying(player3, false); - player2.finish(); + player2.finish(); - assertPlaying(player1, false); - assertPlaying(player2, false); - assertPlaying(player3, true); + assertPlaying(player1, false); + assertPlaying(player2, false); + assertPlaying(player3, true); - player3.finish(); + player3.finish(); - assertPlaying(player1, false); - assertPlaying(player2, false); - assertPlaying(player3, false); - }); - }))); + assertPlaying(player1, false); + assertPlaying(player2, false); + assertPlaying(player3, false); + })); - it('should run animations in parallel if a group is used', - inject( - [TestComponentBuilder, AnimationDriver], - fakeAsync((tcb: TestComponentBuilder, driver: MockAnimationDriver) => { - tcb.overrideAnimations(DummyIfCmp, [ - trigger('myAnimation', [ - transition('void => *', [ - style({'width': 0, 'height': 0}), - group([animate(1000, style({'width': 100})), animate(5000, style({'height': 500}))]), - group([animate(1000, style({'width': 0})), animate(5000, style({'height': 0}))]) - ]) - ]) - ]) - .createAsync(DummyIfCmp) - .then((fixture) => { + it('should run animations in parallel if a group is used', fakeAsync(() => { + TestBed.overrideComponent(DummyIfCmp, { + set: { + template: ` +
+ `, + animations: [ + trigger('myAnimation', [ + transition('void => *', [ + style({'width': 0, 'height': 0}), + group([animate(1000, style({'width': 100})), animate(5000, style({'height': 500}))]), + group([animate(1000, style({'width': 0})), animate(5000, style({'height': 0}))]) + ]) + ]) + ] + } + }); - tick(); + const driver = TestBed.get(AnimationDriver) as MockAnimationDriver; + let fixture = TestBed.createComponent(DummyIfCmp); + var cmp = fixture.debugElement.componentInstance; - var cmp = fixture.debugElement.componentInstance; - cmp.exp = true; - fixture.detectChanges(); + cmp.exp = true; + fixture.detectChanges(); - flushMicrotasks(); + flushMicrotasks(); - expect(driver.log.length).toEqual(5); + expect(driver.log.length).toEqual(5); - var player1 = driver.log[0]['player']; - var player2 = driver.log[1]['player']; - var player3 = driver.log[2]['player']; - var player4 = driver.log[3]['player']; - var player5 = driver.log[4]['player']; + var player1 = driver.log[0]['player']; + var player2 = driver.log[1]['player']; + var player3 = driver.log[2]['player']; + var player4 = driver.log[3]['player']; + var player5 = driver.log[4]['player']; - assertPlaying(player1, true); - assertPlaying(player2, false); - assertPlaying(player3, false); - assertPlaying(player4, false); - assertPlaying(player5, false); + assertPlaying(player1, true); + assertPlaying(player2, false); + assertPlaying(player3, false); + assertPlaying(player4, false); + assertPlaying(player5, false); - player1.finish(); + player1.finish(); - assertPlaying(player1, false); - assertPlaying(player2, true); - assertPlaying(player3, true); - assertPlaying(player4, false); - assertPlaying(player5, false); + assertPlaying(player1, false); + assertPlaying(player2, true); + assertPlaying(player3, true); + assertPlaying(player4, false); + assertPlaying(player5, false); - player2.finish(); + player2.finish(); - assertPlaying(player1, false); - assertPlaying(player2, false); - assertPlaying(player3, true); - assertPlaying(player4, false); - assertPlaying(player5, false); + assertPlaying(player1, false); + assertPlaying(player2, false); + assertPlaying(player3, true); + assertPlaying(player4, false); + assertPlaying(player5, false); - player3.finish(); + player3.finish(); - assertPlaying(player1, false); - assertPlaying(player2, false); - assertPlaying(player3, false); - assertPlaying(player4, true); - assertPlaying(player5, true); - }); - }))); + assertPlaying(player1, false); + assertPlaying(player2, false); + assertPlaying(player3, false); + assertPlaying(player4, true); + assertPlaying(player5, true); + })); }); + describe('keyframes', () => { - it( - 'should create an animation step with multiple keyframes', - inject( - [TestComponentBuilder, AnimationDriver], fakeAsync( - (tcb: TestComponentBuilder, - driver: MockAnimationDriver) => { - tcb.overrideAnimations( - DummyIfCmp, - [trigger( - 'myAnimation', - [transition( - 'void => *', - [animate( - 1000, keyframes([ - style([{ - 'width': 0, - offset: 0 - }]), - style([{ - 'width': 100, - offset: 0.25 - }]), - style([{ - 'width': 200, - offset: 0.75 - }]), - style([{ - 'width': 300, - offset: 1 - }]) - ]))])])]) - .createAsync(DummyIfCmp) - .then((fixture) => { + it('should create an animation step with multiple keyframes', fakeAsync(() => { + TestBed.overrideComponent(DummyIfCmp, { + set: { + template: ` +
+ `, + animations: [trigger( + 'myAnimation', + [transition('void => *', [animate( + 1000, keyframes([ + style([{'width': 0, offset: 0}]), + style([{'width': 100, offset: 0.25}]), + style([{'width': 200, offset: 0.75}]), + style([{'width': 300, offset: 1}]) + ]))])])] + } + }); - tick(); + const driver = TestBed.get(AnimationDriver) as MockAnimationDriver; + let fixture = TestBed.createComponent(DummyIfCmp); + var cmp = fixture.debugElement.componentInstance; + cmp.exp = true; + fixture.detectChanges(); + flushMicrotasks(); - var cmp = - fixture.debugElement - .componentInstance; - cmp.exp = true; - fixture.detectChanges(); - flushMicrotasks(); - - var keyframes = - driver - .log[0] - ['keyframeLookup']; - expect(keyframes.length) - .toEqual(4); - expect(keyframes[0]).toEqual([ - 0, {'width': 0} - ]); - expect(keyframes[1]).toEqual([ - 0.25, {'width': 100} - ]); - expect(keyframes[2]).toEqual([ - 0.75, {'width': 200} - ]); - expect(keyframes[3]).toEqual([ - 1, {'width': 300} - ]); - }); - }))); + var kf = driver.log[0]['keyframeLookup']; + expect(kf.length).toEqual(4); + expect(kf[0]).toEqual([0, {'width': 0}]); + expect(kf[1]).toEqual([0.25, {'width': 100}]); + expect(kf[2]).toEqual([0.75, {'width': 200}]); + expect(kf[3]).toEqual([1, {'width': 300}]); + })); it('should fetch any keyframe styles that are not defined in the first keyframe from the previous entries or getCompuedStyle', - inject( - [TestComponentBuilder, AnimationDriver], - fakeAsync((tcb: TestComponentBuilder, driver: MockAnimationDriver) => { - tcb.overrideAnimations(DummyIfCmp, [ - trigger('myAnimation', [ - transition('void => *', [ - style({ 'color': 'white' }), - animate(1000, style({ 'color': 'silver' })), - animate(1000, keyframes([ - style([{ 'color': 'gold', offset: 0.25 }]), - style([{ 'color': 'bronze', 'background-color':'teal', offset: 0.50 }]), - style([{ 'color': 'platinum', offset: 0.75 }]), - style([{ 'color': 'diamond', offset: 1 }]) - ])) - ]) - ]) - ]) - .createAsync(DummyIfCmp) - .then((fixture) => { + fakeAsync(() => { + TestBed.overrideComponent(DummyIfCmp, { + set: { + template: ` +
+ `, + animations: [ + trigger('myAnimation', [ + transition('void => *', [ + style({'color': 'white'}), + animate(1000, style({'color': 'silver'})), + animate(1000, keyframes([ + style([{'color': 'gold', offset: 0.25}]), + style([{'color': 'bronze', 'background-color': 'teal', offset: 0.50}]), + style([{'color': 'platinum', offset: 0.75}]), + style([{'color': 'diamond', offset: 1}]) + ])) + ]) + ]) + ] + } + }); - tick(); + const driver = TestBed.get(AnimationDriver) as MockAnimationDriver; + let fixture = TestBed.createComponent(DummyIfCmp); + var cmp = fixture.debugElement.componentInstance; + cmp.exp = true; + fixture.detectChanges(); + flushMicrotasks(); - var cmp = fixture.debugElement.componentInstance; - cmp.exp = true; - fixture.detectChanges(); - flushMicrotasks(); - - var keyframes = driver.log[1]['keyframeLookup']; - expect(keyframes.length).toEqual(5); - expect(keyframes[0]).toEqual([0, {'color': 'silver', 'background-color':AUTO_STYLE }]); - expect(keyframes[1]).toEqual([0.25, {'color': 'gold'}]); - expect(keyframes[2]).toEqual([0.50, {'color': 'bronze', 'background-color':'teal'}]); - expect(keyframes[3]).toEqual([0.75, {'color': 'platinum'}]); - expect(keyframes[4]).toEqual([1, {'color': 'diamond', 'background-color':'teal'}]); - }); - }))); + var kf = driver.log[1]['keyframeLookup']; + expect(kf.length).toEqual(5); + expect(kf[0]).toEqual([0, {'color': 'silver', 'background-color': AUTO_STYLE}]); + expect(kf[1]).toEqual([0.25, {'color': 'gold'}]); + expect(kf[2]).toEqual([0.50, {'color': 'bronze', 'background-color': 'teal'}]); + expect(kf[3]).toEqual([0.75, {'color': 'platinum'}]); + expect(kf[4]).toEqual([1, {'color': 'diamond', 'background-color': 'teal'}]); + })); }); it('should cancel the previously running animation active with the same element/animationName pair', - inject( - [TestComponentBuilder, AnimationDriver], - fakeAsync((tcb: TestComponentBuilder, driver: MockAnimationDriver) => { - tcb.overrideAnimations( - DummyIfCmp, - [trigger( - 'myAnimation', - [transition( - '* => *', - [style({'opacity': 0}), animate(500, style({'opacity': 1}))])])]) - .createAsync(DummyIfCmp) - .then((fixture) => { + fakeAsync(() => { + TestBed.overrideComponent(DummyIfCmp, { + set: { + template: ` +
+ `, + animations: [trigger( + 'myAnimation', + [transition( + '* => *', + [style({'opacity': 0}), animate(500, style({'opacity': 1}))])])] + } + }); - tick(); + const driver = TestBed.get(AnimationDriver) as MockAnimationDriver; + let fixture = TestBed.createComponent(DummyIfCmp); + var cmp = fixture.debugElement.componentInstance; - var cmp = fixture.debugElement.componentInstance; + cmp.exp = 'state1'; + fixture.detectChanges(); + flushMicrotasks(); - cmp.exp = 'state1'; - fixture.detectChanges(); - flushMicrotasks(); + var enterCompleted = false; + var enterPlayer = driver.log[0]['player']; + enterPlayer.onDone(() => enterCompleted = true); - var enterCompleted = false; - var enterPlayer = driver.log[0]['player']; - enterPlayer.onDone(() => enterCompleted = true); + expect(enterCompleted).toEqual(false); - expect(enterCompleted).toEqual(false); + cmp.exp = 'state2'; + fixture.detectChanges(); + flushMicrotasks(); - cmp.exp = 'state2'; - fixture.detectChanges(); - flushMicrotasks(); + expect(enterCompleted).toEqual(true); + })); - expect(enterCompleted).toEqual(true); - }); - }))); + it('should destroy all animation players once the animation is complete', fakeAsync(() => { + TestBed.overrideComponent(DummyIfCmp, { + set: { + template: ` +
+ `, + animations: [ + trigger('myAnimation', [ + transition('void => *', [ + style({'background': 'red', 'opacity': 0.5}), + animate(500, style({'background': 'black'})), + group([ + animate(500, style({'background': 'black'})), + animate(1000, style({'opacity': '0.2'})), + ]), + sequence([ + animate(500, style({'opacity': '1'})), + animate(1000, style({'background': 'white'})) + ]) + ]) + ]) + ] + } + }); - it('should destroy all animation players once the animation is complete', - inject( - [TestComponentBuilder, AnimationDriver], - fakeAsync((tcb: TestComponentBuilder, driver: MockAnimationDriver) => { - tcb.overrideAnimations(DummyIfCmp, [ - trigger('myAnimation', [ - transition('void => *', [ - style({'background': 'red', 'opacity': 0.5}), - animate(500, style({'background': 'black'})), - group([ - animate(500, style({'background': 'black'})), - animate(1000, style({'opacity': '0.2'})), - ]), - sequence([ - animate(500, style({'opacity': '1'})), - animate(1000, style({'background': 'white'})) - ]) - ]) - ]) - ]).createAsync(DummyIfCmp) - .then((fixture) => { - tick(); + const driver = TestBed.get(AnimationDriver) as MockAnimationDriver; + let fixture = TestBed.createComponent(DummyIfCmp); + var cmp = fixture.debugElement.componentInstance; - var cmp = fixture.debugElement.componentInstance; - cmp.exp = true; - fixture.detectChanges(); + cmp.exp = true; + fixture.detectChanges(); - flushMicrotasks(); + flushMicrotasks(); - expect(driver.log.length).toEqual(5); + expect(driver.log.length).toEqual(5); - driver.log.forEach(entry => entry['player'].finish()); - driver.log.forEach(entry => { - var player = entry['player']; - expect(player.log[player.log.length - 2]).toEqual('finish'); - expect(player.log[player.log.length - 1]).toEqual('destroy'); - }); - }); - }))); + driver.log.forEach(entry => entry['player'].finish()); + driver.log.forEach(entry => { + var player = entry['player']; + expect(player.log[player.log.length - 2]).toEqual('finish'); + expect(player.log[player.log.length - 1]).toEqual('destroy'); + }); + })); it('should use first matched animation when multiple animations are registered', - inject( - [TestComponentBuilder, AnimationDriver], - fakeAsync((tcb: TestComponentBuilder, driver: MockAnimationDriver) => { - tcb = tcb.overrideTemplate(DummyIfCmp, ` -
-
- `); - tcb.overrideAnimations( - DummyIfCmp, - [ - trigger( - 'rotate', - [ - transition( - 'start => *', - [ - style({'color': 'white'}), - animate(500, style({'color': 'red'})) - ]), - transition( - 'start => end', - [ - style({'color': 'white'}), - animate(500, style({'color': 'pink'})) - ]) - ]), - ]) - .createAsync(DummyIfCmp) - .then((fixture) => { - tick(); + fakeAsync(() => { + TestBed.overrideComponent(DummyIfCmp, { + set: { + template: ` +
+
+ `, + animations: [ + trigger( + 'rotate', + [ + transition( + 'start => *', + [ + style({'color': 'white'}), + animate(500, style({'color': 'red'})) + ]), + transition( + 'start => end', + [ + style({'color': 'white'}), + animate(500, style({'color': 'pink'})) + ]) + ]) + ] + } + }); - var cmp = fixture.debugElement.componentInstance; - cmp.exp = 'start'; - cmp.exp2 = 'start'; - fixture.detectChanges(); - flushMicrotasks(); + const driver = TestBed.get(AnimationDriver) as MockAnimationDriver; + let fixture = TestBed.createComponent(DummyIfCmp); + var cmp = fixture.debugElement.componentInstance; - expect(driver.log.length).toEqual(0); + cmp.exp = 'start'; + cmp.exp2 = 'start'; + fixture.detectChanges(); + flushMicrotasks(); - cmp.exp = 'something'; - fixture.detectChanges(); - flushMicrotasks(); + expect(driver.log.length).toEqual(0); - expect(driver.log.length).toEqual(1); + cmp.exp = 'something'; + fixture.detectChanges(); + flushMicrotasks(); - var animation1 = driver.log[0]; - var keyframes1 = animation1['keyframeLookup']; - var toStyles1 = keyframes1[1][1]; - expect(toStyles1['color']).toEqual('red'); + expect(driver.log.length).toEqual(1); - cmp.exp2 = 'end'; - fixture.detectChanges(); - flushMicrotasks(); + var animation1 = driver.log[0]; + var keyframes1 = animation1['keyframeLookup']; + var toStyles1 = keyframes1[1][1]; + expect(toStyles1['color']).toEqual('red'); - expect(driver.log.length).toEqual(2); + cmp.exp2 = 'end'; + fixture.detectChanges(); + flushMicrotasks(); - var animation2 = driver.log[1]; - var keyframes2 = animation2['keyframeLookup']; - var toStyles2 = keyframes2[1][1]; - expect(toStyles2['color']).toEqual('red'); - }); - }))); + expect(driver.log.length).toEqual(2); + + var animation2 = driver.log[1]; + var keyframes2 = animation2['keyframeLookup']; + var toStyles2 = keyframes2[1][1]; + expect(toStyles2['color']).toEqual('red'); + })); it('should not remove the element until the void transition animation is complete', - inject( - [TestComponentBuilder, AnimationDriver], - fakeAsync((tcb: TestComponentBuilder, driver: MockAnimationDriver) => { - makeAnimationCmp( - tcb, '
', - trigger( - 'myAnimation', - [transition('* => void', [animate(1000, style({'opacity': 0}))])]), - (fixture: any /** TODO #9100 */) => { - tick(); + fakeAsync(() => { + TestBed.overrideComponent(DummyIfCmp, { + set: { + template: ` +
+ `, + animations: [trigger( + 'myAnimation', + [transition('* => void', [animate(1000, style({'opacity': 0}))])])] + } + }); - var cmp = fixture.debugElement.componentInstance; - cmp.exp = true; - fixture.detectChanges(); - flushMicrotasks(); + const driver = TestBed.get(AnimationDriver) as MockAnimationDriver; + let fixture = TestBed.createComponent(DummyIfCmp); + var cmp = fixture.debugElement.componentInstance; + cmp.exp = true; + fixture.detectChanges(); + flushMicrotasks(); - cmp.exp = false; - fixture.detectChanges(); - flushMicrotasks(); + cmp.exp = false; + fixture.detectChanges(); + flushMicrotasks(); - var player = driver.log[0]['player']; - var container = fixture.debugElement.nativeElement; - var ifElm = getDOM().querySelector(container, '.my-if'); - expect(ifElm).toBeTruthy(); + var player = driver.log[0]['player']; + var container = fixture.debugElement.nativeElement; + var ifElm = getDOM().querySelector(container, '.my-if'); + expect(ifElm).toBeTruthy(); - player.finish(); - ifElm = getDOM().querySelector(container, '.my-if'); - expect(ifElm).toBeFalsy(); - }); - }))); + player.finish(); + ifElm = getDOM().querySelector(container, '.my-if'); + expect(ifElm).toBeFalsy(); + })); it('should fill an animation with the missing style values if not defined within an earlier style step', - inject( - [TestComponentBuilder, AnimationDriver], - fakeAsync((tcb: TestComponentBuilder, driver: MockAnimationDriver) => { - makeAnimationCmp( - tcb, '
', - trigger('myAnimation', [transition( - '* => *', - [ - animate(1000, style({'opacity': 0})), - animate(1000, style({'opacity': 1})) - ])]), - (fixture: any /** TODO #9100 */) => { - tick(); + fakeAsync(() => { + TestBed.overrideComponent(DummyIfCmp, { + set: { + template: ` +
+ `, + animations: + [trigger('myAnimation', [transition( + '* => *', + [ + animate(1000, style({'opacity': 0})), + animate(1000, style({'opacity': 1})) + ])])] + } + }); - var cmp = fixture.debugElement.componentInstance; - cmp.exp = 'state1'; - fixture.detectChanges(); - flushMicrotasks(); + const driver = TestBed.get(AnimationDriver) as MockAnimationDriver; + let fixture = TestBed.createComponent(DummyIfCmp); + var cmp = fixture.debugElement.componentInstance; + cmp.exp = 'state1'; + fixture.detectChanges(); + flushMicrotasks(); - var animation1 = driver.log[0]; - var keyframes1 = animation1['keyframeLookup']; - expect(keyframes1[0]).toEqual([0, {'opacity': AUTO_STYLE}]); - expect(keyframes1[1]).toEqual([1, {'opacity': 0}]); + var animation1 = driver.log[0]; + var keyframes1 = animation1['keyframeLookup']; + expect(keyframes1[0]).toEqual([0, {'opacity': AUTO_STYLE}]); + expect(keyframes1[1]).toEqual([1, {'opacity': 0}]); - var animation2 = driver.log[1]; - var keyframes2 = animation2['keyframeLookup']; - expect(keyframes2[0]).toEqual([0, {'opacity': 0}]); - expect(keyframes2[1]).toEqual([1, {'opacity': 1}]); - }); - }))); + var animation2 = driver.log[1]; + var keyframes2 = animation2['keyframeLookup']; + expect(keyframes2[0]).toEqual([0, {'opacity': 0}]); + expect(keyframes2[1]).toEqual([1, {'opacity': 1}]); + })); it('should perform two transitions in parallel if defined in different state triggers', - inject( - [TestComponentBuilder, AnimationDriver], - fakeAsync((tcb: TestComponentBuilder, driver: MockAnimationDriver) => { - makeAnimationCmp( - tcb, '
', - [ - trigger( - 'one', - [transition( - 'state1 => state2', - [style({'opacity': 0}), animate(1000, style({'opacity': 1}))])]), - trigger( - 'two', - [transition( - 'state1 => state2', - [style({'width': 100}), animate(1000, style({'width': 1000}))])]) - ], - (fixture: any /** TODO #9100 */) => { - tick(); + fakeAsync(() => { + TestBed.overrideComponent(DummyIfCmp, { + set: { + template: ` +
+ `, + animations: [ + trigger( + 'one', [transition( + 'state1 => state2', + [style({'opacity': 0}), animate(1000, style({'opacity': 1}))])]), + trigger( + 'two', + [transition( + 'state1 => state2', + [style({'width': 100}), animate(1000, style({'width': 1000}))])]) + ] + } + }); - var cmp = fixture.debugElement.componentInstance; - cmp.exp = 'state1'; - cmp.exp2 = 'state1'; - fixture.detectChanges(); - flushMicrotasks(); + const driver = TestBed.get(AnimationDriver) as MockAnimationDriver; + let fixture = TestBed.createComponent(DummyIfCmp); + var cmp = fixture.debugElement.componentInstance; + cmp.exp = 'state1'; + cmp.exp2 = 'state1'; + fixture.detectChanges(); + flushMicrotasks(); - cmp.exp = 'state2'; - fixture.detectChanges(); - flushMicrotasks(); + cmp.exp = 'state2'; + fixture.detectChanges(); + flushMicrotasks(); - expect(driver.log.length).toEqual(1); + expect(driver.log.length).toEqual(1); - var count = 0; - var animation1 = driver.log[0]; - var player1 = animation1['player']; - player1.onDone(() => count++); + var count = 0; + var animation1 = driver.log[0]; + var player1 = animation1['player']; + player1.onDone(() => count++); - expect(count).toEqual(0); + expect(count).toEqual(0); - cmp.exp2 = 'state2'; - fixture.detectChanges(); - flushMicrotasks(); + cmp.exp2 = 'state2'; + fixture.detectChanges(); + flushMicrotasks(); - expect(driver.log.length).toEqual(2); - expect(count).toEqual(0); + expect(driver.log.length).toEqual(2); + expect(count).toEqual(0); - var animation2 = driver.log[1]; - var player2 = animation2['player']; - player2.onDone(() => count++); + var animation2 = driver.log[1]; + var player2 = animation2['player']; + player2.onDone(() => count++); - expect(count).toEqual(0); - player1.finish(); - expect(count).toEqual(1); - player2.finish(); - expect(count).toEqual(2); - }); - }))); + expect(count).toEqual(0); + player1.finish(); + expect(count).toEqual(1); + player2.finish(); + expect(count).toEqual(2); + })); }); describe('ng directives', () => { @@ -770,281 +745,573 @@ function declareTests({useJit}: {useJit: boolean}) { }; it('should animate when items are inserted into the list at different points', - inject( - [TestComponentBuilder, AnimationDriver], - fakeAsync((tcb: TestComponentBuilder, driver: MockAnimationDriver) => { - makeAnimationCmp( - tcb, tpl, - [ - trigger('trigger', [transition('void => *', [animate(1000)])]), - ], - (fixture: any /** TODO #9100 */) => { - var cmp = fixture.debugElement.componentInstance; - var parent = fixture.debugElement.nativeElement; - cmp.items = [0, 2, 4, 6, 8]; - fixture.detectChanges(); - flushMicrotasks(); + fakeAsync(() => { + TestBed.overrideComponent(DummyIfCmp, { + set: { + template: tpl, + animations: [trigger('trigger', [transition('void => *', [animate(1000)])])] + } + }); - expect(driver.log.length).toEqual(5); - assertParentChildContents(parent, '0 -> 2 -> 4 -> 6 -> 8'); + const driver = TestBed.get(AnimationDriver) as MockAnimationDriver; + let fixture = TestBed.createComponent(DummyIfCmp); + var cmp = fixture.debugElement.componentInstance; + var parent = fixture.debugElement.nativeElement; + cmp.items = [0, 2, 4, 6, 8]; + fixture.detectChanges(); + flushMicrotasks(); - driver.log = []; - cmp.items = [0, 1, 2, 3, 4, 5, 6, 7, 8]; - fixture.detectChanges(); - flushMicrotasks(); + expect(driver.log.length).toEqual(5); + assertParentChildContents(parent, '0 -> 2 -> 4 -> 6 -> 8'); - expect(driver.log.length).toEqual(4); - assertParentChildContents( - parent, '0 -> 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8'); - }); - }))); + driver.log = []; + cmp.items = [0, 1, 2, 3, 4, 5, 6, 7, 8]; + fixture.detectChanges(); + flushMicrotasks(); + + expect(driver.log.length).toEqual(4); + assertParentChildContents(parent, '0 -> 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8'); + })); it('should animate when items are removed + moved into the list at different points and retain DOM ordering during the animation', - inject( - [TestComponentBuilder, AnimationDriver], - fakeAsync((tcb: TestComponentBuilder, driver: MockAnimationDriver) => { - makeAnimationCmp( - tcb, tpl, - [ - trigger('trigger', [transition('* => *', [animate(1000)])]), - ], - (fixture: any /** TODO #9100 */) => { - var cmp = fixture.debugElement.componentInstance; - var parent = fixture.debugElement.nativeElement; + fakeAsync(() => { + TestBed.overrideComponent(DummyIfCmp, { + set: { + template: tpl, + animations: [trigger('trigger', [transition('* => *', [animate(1000)])])] + } + }); - cmp.items = [0, 1, 2, 3, 4]; - fixture.detectChanges(); - flushMicrotasks(); + const driver = TestBed.get(AnimationDriver) as MockAnimationDriver; + let fixture = TestBed.createComponent(DummyIfCmp); + var cmp = fixture.debugElement.componentInstance; + var parent = fixture.debugElement.nativeElement; - expect(driver.log.length).toEqual(5); - driver.log = []; + cmp.items = [0, 1, 2, 3, 4]; + fixture.detectChanges(); + flushMicrotasks(); - cmp.items = [3, 4, 0, 9]; - fixture.detectChanges(); - flushMicrotasks(); + expect(driver.log.length).toEqual(5); + driver.log = []; - // TODO (matsko): update comment below once move animations are a thing - // there are only three animations since we do - // not yet support move-based animations - expect(driver.log.length).toEqual(3); + cmp.items = [3, 4, 0, 9]; + fixture.detectChanges(); + flushMicrotasks(); - // move(~), add(+), remove(-) - // -1, -2, ~3, ~4, ~0, +9 - var rm0 = driver.log.shift(); - var rm1 = driver.log.shift(); - var in0 = driver.log.shift(); + // TODO (matsko): update comment below once move animations are a thing + // there are only three animations since we do + // not yet support move-based animations + expect(driver.log.length).toEqual(3); - // we want to assert that the DOM chain is still preserved - // until the animations are closed - assertParentChildContents(parent, '3 -> 4 -> 0 -> 9 -> 1 -> 2'); + // move(~), add(+), remove(-) + // -1, -2, ~3, ~4, ~0, +9 + var rm0 = driver.log.shift(); + var rm1 = driver.log.shift(); + var in0 = driver.log.shift(); - rm0['player'].finish(); - assertParentChildContents(parent, '3 -> 4 -> 0 -> 9 -> 2'); + // we want to assert that the DOM chain is still preserved + // until the animations are closed + assertParentChildContents(parent, '3 -> 4 -> 0 -> 9 -> 1 -> 2'); - rm1['player'].finish(); - assertParentChildContents(parent, '3 -> 4 -> 0 -> 9'); - }); - }))); - }); + rm0['player'].finish(); + assertParentChildContents(parent, '3 -> 4 -> 0 -> 9 -> 2'); - describe('[ngClass]', () => { - it('should persist ngClass class values when a remove element animation is active', - inject( - [TestComponentBuilder, AnimationDriver], - fakeAsync( - (tcb: TestComponentBuilder, driver: InnerContentTrackingAnimationDriver) => { - makeAnimationCmp( - tcb, `
`, - [ - trigger('trigger', [transition('* => void', [animate(1000)])]), - ], - (fixture: any /** TODO #9100 */) => { - var cmp = fixture.debugElement.componentInstance; - cmp.exp = true; - cmp.exp2 = 'blue'; - fixture.detectChanges(); - flushMicrotasks(); - - expect(driver.log.length).toEqual(0); - - cmp.exp = false; - fixture.detectChanges(); - flushMicrotasks(); - - var animation = driver.log.pop(); - var element = animation['element']; - (expect(element)).toHaveCssClass('blue'); - }); - }))); + rm1['player'].finish(); + assertParentChildContents(parent, '3 -> 4 -> 0 -> 9'); + })); }); }); describe('DOM order tracking', () => { if (!getDOM().supportsDOMEvents()) return; - beforeEachProviders( - () => [{provide: AnimationDriver, useClass: InnerContentTrackingAnimationDriver}]); + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [{provide: AnimationDriver, useClass: InnerContentTrackingAnimationDriver}] + }); + }); it('should evaluate all inner children and their bindings before running the animation on a parent', - inject( - [TestComponentBuilder, AnimationDriver], - fakeAsync((tcb: TestComponentBuilder, driver: InnerContentTrackingAnimationDriver) => { - makeAnimationCmp( - tcb, `
-
inner child guy
-
`, - [trigger( - 'status', - [ - state('final', style({'height': '*'})), - transition('* => *', [animate(1000)]) - ])], - (fixture: any /** TODO #9100 */) => { - tick(); + fakeAsync(() => { + TestBed.overrideComponent(DummyIfCmp, { + set: { + template: ` +
+
inner child guy
+
+ `, + animations: [trigger( + 'status', + [ + state('final', style({'height': '*'})), transition('* => *', [animate(1000)]) + ])] + } + }); - var cmp = fixture.debugElement.componentInstance; - var node = - getDOM().querySelector(fixture.debugElement.nativeElement, '.target'); - cmp.exp = true; - cmp.exp2 = true; - fixture.detectChanges(); - flushMicrotasks(); + const driver = TestBed.get(AnimationDriver) as InnerContentTrackingAnimationDriver; + let fixture = TestBed.createComponent(DummyIfCmp); + var cmp = fixture.debugElement.componentInstance; + var node = getDOM().querySelector(fixture.debugElement.nativeElement, '.target'); + cmp.exp = true; + cmp.exp2 = true; + fixture.detectChanges(); + flushMicrotasks(); - var animation = driver.log.pop(); - var player = animation['player']; - expect(player.capturedInnerText).toEqual('inner child guy'); - }); - }))); + var animation = driver.log.pop(); + var player = animation['player']; + expect(player.capturedInnerText).toEqual('inner child guy'); + })); it('should run the initialization stage after all children have been evaluated', - inject( - [TestComponentBuilder, AnimationDriver], - fakeAsync((tcb: TestComponentBuilder, driver: InnerContentTrackingAnimationDriver) => { - makeAnimationCmp( - tcb, `
-
-
inner child guy
-
`, + fakeAsync(() => { + TestBed.overrideComponent(DummyIfCmp, { + set: { + template: ` +
+
+
inner child guy
+
+ `, + animations: [trigger('status', [transition('* => *', sequence([ animate(1000, style({height: 0})), animate(1000, style({height: '*'})) - ]))])], - (fixture: any /** TODO #9100 */) => { - tick(); + ]))])] + } + }); - var cmp = fixture.debugElement.componentInstance; - cmp.exp = true; - cmp.exp2 = true; - fixture.detectChanges(); - flushMicrotasks(); - fixture.detectChanges(); + const driver = TestBed.get(AnimationDriver) as InnerContentTrackingAnimationDriver; + let fixture = TestBed.createComponent(DummyIfCmp); + var cmp = fixture.debugElement.componentInstance; + cmp.exp = true; + cmp.exp2 = true; + fixture.detectChanges(); + flushMicrotasks(); + fixture.detectChanges(); - var animation = driver.log.pop(); - var player = animation['player']; + var animation = driver.log.pop(); + var player = animation['player']; - // this is just to confirm that the player is using the parent element - expect(player.element.className).toEqual('target'); - expect(player.computedHeight).toEqual('60px'); - }); - }))); + // this is just to confirm that the player is using the parent element + expect(player.element.className).toEqual('target'); + expect(player.computedHeight).toEqual('60px'); + })); it('should not trigger animations more than once within a view that contains multiple animation triggers', - inject( - [TestComponentBuilder, AnimationDriver], - fakeAsync((tcb: TestComponentBuilder, driver: InnerContentTrackingAnimationDriver) => { - makeAnimationCmp( - tcb, `
-
`, - [ - trigger('one', [transition('* => *', [animate(1000)])]), - trigger('two', [transition('* => *', [animate(2000)])]) - ], - (fixture: any /** TODO #9100 */) => { - var cmp = fixture.debugElement.componentInstance; - cmp.exp = true; - cmp.exp2 = true; - fixture.detectChanges(); - flushMicrotasks(); + fakeAsync(() => { + TestBed.overrideComponent(DummyIfCmp, { + set: { + template: ` +
+
+ `, + animations: [ + trigger('one', [transition('* => *', [animate(1000)])]), + trigger('two', [transition('* => *', [animate(2000)])]) + ] + } + }); - expect(driver.log.length).toEqual(2); - var animation1 = driver.log.pop(); - var animation2 = driver.log.pop(); - var player1 = animation1['player']; - var player2 = animation2['player']; - expect(player1.playAttempts).toEqual(1); - expect(player2.playAttempts).toEqual(1); - }); - }))); + const driver = TestBed.get(AnimationDriver) as InnerContentTrackingAnimationDriver; + let fixture = TestBed.createComponent(DummyIfCmp); + var cmp = fixture.debugElement.componentInstance; + cmp.exp = true; + cmp.exp2 = true; + fixture.detectChanges(); + flushMicrotasks(); - it('should trigger animations when animations are detached from the page', - inject( - [TestComponentBuilder, AnimationDriver], - fakeAsync((tcb: TestComponentBuilder, driver: InnerContentTrackingAnimationDriver) => { - makeAnimationCmp( - tcb, `
`, - [ - trigger('trigger', [transition('* => void', [animate(1000)])]), - ], - (fixture: any /** TODO #9100 */) => { - var cmp = fixture.debugElement.componentInstance; - cmp.exp = true; - fixture.detectChanges(); - flushMicrotasks(); + expect(driver.log.length).toEqual(2); + var animation1 = driver.log.pop(); + var animation2 = driver.log.pop(); + var player1 = animation1['player']; + var player2 = animation2['player']; + expect(player1.playAttempts).toEqual(1); + expect(player2.playAttempts).toEqual(1); + })); - expect(driver.log.length).toEqual(0); + it('should trigger animations when animations are detached from the page', fakeAsync(() => { + TestBed.overrideComponent(DummyIfCmp, { + set: { + template: ` +
+ `, + animations: [ + trigger('trigger', [transition('* => void', [animate(1000)])]), + ] + } + }); - cmp.exp = false; - fixture.detectChanges(); - flushMicrotasks(); + const driver = TestBed.get(AnimationDriver) as InnerContentTrackingAnimationDriver; + let fixture = TestBed.createComponent(DummyIfCmp); + var cmp = fixture.debugElement.componentInstance; + cmp.exp = true; + fixture.detectChanges(); + flushMicrotasks(); - expect(driver.log.length).toEqual(1); - var animation = driver.log.pop(); - var player = animation['player']; - expect(player.playAttempts).toEqual(1); - }); - }))); + expect(driver.log.length).toEqual(0); + + cmp.exp = false; + fixture.detectChanges(); + flushMicrotasks(); + + expect(driver.log.length).toEqual(1); + var animation = driver.log.pop(); + var player = animation['player']; + expect(player.playAttempts).toEqual(1); + })); + }); + + describe('animation output events', () => { + it('should fire the associated animation output expression when the animation starts even if no animation is fired', + () => { + TestBed.overrideComponent(DummyIfCmp, { + set: { + template: ` +
+ `, + animations: [ + trigger('trigger', [transition('one => two', [animate(1000)])]), + ] + } + }); + + const driver = TestBed.get(AnimationDriver) as InnerContentTrackingAnimationDriver; + let fixture = TestBed.createComponent(DummyIfCmp); + var isAnimationRunning = false; + var calls = 0; + var cmp = fixture.debugElement.componentInstance; + cmp.callback = (e: any) => { + isAnimationRunning = e['running']; + calls++; + }; + + cmp.exp = 'one'; + fixture.detectChanges(); + + expect(calls).toEqual(1); + expect(isAnimationRunning).toEqual(false); + + cmp.exp = 'two'; + fixture.detectChanges(); + + expect(calls).toEqual(2); + expect(isAnimationRunning).toEqual(true); + }); + + it('should fire the associated animation output expression when the animation ends even if no animation is fired', + fakeAsync(() => { + TestBed.overrideComponent(DummyIfCmp, { + set: { + template: ` +
+ `, + animations: [ + trigger('trigger', [transition('one => two', [animate(1000)])]), + ] + } + }); + + const driver = TestBed.get(AnimationDriver) as InnerContentTrackingAnimationDriver; + let fixture = TestBed.createComponent(DummyIfCmp); + var isAnimationRunning = false; + var calls = 0; + var cmp = fixture.debugElement.componentInstance; + cmp.callback = (e: any) => { + isAnimationRunning = e['running']; + calls++; + }; + cmp.exp = 'one'; + fixture.detectChanges(); + + expect(calls).toEqual(0); + flushMicrotasks(); + + expect(calls).toEqual(1); + expect(isAnimationRunning).toEqual(false); + + cmp.exp = 'two'; + fixture.detectChanges(); + + expect(calls).toEqual(1); + + var player = driver.log.shift()['player']; + player.finish(); + + expect(calls).toEqual(2); + expect(isAnimationRunning).toEqual(true); + })); + + it('should emit the `fromState` and `toState` within the event data when a callback is fired', + fakeAsync(() => { + TestBed.overrideComponent(DummyIfCmp, { + set: { + template: ` +
+ `, + animations: [ + trigger('trigger', [transition('one => two', [animate(1000)])]), + ] + } + }); + + const driver = TestBed.get(AnimationDriver) as InnerContentTrackingAnimationDriver; + let fixture = TestBed.createComponent(DummyIfCmp); + var eventData: any = {}; + var cmp = fixture.debugElement.componentInstance; + cmp.callback = (e: any) => { eventData = e; }; + cmp.exp = 'one'; + fixture.detectChanges(); + flushMicrotasks(); + expect(eventData['fromState']).toEqual('void'); + expect(eventData['toState']).toEqual('one'); + + cmp.exp = 'two'; + fixture.detectChanges(); + flushMicrotasks(); + expect(eventData['fromState']).toEqual('one'); + expect(eventData['toState']).toEqual('two'); + })); + + it('should throw an error if an animation output is referenced is not defined within the component', + () => { + TestBed.overrideComponent(DummyIfCmp, { + set: { + template: ` +
+ ` + } + }); + + var message = ''; + try { + let fixture = TestBed.createComponent(DummyIfCmp); + fixture.detectChanges(); + } catch (e) { + message = e.message; + } + + expect(message).toMatch( + /- Couldn't find the corresponding animation trigger definition 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', + () => { + TestBed.overrideComponent(DummyIfCmp, { + set: { + template: ` +
+ `, + animations: [trigger('trigger', [transition('one => two', [animate(1000)])])] + } + }); + + var message = ''; + try { + let fixture = TestBed.createComponent(DummyIfCmp); + fixture.detectChanges(); + } catch (e) { + message = e.message; + } + + expect(message).toMatch( + /- 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', () => { + TestBed.overrideComponent(DummyIfCmp, { + set: { + template: ` +
+ `, + animations: [trigger('trigger', [transition('one => two', [animate(1000)])])] + } + }); + + var message = ''; + try { + let fixture = TestBed.createComponent(DummyIfCmp); + fixture.detectChanges(); + } catch (e) { + message = e.message; + } + + expect(message).toMatch( + /The provided animation output phase value "jump" for "@trigger" is not supported \(use start or done\)/); + }); + + it('should throw an error if the animation output event phase value is missing', () => { + TestBed.overrideComponent(DummyIfCmp, { + set: { + template: ` +
+ `, + animations: [trigger('trigger', [transition('one => two', [animate(1000)])])] + } + }); + + var message = ''; + try { + let fixture = TestBed.createComponent(DummyIfCmp); + fixture.detectChanges(); + } catch (e) { + message = e.message; + } + + expect(message).toMatch( + /The animation trigger output event \(@trigger\) is missing its phase value name \(start or done are currently supported\)/); + }); + + it('should throw an error when an animation output is referenced but the host-level animation binding is missing', + () => { + TestBed.overrideComponent( + DummyLoadingCmp, {set: {host: {'(@trigger.done)': 'callback($event)'}}}); + + var message = ''; + try { + let fixture = TestBed.createComponent(DummyLoadingCmp); + fixture.detectChanges(); + } catch (e) { + message = e.message; + } + + expect(message).toMatch( + /Couldn't find the corresponding host-level animation trigger definition for \(@trigger\)/); + }); + + it('should allow host and element-level animation bindings to be defined on the same tag/component', + fakeAsync(() => { + TestBed.overrideComponent(DummyLoadingCmp, { + set: { + host: { + '[attr.title]': 'exp', + '[@loading]': 'exp', + '(@loading.start)': 'callback($event)' + }, + animations: [trigger('loading', [transition('* => *', [animate(1000)])])] + } + }); + TestBed.overrideComponent(DummyIfCmp, { + set: { + template: ` + + `, + animations: [trigger('trigger', [transition('* => *', [animate(1000)])])] + } + }); + + const driver = TestBed.get(AnimationDriver) as InnerContentTrackingAnimationDriver; + var ifCalls = 0; + var loadingCalls = 0; + let fixture = TestBed.createComponent(DummyIfCmp); + var ifCmp = fixture.debugElement.componentInstance; + var loadingCmp = fixture.debugElement.childNodes[1].componentInstance; + + ifCmp.callback = (e: any) => ifCalls++; + loadingCmp.callback = (e: any) => loadingCalls++; + + expect(ifCalls).toEqual(0); + expect(loadingCalls).toEqual(0); + + ifCmp.exp = 'one'; + loadingCmp.exp = 'one'; + fixture.detectChanges(); + flushMicrotasks(); + + expect(ifCalls).toEqual(1); + expect(loadingCalls).toEqual(1); + + ifCmp.exp = 'two'; + loadingCmp.exp = 'two'; + fixture.detectChanges(); + flushMicrotasks(); + + expect(ifCalls).toEqual(2); + expect(loadingCalls).toEqual(2); + })); + }); + + describe('ng directives', () => { + describe('[ngClass]', () => { + it('should persist ngClass class values when a remove element animation is active', + fakeAsync(() => { + TestBed.overrideComponent(DummyIfCmp, { + set: { + template: ` +
+ `, + animations: [trigger('trigger', [transition('* => void', [animate(1000)])])] + } + }); + + const driver = TestBed.get(AnimationDriver) as InnerContentTrackingAnimationDriver; + let fixture = TestBed.createComponent(DummyIfCmp); + var cmp = fixture.debugElement.componentInstance; + cmp.exp = true; + cmp.exp2 = 'blue'; + fixture.detectChanges(); + flushMicrotasks(); + + expect(driver.log.length).toEqual(0); + + cmp.exp = false; + fixture.detectChanges(); + flushMicrotasks(); + + var animation = driver.log.pop(); + var element = animation['element']; + (expect(element)).toHaveCssClass('blue'); + })); + }); }); describe('animation states', () => { it('should throw an error when an animation is referenced that isn\'t defined within the component annotation', - inject( - [TestComponentBuilder, AnimationDriver], - fakeAsync((tcb: TestComponentBuilder, driver: MockAnimationDriver) => { - makeAnimationCmp( - tcb, '
', [], - () => { - throw new BaseException( - 'Error: expected animations for DummyIfCmp to throw an error within this spec'); - }, - (e: any) => { - const message = e.message; - expect(message).toMatch( - /Animation parsing for DummyIfCmp has failed due to the following errors:/); - expect(message).toMatch(/- couldn't find an animation entry for status/); - }); - }))); + () => { + TestBed.overrideComponent(DummyIfCmp, { + set: { + template: ` +
+ `, + animations: [] + } + }); - it('should be permitted to be registered on the host element', - inject( - [TestComponentBuilder, AnimationDriver], - fakeAsync((tcb: TestComponentBuilder, driver: MockAnimationDriver) => { - tcb = tcb.overrideAnimations(DummyLoadingCmp, [trigger('loading', [ - state('final', style({'background': 'grey'})), - transition('* => final', [animate(1000)]) - ])]); - tcb.createAsync(DummyLoadingCmp).then(fixture => { - var cmp = fixture.debugElement.componentInstance; - cmp.exp = 'final'; - fixture.detectChanges(); - flushMicrotasks(); + var failureMessage = ''; + try { + let fixture = TestBed.createComponent(DummyLoadingCmp); + } catch (e) { + failureMessage = e.message; + } - var animation = driver.log.pop(); - var keyframes = animation['keyframeLookup']; - expect(keyframes[1]).toEqual([1, {'background': 'grey'}]); - }); - tick(); - }))); + 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/); + }); + it('should be permitted to be registered on the host element', fakeAsync(() => { + TestBed.overrideComponent(DummyLoadingCmp, { + set: { + host: {'[@loading]': 'exp'}, + animations: [trigger( + 'loading', + [ + state('final', style({'background': 'grey'})), + transition('* => final', [animate(1000)]) + ])] + } + }); + + const driver = TestBed.get(AnimationDriver) as MockAnimationDriver; + let fixture = TestBed.createComponent(DummyLoadingCmp); + var cmp = fixture.debugElement.componentInstance; + cmp.exp = 'final'; + fixture.detectChanges(); + flushMicrotasks(); + + var animation = driver.log.pop(); + var kf = animation['keyframeLookup']; + expect(kf[1]).toEqual([1, {'background': 'grey'}]); + })); it('should throw an error if a host-level referenced animation is not defined within the component', () => { @@ -1052,299 +1319,294 @@ function declareTests({useJit}: {useJit: boolean}) { var failureMessage = ''; try { - inject([AnimationDriver], (driver: AnimationDriver) => {})(); + const driver = TestBed.get(AnimationDriver) as MockAnimationDriver; } catch (e) { 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', - inject( - [TestComponentBuilder, AnimationDriver], - fakeAsync((tcb: TestComponentBuilder, driver: MockAnimationDriver) => { - makeAnimationCmp( - tcb, '
', - [trigger( - 'status', - [ - state('final', style({'top': '100px'})), - transition('* => final', [animate(1000)]) - ])], - (fixture: any /** TODO #9100 */) => { - tick(); + fakeAsync(() => { + TestBed.overrideComponent(DummyIfCmp, { + set: { + template: ` +
+ `, + animations: [trigger( + 'status', + [ + state('final', style({'top': '100px'})), + transition('* => final', [animate(1000)]) + ])] + } + }); - var cmp = fixture.debugElement.componentInstance; - var node = - getDOM().querySelector(fixture.debugElement.nativeElement, '.target'); - cmp.exp = 'final'; - fixture.detectChanges(); - flushMicrotasks(); + const driver = TestBed.get(AnimationDriver) as MockAnimationDriver; + let fixture = TestBed.createComponent(DummyIfCmp); + var cmp = fixture.debugElement.componentInstance; + var node = getDOM().querySelector(fixture.debugElement.nativeElement, '.target'); + cmp.exp = 'final'; + fixture.detectChanges(); + flushMicrotasks(); - var animation = driver.log[0]; - var player = animation['player']; - player.finish(); + var animation = driver.log[0]; + var player = animation['player']; + player.finish(); - expect(getDOM().getStyle(node, 'top')).toEqual('100px'); - }); - }))); + expect(getDOM().getStyle(node, 'top')).toEqual('100px'); + })); it('should animate to and retain the default animation state styles once the animation is complete if defined', - inject( - [TestComponentBuilder, AnimationDriver], - fakeAsync((tcb: TestComponentBuilder, driver: MockAnimationDriver) => { - makeAnimationCmp( - tcb, '
', - [trigger( - 'status', - [ - state(DEFAULT_STATE, style({'background': 'grey'})), - state('green', style({'background': 'green'})), - state('red', style({'background': 'red'})), - transition('* => *', [animate(1000)]) - ])], - (fixture: any /** TODO #9100 */) => { - tick(); + fakeAsync(() => { + TestBed.overrideComponent(DummyIfCmp, { + set: { + template: ` +
+ `, + animations: [trigger( + 'status', + [ + state(DEFAULT_STATE, style({'background': 'grey'})), + state('green', style({'background': 'green'})), + state('red', style({'background': 'red'})), + transition('* => *', [animate(1000)]) + ])] + } + }); - var cmp = fixture.debugElement.componentInstance; - var node = - getDOM().querySelector(fixture.debugElement.nativeElement, '.target'); - cmp.exp = 'green'; - fixture.detectChanges(); - flushMicrotasks(); + const driver = TestBed.get(AnimationDriver) as MockAnimationDriver; + let fixture = TestBed.createComponent(DummyIfCmp); + var cmp = fixture.debugElement.componentInstance; + var node = getDOM().querySelector(fixture.debugElement.nativeElement, '.target'); + cmp.exp = 'green'; + fixture.detectChanges(); + flushMicrotasks(); - var animation = driver.log.pop(); - var keyframes = animation['keyframeLookup']; - expect(keyframes[1]).toEqual([1, {'background': 'green'}]); + var animation = driver.log.pop(); + var kf = animation['keyframeLookup']; + expect(kf[1]).toEqual([1, {'background': 'green'}]); - cmp.exp = 'blue'; - fixture.detectChanges(); - flushMicrotasks(); + cmp.exp = 'blue'; + fixture.detectChanges(); + flushMicrotasks(); - animation = driver.log.pop(); - keyframes = animation['keyframeLookup']; - expect(keyframes[0]).toEqual([0, {'background': 'green'}]); - expect(keyframes[1]).toEqual([1, {'background': 'grey'}]); + animation = driver.log.pop(); + kf = animation['keyframeLookup']; + expect(kf[0]).toEqual([0, {'background': 'green'}]); + expect(kf[1]).toEqual([1, {'background': 'grey'}]); - cmp.exp = 'red'; - fixture.detectChanges(); - flushMicrotasks(); + cmp.exp = 'red'; + fixture.detectChanges(); + flushMicrotasks(); - animation = driver.log.pop(); - keyframes = animation['keyframeLookup']; - expect(keyframes[0]).toEqual([0, {'background': 'grey'}]); - expect(keyframes[1]).toEqual([1, {'background': 'red'}]); + animation = driver.log.pop(); + kf = animation['keyframeLookup']; + expect(kf[0]).toEqual([0, {'background': 'grey'}]); + expect(kf[1]).toEqual([1, {'background': 'red'}]); - cmp.exp = 'orange'; - fixture.detectChanges(); - flushMicrotasks(); + cmp.exp = 'orange'; + fixture.detectChanges(); + flushMicrotasks(); - animation = driver.log.pop(); - keyframes = animation['keyframeLookup']; - expect(keyframes[0]).toEqual([0, {'background': 'red'}]); - expect(keyframes[1]).toEqual([1, {'background': 'grey'}]); - }); - }))); + animation = driver.log.pop(); + kf = animation['keyframeLookup']; + expect(kf[0]).toEqual([0, {'background': 'red'}]); + expect(kf[1]).toEqual([1, {'background': 'grey'}]); + })); it('should seed in the origin animation state styles into the first animation step', - inject( - [TestComponentBuilder, AnimationDriver], - fakeAsync((tcb: TestComponentBuilder, driver: MockAnimationDriver) => { - makeAnimationCmp( - tcb, '
', - [trigger( - 'status', - [ - state('void', style({'height': '100px'})), - transition('* => *', [animate(1000)]) - ])], - (fixture: any /** TODO #9100 */) => { - tick(); + fakeAsync(() => { + TestBed.overrideComponent(DummyIfCmp, { + set: { + template: ` +
+ `, + animations: [trigger( + 'status', + [ + state('void', style({'height': '100px'})), + transition('* => *', [animate(1000)]) + ])] + } + }); - var cmp = fixture.debugElement.componentInstance; - var node = - getDOM().querySelector(fixture.debugElement.nativeElement, '.target'); - cmp.exp = 'final'; - fixture.detectChanges(); - flushMicrotasks(); + const driver = TestBed.get(AnimationDriver) as MockAnimationDriver; + let fixture = TestBed.createComponent(DummyIfCmp); + var cmp = fixture.debugElement.componentInstance; + var node = getDOM().querySelector(fixture.debugElement.nativeElement, '.target'); + cmp.exp = 'final'; + fixture.detectChanges(); + flushMicrotasks(); - var animation = driver.log[0]; - expect(animation['startingStyles']).toEqual({'height': '100px'}); - }); - }))); + var animation = driver.log[0]; + expect(animation['startingStyles']).toEqual({'height': '100px'}); + })); it('should perform a state change even if there is no transition that is found', - inject( - [TestComponentBuilder, AnimationDriver], - fakeAsync((tcb: TestComponentBuilder, driver: MockAnimationDriver) => { - makeAnimationCmp( - tcb, '
', - [trigger( - 'status', - [ - state('void', style({'width': '0px'})), - state('final', style({'width': '100px'})), - ])], - (fixture: any /** TODO #9100 */) => { - tick(); + fakeAsync(() => { + TestBed.overrideComponent(DummyIfCmp, { + set: { + template: ` +
+ `, + animations: [trigger( + 'status', + [ + state('void', style({'width': '0px'})), + state('final', style({'width': '100px'})), + ])] + } + }); - var cmp = fixture.debugElement.componentInstance; - var node = - getDOM().querySelector(fixture.debugElement.nativeElement, '.target'); - cmp.exp = 'final'; - fixture.detectChanges(); - flushMicrotasks(); + const driver = TestBed.get(AnimationDriver) as MockAnimationDriver; + let fixture = TestBed.createComponent(DummyIfCmp); + var cmp = fixture.debugElement.componentInstance; + var node = getDOM().querySelector(fixture.debugElement.nativeElement, '.target'); + cmp.exp = 'final'; + fixture.detectChanges(); + flushMicrotasks(); - expect(driver.log.length).toEqual(0); - flushMicrotasks(); + expect(driver.log.length).toEqual(0); + flushMicrotasks(); - expect(getDOM().getStyle(node, 'width')).toEqual('100px'); - }); - }))); + expect(getDOM().getStyle(node, 'width')).toEqual('100px'); + })); - it('should allow multiple states to be defined with the same styles', - inject( - [TestComponentBuilder, AnimationDriver], - fakeAsync((tcb: TestComponentBuilder, driver: MockAnimationDriver) => { - makeAnimationCmp( - tcb, '
', - [trigger( - 'status', - [ - state('a, c', style({'height': '100px'})), - state('b, d', style({'width': '100px'})), - ])], - (fixture: any /** TODO #9100 */) => { - tick(); + it('should allow multiple states to be defined with the same styles', fakeAsync(() => { + TestBed.overrideComponent(DummyIfCmp, { + set: { + template: ` +
+ `, + animations: [trigger( + 'status', + [ + state('a, c', style({'height': '100px'})), + state('b, d', style({'width': '100px'})) + ])] + } + }); - var cmp = fixture.debugElement.componentInstance; - var node = - getDOM().querySelector(fixture.debugElement.nativeElement, '.target'); + const driver = TestBed.get(AnimationDriver) as MockAnimationDriver; + let fixture = TestBed.createComponent(DummyIfCmp); + var cmp = fixture.debugElement.componentInstance; + var node = getDOM().querySelector(fixture.debugElement.nativeElement, '.target'); - cmp.exp = 'a'; - fixture.detectChanges(); - flushMicrotasks(); - flushMicrotasks(); + cmp.exp = 'a'; + fixture.detectChanges(); + flushMicrotasks(); - expect(getDOM().getStyle(node, 'height')).toEqual('100px'); - expect(getDOM().getStyle(node, 'width')).not.toEqual('100px'); + expect(getDOM().getStyle(node, 'height')).toEqual('100px'); + expect(getDOM().getStyle(node, 'width')).not.toEqual('100px'); - cmp.exp = 'b'; - fixture.detectChanges(); - flushMicrotasks(); - flushMicrotasks(); + cmp.exp = 'b'; + fixture.detectChanges(); + flushMicrotasks(); - expect(getDOM().getStyle(node, 'height')).not.toEqual('100px'); - expect(getDOM().getStyle(node, 'width')).toEqual('100px'); + expect(getDOM().getStyle(node, 'height')).not.toEqual('100px'); + expect(getDOM().getStyle(node, 'width')).toEqual('100px'); - cmp.exp = 'c'; - fixture.detectChanges(); - flushMicrotasks(); - flushMicrotasks(); + cmp.exp = 'c'; + fixture.detectChanges(); + flushMicrotasks(); - expect(getDOM().getStyle(node, 'height')).toEqual('100px'); - expect(getDOM().getStyle(node, 'width')).not.toEqual('100px'); + expect(getDOM().getStyle(node, 'height')).toEqual('100px'); + expect(getDOM().getStyle(node, 'width')).not.toEqual('100px'); - cmp.exp = 'd'; - fixture.detectChanges(); - flushMicrotasks(); - flushMicrotasks(); + cmp.exp = 'd'; + fixture.detectChanges(); + flushMicrotasks(); - expect(getDOM().getStyle(node, 'height')).not.toEqual('100px'); - expect(getDOM().getStyle(node, 'width')).toEqual('100px'); + expect(getDOM().getStyle(node, 'height')).not.toEqual('100px'); + expect(getDOM().getStyle(node, 'width')).toEqual('100px'); - cmp.exp = 'e'; - fixture.detectChanges(); - flushMicrotasks(); - flushMicrotasks(); + cmp.exp = 'e'; + fixture.detectChanges(); + flushMicrotasks(); - expect(getDOM().getStyle(node, 'height')).not.toEqual('100px'); - expect(getDOM().getStyle(node, 'width')).not.toEqual('100px'); - }); - }))); + expect(getDOM().getStyle(node, 'height')).not.toEqual('100px'); + expect(getDOM().getStyle(node, 'width')).not.toEqual('100px'); + })); - it('should allow multiple transitions to be defined with the same sequence', - inject( - [TestComponentBuilder, AnimationDriver], - fakeAsync((tcb: TestComponentBuilder, driver: MockAnimationDriver) => { - makeAnimationCmp( - tcb, '
', - [trigger( - 'status', - [ - transition('a => b, b => c', [animate(1000)]), - transition('* => *', [animate(300)]) - ])], - (fixture: any /** TODO #9100 */) => { - tick(); + it('should allow multiple transitions to be defined with the same sequence', fakeAsync(() => { + TestBed.overrideComponent(DummyIfCmp, { + set: { + template: ` +
+ `, + animations: [trigger( + 'status', + [ + transition('a => b, b => c', [animate(1000)]), + transition('* => *', [animate(300)]) + ])] + } + }); - var cmp = fixture.debugElement.componentInstance; - var node = - getDOM().querySelector(fixture.debugElement.nativeElement, '.target'); + const driver = TestBed.get(AnimationDriver) as MockAnimationDriver; + let fixture = TestBed.createComponent(DummyIfCmp); + var cmp = fixture.debugElement.componentInstance; + var node = getDOM().querySelector(fixture.debugElement.nativeElement, '.target'); - cmp.exp = 'a'; - fixture.detectChanges(); - flushMicrotasks(); + cmp.exp = 'a'; + fixture.detectChanges(); + flushMicrotasks(); - expect(driver.log.pop()['duration']).toEqual(300); + expect(driver.log.pop()['duration']).toEqual(300); - cmp.exp = 'b'; - fixture.detectChanges(); - flushMicrotasks(); + cmp.exp = 'b'; + fixture.detectChanges(); + flushMicrotasks(); - expect(driver.log.pop()['duration']).toEqual(1000); + expect(driver.log.pop()['duration']).toEqual(1000); - cmp.exp = 'c'; - fixture.detectChanges(); - flushMicrotasks(); + cmp.exp = 'c'; + fixture.detectChanges(); + flushMicrotasks(); - expect(driver.log.pop()['duration']).toEqual(1000); + expect(driver.log.pop()['duration']).toEqual(1000); - cmp.exp = 'd'; - fixture.detectChanges(); - flushMicrotasks(); + cmp.exp = 'd'; + fixture.detectChanges(); + flushMicrotasks(); - expect(driver.log.pop()['duration']).toEqual(300); - }); - }))); + expect(driver.log.pop()['duration']).toEqual(300); + })); it('should balance the animation with the origin/destination styles as keyframe animation properties', - inject( - [TestComponentBuilder, AnimationDriver], - fakeAsync((tcb: TestComponentBuilder, driver: MockAnimationDriver) => { - makeAnimationCmp( - tcb, '
', - [trigger( - 'status', - [ - state('void', style({'height': '100px', 'opacity': 0})), - state('final', style({'height': '333px', 'width': '200px'})), - transition('void => final', [animate(1000)]) - ])], - (fixture: any /** TODO #9100 */) => { - tick(); + () => { + TestBed.overrideComponent(DummyIfCmp, { + set: { + template: ` +
+ `, + animations: [trigger( + 'status', + [ + state('void', style({'height': '100px', 'opacity': 0})), + state('final', style({'height': '333px', 'width': '200px'})), + transition('void => final', [animate(1000)]) + ])] + } + }); - var cmp = fixture.debugElement.componentInstance; - var node = - getDOM().querySelector(fixture.debugElement.nativeElement, '.target'); + const driver = TestBed.get(AnimationDriver) as MockAnimationDriver; + let fixture = TestBed.createComponent(DummyIfCmp); + var cmp = fixture.debugElement.componentInstance; + var node = getDOM().querySelector(fixture.debugElement.nativeElement, '.target'); - cmp.exp = 'final'; - fixture.detectChanges(); - flushMicrotasks(); + cmp.exp = 'final'; + fixture.detectChanges(); - var animation = driver.log.pop(); - var keyframes = animation['keyframeLookup']; + var animation = driver.log.pop(); + var kf = animation['keyframeLookup']; - expect(keyframes[0]).toEqual([ - 0, {'height': '100px', 'opacity': 0, 'width': AUTO_STYLE} - ]); + expect(kf[0]).toEqual([0, {'height': '100px', 'opacity': 0, 'width': AUTO_STYLE}]); - expect(keyframes[1]).toEqual([ - 1, {'height': '333px', 'opacity': AUTO_STYLE, 'width': '200px'} - ]); - }); - }))); + expect(kf[1]).toEqual([1, {'height': '333px', 'opacity': AUTO_STYLE, 'width': '200px'}]); + }); }); }); } @@ -1387,12 +1649,12 @@ class DummyIfCmp { exp = false; exp2 = false; items = [0, 1, 2, 3, 4]; + callback: Function = () => {}; } @Component({ - selector: 'if-cmp', + selector: 'dummy-loading-cmp', host: {'[@loading]': 'exp'}, - directives: [NgIf], animations: [trigger('loading', [])], template: `
loading...
@@ -1400,4 +1662,20 @@ class DummyIfCmp { }) class DummyLoadingCmp { exp = false; + callback = () => {}; +} + +@Component({ + selector: 'if-cmp', + host: { + '(@loading.start)': 'callback($event,"start")', + '(@loading.done)': 'callback($event,"done")' + }, + template: ` +
loading...
+ ` +}) +class BrokenDummyLoadingCmp { + exp = false; + callback = () => {}; } diff --git a/modules/playground/src/animate/app/animate-app.ts b/modules/playground/src/animate/app/animate-app.ts index a6ead6bc39..52a2d1e186 100644 --- a/modules/playground/src/animate/app/animate-app.ts +++ b/modules/playground/src/animate/app/animate-app.ts @@ -10,7 +10,9 @@ import {Component, animate, group, keyframes, sequence, state, style, transition @Component({ host: { - '[@backgroundAnimation]': 'bgStatus' + '[@backgroundAnimation]': 'bgStatus', + '(@backgroundAnimation.start)': 'bgStatusChanged($event, "started")', + '(@backgroundAnimation.done)': 'bgStatusChanged($event, "completed")' }, selector: 'animate-app', styleUrls: ['css/animate-app.css'], @@ -80,6 +82,10 @@ export class AnimateApp { this.items[Math.floor(Math.random() * this.items.length)] = 99; } + bgStatusChanged(data: {[key: string]: any}, phase: string) { + alert(`backgroundAnimation has ${phase} from ${data['fromState']} to ${data['toState']}`); + } + get state() { return this._state; } set state(s) { this._state = s;