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
This commit is contained in:
Matias Niemelä 2016-08-22 17:18:25 -07:00 committed by Kara
parent ca41b4f5ff
commit 45e8e73670
15 changed files with 1598 additions and 1125 deletions

View File

@ -78,6 +78,8 @@ export type AnimationKeyframe = t.AnimationKeyframe;
export var AnimationKeyframe: typeof t.AnimationKeyframe = r.AnimationKeyframe; export var AnimationKeyframe: typeof t.AnimationKeyframe = r.AnimationKeyframe;
export type AnimationStyles = t.AnimationStyles; export type AnimationStyles = t.AnimationStyles;
export var AnimationStyles: typeof t.AnimationStyles = r.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 ANY_STATE = r.ANY_STATE;
export var DEFAULT_STATE = r.DEFAULT_STATE; export var DEFAULT_STATE = r.DEFAULT_STATE;
export var EMPTY_STATE = r.EMPTY_STATE; export var EMPTY_STATE = r.EMPTY_STATE;

View File

@ -8,7 +8,7 @@
import {AUTO_STYLE, BaseException} from '@angular/core'; 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 {CompileDirectiveMetadata} from '../compile_metadata';
import {StringMapWrapper} from '../facade/collection'; import {StringMapWrapper} from '../facade/collection';
import {isBlank, isPresent} from '../facade/lang'; 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 * as t from '../template_parser/template_ast';
import {AnimationAst, AnimationAstVisitor, AnimationEntryAst, AnimationGroupAst, AnimationKeyframeAst, AnimationSequenceAst, AnimationStateAst, AnimationStateDeclarationAst, AnimationStateTransitionAst, AnimationStepAst, AnimationStylesAst} from './animation_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<CompileDirectiveMetadata, CompiledAnimation[]>(); const animationCompilationCache =
new Map<CompileDirectiveMetadata, CompiledAnimationTriggerResult[]>();
export class CompiledAnimation { export class CompiledAnimationTriggerResult {
constructor( constructor(
public name: string, public statesMapStatement: o.Statement, public name: string, public statesMapStatement: o.Statement,
public statesVariableName: string, public fnStatement: o.Statement, public statesVariableName: string, public fnStatement: o.Statement,
public fnVariable: o.Expression) {} public fnVariable: o.Expression) {}
} }
export class CompiledComponentAnimationResult {
constructor(
public outputs: AnimationOutput[], public triggers: CompiledAnimationTriggerResult[]) {}
}
export class AnimationCompiler { export class AnimationCompiler {
compileComponent(component: CompileDirectiveMetadata, template: t.TemplateAst[]): compileComponent(component: CompileDirectiveMetadata, template: t.TemplateAst[]):
CompiledAnimation[] { CompiledComponentAnimationResult {
var compiledAnimations: CompiledAnimation[] = []; var compiledAnimations: CompiledAnimationTriggerResult[] = [];
var groupedErrors: string[] = []; var groupedErrors: string[] = [];
var triggerLookup: {[key: string]: CompiledAnimation} = {}; var triggerLookup: {[key: string]: CompiledAnimationTriggerResult} = {};
var componentName = component.type.name; var componentName = component.type.name;
component.template.animations.forEach(entry => { component.template.animations.forEach(entry => {
@ -59,9 +65,8 @@ export class AnimationCompiler {
} }
}); });
_validateAnimationProperties(compiledAnimations, template).forEach(entry => { var validatedProperties = _validateAnimationProperties(compiledAnimations, template);
groupedErrors.push(entry.msg); validatedProperties.errors.forEach(error => { groupedErrors.push(error.msg); });
});
if (groupedErrors.length > 0) { if (groupedErrors.length > 0) {
var errorMessageStr = var errorMessageStr =
@ -71,7 +76,7 @@ export class AnimationCompiler {
} }
animationCompilationCache.set(component, compiledAnimations); animationCompilationCache.set(component, compiledAnimations);
return compiledAnimations; return new CompiledComponentAnimationResult(validatedProperties.outputs, compiledAnimations);
} }
} }
@ -292,12 +297,13 @@ class _AnimationBuilder implements AnimationAstVisitor {
.toStmt()])]) .toStmt()])])
.toStmt()); .toStmt());
statements.push(_ANIMATION_FACTORY_VIEW_VAR statements.push(
_ANIMATION_FACTORY_VIEW_VAR
.callMethod( .callMethod(
'queueAnimation', 'queueAnimation',
[ [
_ANIMATION_FACTORY_ELEMENT_VAR, o.literal(this.animationName), _ANIMATION_FACTORY_ELEMENT_VAR, o.literal(this.animationName),
_ANIMATION_PLAYER_VAR _ANIMATION_PLAYER_VAR, _ANIMATION_CURRENT_STATE_VAR, _ANIMATION_NEXT_STATE_VAR
]) ])
.toStmt()); .toStmt());
@ -313,7 +319,7 @@ class _AnimationBuilder implements AnimationAstVisitor {
statements); statements);
} }
build(ast: AnimationAst): CompiledAnimation { build(ast: AnimationAst): CompiledAnimationTriggerResult {
var context = new _AnimationBuilderContext(); var context = new _AnimationBuilderContext();
var fnStatement = ast.visit(this, context).toDeclStmt(this._fnVarName); var fnStatement = ast.visit(this, context).toDeclStmt(this._fnVarName);
var fnVariable = o.variable(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(); var compiledStatesMapExpr = this._statesMapVar.set(o.literalMap(lookupMap)).toDeclStmt();
return new CompiledAnimation( return new CompiledAnimationTriggerResult(
this.animationName, compiledStatesMapExpr, this._statesMapVarName, fnStatement, fnVariable); this.animationName, compiledStatesMapExpr, this._statesMapVarName, fnStatement, fnVariable);
} }
} }
@ -385,60 +391,92 @@ function _getStylesArray(obj: any): {[key: string]: any}[] {
} }
function _validateAnimationProperties( function _validateAnimationProperties(
compiledAnimations: CompiledAnimation[], template: t.TemplateAst[]): AnimationParseError[] { compiledAnimations: CompiledAnimationTriggerResult[],
template: t.TemplateAst[]): AnimationPropertyValidationOutput {
var visitor = new _AnimationTemplatePropertyVisitor(compiledAnimations); var visitor = new _AnimationTemplatePropertyVisitor(compiledAnimations);
t.templateVisitAll(visitor, template); 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 { class _AnimationTemplatePropertyVisitor implements t.TemplateAstVisitor {
private _animationRegistry: {[key: string]: boolean}; private _animationRegistry: {[key: string]: boolean};
public errors: AnimationParseError[] = []; public errors: AnimationParseError[] = [];
public outputs: AnimationOutput[] = [];
constructor(animations: CompiledAnimation[]) { constructor(animations: CompiledAnimationTriggerResult[]) {
this._animationRegistry = this._buildCompileAnimationLookup(animations); this._animationRegistry = this._buildCompileAnimationLookup(animations);
} }
private _buildCompileAnimationLookup(animations: CompiledAnimation[]): {[key: string]: boolean} { private _buildCompileAnimationLookup(animations: CompiledAnimationTriggerResult[]):
{[key: string]: boolean} {
var map: {[key: string]: boolean} = {}; var map: {[key: string]: boolean} = {};
animations.forEach(entry => { map[entry.name] = true; }); animations.forEach(entry => { map[entry.name] = true; });
return map; return map;
} }
visitElement(ast: t.ElementAst, ctx: any): any { private _validateAnimationInputOutputPairs(
var inputAsts: t.BoundElementPropertyAst[] = ast.inputs; inputAsts: t.BoundElementPropertyAst[], outputAsts: t.BoundEventAst[],
var componentAnimationRegistry = this._animationRegistry; animationRegistry: {[key: string]: any}, isHostLevel: boolean): void {
var detectedAnimationInputs: {[key: string]: boolean} = {};
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);
}
}
inputAsts.forEach(input => { inputAsts.forEach(input => {
if (input.type == t.PropertyBindingType.Animation) { if (input.type == t.PropertyBindingType.Animation) {
var animationName = input.name; var triggerName = input.name;
if (!isPresent(componentAnimationRegistry[animationName])) { if (isPresent(animationRegistry[triggerName])) {
detectedAnimationInputs[triggerName] = true;
} else {
this.errors.push( 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); t.templateVisitAll(this, ast.children);
} }
visitEvent(ast: t.BoundEventAst, ctx: any): any {}
visitBoundText(ast: t.BoundTextAst, ctx: any): any {} visitBoundText(ast: t.BoundTextAst, ctx: any): any {}
visitText(ast: t.TextAst, ctx: any): any {} visitText(ast: t.TextAst, ctx: any): any {}
visitEmbeddedTemplate(ast: t.EmbeddedTemplateAst, ctx: any): any {} visitEmbeddedTemplate(ast: t.EmbeddedTemplateAst, ctx: any): any {}
visitNgContent(ast: t.NgContentAst, ctx: any): any {} visitNgContent(ast: t.NgContentAst, ctx: any): any {}
visitAttr(ast: t.AttrAst, ctx: any): any {} visitAttr(ast: t.AttrAst, ctx: any): any {}
visitDirective(ast: t.DirectiveAst, ctx: any): any {} visitDirective(ast: t.DirectiveAst, ctx: any): any {}
visitEvent(ast: t.BoundEventAst, ctx: any): any {}
visitReference(ast: t.ReferenceAst, ctx: any): any {} visitReference(ast: t.ReferenceAst, ctx: any): any {}
visitVariable(ast: t.VariableAst, ctx: any): any {} visitVariable(ast: t.VariableAst, ctx: any): any {}
visitDirectiveProperty(ast: t.BoundDirectivePropertyAst, ctx: any): any {} visitDirectiveProperty(ast: t.BoundDirectivePropertyAst, ctx: any): any {}

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license * 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 {CompileAnimationAnimateMetadata, CompileAnimationEntryMetadata, CompileAnimationGroupMetadata, CompileAnimationKeyframesSequenceMetadata, CompileAnimationMetadata, CompileAnimationSequenceMetadata, CompileAnimationStateDeclarationMetadata, CompileAnimationStateTransitionMetadata, CompileAnimationStyleMetadata, CompileAnimationWithStepsMetadata} from '../compile_metadata';
import {ListWrapper, StringMapWrapper} from '../facade/collection'; import {ListWrapper, StringMapWrapper} from '../facade/collection';
import {NumberWrapper, isArray, isBlank, isPresent, isString, isStringMap} from '../facade/lang'; 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); 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( function _parseAnimationDeclarationStates(
stateMetadata: CompileAnimationStateDeclarationMetadata, stateMetadata: CompileAnimationStateDeclarationMetadata,
errors: AnimationParseError[]): AnimationStateDeclarationAst[] { errors: AnimationParseError[]): AnimationStateDeclarationAst[] {

View File

@ -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 {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 {CompileIdentifierMetadata, CompileTokenMetadata} from './compile_metadata';
import {assetUrl} from './util'; import {assetUrl} from './util';
@ -53,6 +54,7 @@ var impAnimationSequencePlayer = AnimationSequencePlayer_;
var impAnimationKeyframe = AnimationKeyframe_; var impAnimationKeyframe = AnimationKeyframe_;
var impAnimationStyles = AnimationStyles_; var impAnimationStyles = AnimationStyles_;
var impNoOpAnimationPlayer = NoOpAnimationPlayer_; var impNoOpAnimationPlayer = NoOpAnimationPlayer_;
var impAnimationOutput = AnimationOutput_;
var ANIMATION_STYLE_UTIL_ASSET_URL = assetUrl('core', 'animation/animation_style_util'); var ANIMATION_STYLE_UTIL_ASSET_URL = assetUrl('core', 'animation/animation_style_util');
@ -258,6 +260,11 @@ export class Identifiers {
moduleUrl: assetUrl('core', 'i18n/tokens'), moduleUrl: assetUrl('core', 'i18n/tokens'),
runtime: TRANSLATIONS_FORMAT_ runtime: TRANSLATIONS_FORMAT_
}); });
static AnimationOutput = new CompileIdentifierMetadata({
name: 'AnimationOutput',
moduleUrl: assetUrl('core', 'animation/animation_output'),
runtime: impAnimationOutput
});
} }
export function identifierToken(identifier: CompileIdentifierMetadata): CompileTokenMetadata { export function identifierToken(identifier: CompileIdentifierMetadata): CompileTokenMetadata {

View File

@ -7,7 +7,7 @@
*/ */
import {ViewType} from '../../core_private'; 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 {CompileDirectiveMetadata, CompileIdentifierMap, CompileIdentifierMetadata, CompilePipeMetadata, CompileTokenMetadata} from '../compile_metadata';
import {CompilerConfig} from '../config'; import {CompilerConfig} from '../config';
import {ListWrapper} from '../facade/collection'; import {ListWrapper} from '../facade/collection';
@ -71,7 +71,7 @@ export class CompileView implements NameResolver {
constructor( constructor(
public component: CompileDirectiveMetadata, public genConfig: CompilerConfig, public component: CompileDirectiveMetadata, public genConfig: CompilerConfig,
public pipeMetas: CompilePipeMetadata[], public styles: o.Expression, 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[][]) { public declarationElement: CompileElement, public templateVariableBindings: string[][]) {
this.createMethod = new CompileMethod(this); this.createMethod = new CompileMethod(this);
this.injectorGetMethod = new CompileMethod(this); this.injectorGetMethod = new CompileMethod(this);

View File

@ -6,10 +6,11 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {AnimationOutput} from '../../core_private';
import {CompileDirectiveMetadata} from '../compile_metadata'; import {CompileDirectiveMetadata} from '../compile_metadata';
import {ListWrapper, StringMapWrapper} from '../facade/collection'; import {ListWrapper, StringMapWrapper} from '../facade/collection';
import {StringWrapper, isBlank, isPresent} from '../facade/lang'; import {StringWrapper, isBlank, isPresent} from '../facade/lang';
import {identifierToken} from '../identifiers'; import {Identifiers, identifierToken} from '../identifiers';
import * as o from '../output/output_ast'; import * as o from '../output/output_ast';
import {BoundEventAst, DirectiveAst} from '../template_parser/template_ast'; import {BoundEventAst, DirectiveAst} from '../template_parser/template_ast';
@ -19,6 +20,10 @@ import {CompileMethod} from './compile_method';
import {EventHandlerVars, ViewProperties} from './constants'; import {EventHandlerVars, ViewProperties} from './constants';
import {convertCdStatementToIr} from './expression_converter'; import {convertCdStatementToIr} from './expression_converter';
export class CompileElementAnimationOutput {
constructor(public listener: CompileEventListener, public output: AnimationOutput) {}
}
export class CompileEventListener { export class CompileEventListener {
private _method: CompileMethod; private _method: CompileMethod;
private _hasComponentHostListener: boolean = false; private _hasComponentHostListener: boolean = false;
@ -39,6 +44,8 @@ export class CompileEventListener {
return listener; return listener;
} }
get methodName() { return this._methodName; }
constructor( constructor(
public compileElement: CompileElement, public eventTarget: string, public eventName: string, public compileElement: CompileElement, public eventTarget: string, public eventName: string,
listenerIndex: number) { listenerIndex: number) {
@ -112,6 +119,26 @@ export class CompileEventListener {
disposable.set(listenExpr).toDeclStmt(o.FUNCTION_TYPE, [o.StmtModifier.Private])); 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) { listenToDirective(directiveInstance: o.Expression, observablePropName: string) {
var subscription = o.variable(`subscription_${this.compileElement.view.subscriptions.length}`); var subscription = o.variable(`subscription_${this.compileElement.view.subscriptions.length}`);
this.compileElement.view.subscriptions.push(subscription); this.compileElement.view.subscriptions.push(subscription);
@ -166,6 +193,10 @@ export function bindRenderOutputs(eventListeners: CompileEventListener[]) {
eventListeners.forEach(listener => listener.listenToRenderer()); 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 { function convertStmtIntoExpression(stmt: o.Statement): o.Expression {
if (stmt instanceof o.ExpressionStatement) { if (stmt instanceof o.ExpressionStatement) {
return stmt.expr; return stmt.expr;

View File

@ -5,19 +5,20 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * found in the LICENSE file at https://angular.io/license
*/ */
import {AnimationOutput} from '../../core_private';
import {ListWrapper} from '../facade/collection'; import {ListWrapper} from '../facade/collection';
import {identifierToken} from '../identifiers'; 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 {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 {CompileElement, CompileNode} from './compile_element';
import {CompileView} from './compile_view'; 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 {bindDirectiveAfterContentLifecycleCallbacks, bindDirectiveAfterViewLifecycleCallbacks, bindDirectiveDetectChangesLifecycleCallbacks, bindInjectableDestroyLifecycleCallbacks, bindPipeDestroyLifecycleCallbacks} from './lifecycle_binder';
import {bindDirectiveHostProps, bindDirectiveInputs, bindRenderInputs, bindRenderText} from './property_binder'; import {bindDirectiveHostProps, bindDirectiveInputs, bindRenderInputs, bindRenderText} from './property_binder';
export function bindView(view: CompileView, parsedTemplate: TemplateAst[]): void { export function bindView(
var visitor = new ViewBinderVisitor(view); view: CompileView, parsedTemplate: TemplateAst[], animationOutputs: AnimationOutput[]): void {
var visitor = new ViewBinderVisitor(view, animationOutputs);
templateVisitAll(visitor, parsedTemplate); templateVisitAll(visitor, parsedTemplate);
view.pipes.forEach( view.pipes.forEach(
(pipe) => { bindPipeDestroyLifecycleCallbacks(pipe.meta, pipe.instance, pipe.view); }); (pipe) => { bindPipeDestroyLifecycleCallbacks(pipe.meta, pipe.instance, pipe.view); });
@ -25,8 +26,12 @@ export function bindView(view: CompileView, parsedTemplate: TemplateAst[]): void
class ViewBinderVisitor implements TemplateAstVisitor { class ViewBinderVisitor implements TemplateAstVisitor {
private _nodeIndex: number = 0; 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 { visitBoundText(ast: BoundTextAst, parent: CompileElement): any {
var node = this.view.nodes[this._nodeIndex++]; var node = this.view.nodes[this._nodeIndex++];
@ -42,7 +47,23 @@ class ViewBinderVisitor implements TemplateAstVisitor {
visitElement(ast: ElementAst, parent: CompileElement): any { visitElement(ast: ElementAst, parent: CompileElement): any {
var compileElement = <CompileElement>this.view.nodes[this._nodeIndex++]; var compileElement = <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); bindRenderInputs(ast.inputs, compileElement);
bindRenderOutputs(eventListeners); bindRenderOutputs(eventListeners);
ast.directives.forEach((directiveAst) => { ast.directives.forEach((directiveAst) => {
@ -90,7 +111,7 @@ class ViewBinderVisitor implements TemplateAstVisitor {
var providerInstance = compileElement.instances.get(providerAst.token); var providerInstance = compileElement.instances.get(providerAst.token);
bindInjectableDestroyLifecycleCallbacks(providerAst, providerInstance, compileElement); bindInjectableDestroyLifecycleCallbacks(providerAst, providerInstance, compileElement);
}); });
bindView(compileElement.embeddedView, ast.children); bindView(compileElement.embeddedView, ast.children, []);
return null; return null;
} }

View File

@ -283,7 +283,7 @@ class ViewBuilderVisitor implements TemplateAstVisitor {
this.nestedViewCount++; this.nestedViewCount++;
var embeddedView = new CompileView( var embeddedView = new CompileView(
this.view.component, this.view.genConfig, this.view.pipeMetas, o.NULL_EXPR, 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); templateVariableBindings);
this.nestedViewCount += buildView(embeddedView, ast.children, this.targetDependencies); this.nestedViewCount += buildView(embeddedView, ast.children, this.targetDependencies);

View File

@ -38,17 +38,18 @@ export class ViewCompiler {
var dependencies: Array<ViewFactoryDependency|ComponentFactoryDependency> = []; var dependencies: Array<ViewFactoryDependency|ComponentFactoryDependency> = [];
var compiledAnimations = this._animationCompiler.compileComponent(component, template); var compiledAnimations = this._animationCompiler.compileComponent(component, template);
var statements: o.Statement[] = []; var statements: o.Statement[] = [];
compiledAnimations.map(entry => { var animationTriggers = compiledAnimations.triggers;
animationTriggers.forEach(entry => {
statements.push(entry.statesMapStatement); statements.push(entry.statesMapStatement);
statements.push(entry.fnStatement); statements.push(entry.fnStatement);
}); });
var view = new CompileView( var view = new CompileView(
component, this._genConfig, pipes, styles, compiledAnimations, 0, component, this._genConfig, pipes, styles, animationTriggers, 0,
CompileElement.createNull(), []); CompileElement.createNull(), []);
buildView(view, template, dependencies); buildView(view, template, dependencies);
// Need to separate binding from creation to be able to refer to // Need to separate binding from creation to be able to refer to
// variables that have been declared after usage. // variables that have been declared after usage.
bindView(view, template); bindView(view, template, compiledAnimations.outputs);
finishView(view, statements); finishView(view, statements);
return new ViewCompileResult(statements, view.viewFactory.name, dependencies); return new ViewCompileResult(statements, view.viewFactory.name, dependencies);

View File

@ -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 {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 {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 {CompileAnimationEntryMetadata, CompileDirectiveMetadata, CompileTemplateMetadata, CompileTypeMetadata} from '../../src/compile_metadata';
import {CompileMetadataResolver} from '../../src/metadata_resolver'; import {CompileMetadataResolver} from '../../src/metadata_resolver';
@ -22,8 +22,10 @@ export function main() {
var compiler = new AnimationCompiler(); var compiler = new AnimationCompiler();
var compileAnimations = (component: CompileDirectiveMetadata): CompiledAnimation => { var compileAnimations =
return compiler.compileComponent(component, [])[0]; (component: CompileDirectiveMetadata): CompiledAnimationTriggerResult => {
var result = compiler.compileComponent(component, []);
return result.triggers[0];
}; };
var compileTriggers = (input: any[]) => { var compileTriggers = (input: any[]) => {

View File

@ -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 {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 {AnimationGroupPlayer as AnimationGroupPlayer_} from './src/animation/animation_group_player';
import {AnimationKeyframe as AnimationKeyframe_} from './src/animation/animation_keyframe'; 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 {AnimationPlayer as AnimationPlayer_, NoOpAnimationPlayer as NoOpAnimationPlayer_} from './src/animation/animation_player';
import {AnimationSequencePlayer as AnimationSequencePlayer_} from './src/animation/animation_sequence_player'; import {AnimationSequencePlayer as AnimationSequencePlayer_} from './src/animation/animation_sequence_player';
import * as animationUtils from './src/animation/animation_style_util'; 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 var collectAndResolveStyles: typeof animationUtils.collectAndResolveStyles;
export type AnimationStyles = AnimationStyles_; export type AnimationStyles = AnimationStyles_;
export var AnimationStyles: typeof AnimationStyles_; export var AnimationStyles: typeof AnimationStyles_;
export type AnimationOutput = AnimationOutput_;
export var AnimationOutput: typeof AnimationOutput_;
export var ANY_STATE: typeof ANY_STATE_; export var ANY_STATE: typeof ANY_STATE_;
export var DEFAULT_STATE: typeof DEFAULT_STATE_; export var DEFAULT_STATE: typeof DEFAULT_STATE_;
export var EMPTY_STATE: typeof EMPTY_STATE_; export var EMPTY_STATE: typeof EMPTY_STATE_;
@ -185,6 +188,7 @@ export var __core_private__ = {
renderStyles: animationUtils.renderStyles, renderStyles: animationUtils.renderStyles,
collectAndResolveStyles: animationUtils.collectAndResolveStyles, collectAndResolveStyles: animationUtils.collectAndResolveStyles,
AnimationStyles: AnimationStyles_, AnimationStyles: AnimationStyles_,
AnimationOutput: AnimationOutput_,
ANY_STATE: ANY_STATE_, ANY_STATE: ANY_STATE_,
DEFAULT_STATE: DEFAULT_STATE_, DEFAULT_STATE: DEFAULT_STATE_,
EMPTY_STATE: EMPTY_STATE_, EMPTY_STATE: EMPTY_STATE_,

View File

@ -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) {}
}

View File

@ -7,7 +7,8 @@
*/ */
import {AnimationGroupPlayer} from '../animation/animation_group_player'; 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 {ViewAnimationMap} from '../animation/view_animation_map';
import {ChangeDetectorRef, ChangeDetectorStatus} from '../change_detection/change_detection'; import {ChangeDetectorRef, ChangeDetectorStatus} from '../change_detection/change_detection';
import {Injector} from '../di/injector'; import {Injector} from '../di/injector';
@ -50,6 +51,8 @@ export abstract class AppView<T> {
public animationPlayers = new ViewAnimationMap(); public animationPlayers = new ViewAnimationMap();
private _animationListeners = new Map<any, _AnimationOutputWithHandler[]>();
public context: T; public context: T;
constructor( constructor(
@ -77,9 +80,23 @@ export abstract class AppView<T> {
} }
} }
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); 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() { triggerQueuedAnimations() {
@ -90,6 +107,32 @@ export abstract class AppView<T> {
}); });
} }
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<any|any[]>, rootSelectorOrNode: string|any): create(context: T, givenProjectableNodes: Array<any|any[]>, rootSelectorOrNode: string|any):
AppElement { AppElement {
this.context = context; this.context = context;
@ -433,3 +476,7 @@ function _findLastRenderNode(node: any): any {
} }
return lastNode; return lastNode;
} }
class _AnimationOutputWithHandler {
constructor(public output: AnimationOutput, public handler: Function) {}
}

View File

@ -10,7 +10,9 @@ import {Component, animate, group, keyframes, sequence, state, style, transition
@Component({ @Component({
host: { host: {
'[@backgroundAnimation]': 'bgStatus' '[@backgroundAnimation]': 'bgStatus',
'(@backgroundAnimation.start)': 'bgStatusChanged($event, "started")',
'(@backgroundAnimation.done)': 'bgStatusChanged($event, "completed")'
}, },
selector: 'animate-app', selector: 'animate-app',
styleUrls: ['css/animate-app.css'], styleUrls: ['css/animate-app.css'],
@ -80,6 +82,10 @@ export class AnimateApp {
this.items[Math.floor(Math.random() * this.items.length)] = 99; 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; } get state() { return this._state; }
set state(s) { set state(s) {
this._state = s; this._state = s;