From 32feb8a5328221eedaece8f5212b0d9b65210a42 Mon Sep 17 00:00:00 2001 From: Tobias Bosch Date: Wed, 26 Oct 2016 16:58:35 -0700 Subject: [PATCH] refactor(compiler): generate host listeners in DirectiveWrappers Part of #11683 --- .../src/compiler_util/expression_converter.ts | 2 +- .../compiler/src/compiler_util/render_util.ts | 59 +++- .../src/directive_wrapper_compiler.ts | 225 +++++++++++++-- modules/@angular/compiler/src/identifiers.ts | 7 + .../compiler/src/output/output_interpreter.ts | 4 +- .../src/template_parser/template_ast.ts | 18 +- .../src/view_compiler/compile_element.ts | 8 +- .../compiler/src/view_compiler/constants.ts | 7 +- .../src/view_compiler/event_binder.ts | 269 ++++++++---------- .../src/view_compiler/lifecycle_binder.ts | 20 +- .../src/view_compiler/property_binder.ts | 133 +++------ .../compiler/src/view_compiler/util.ts | 4 + .../compiler/src/view_compiler/view_binder.ts | 26 +- modules/@angular/core/src/linker/view.ts | 10 +- .../@angular/core/src/linker/view_utils.ts | 152 ++++++++++ 15 files changed, 628 insertions(+), 316 deletions(-) diff --git a/modules/@angular/compiler/src/compiler_util/expression_converter.ts b/modules/@angular/compiler/src/compiler_util/expression_converter.ts index 14c8af7a75..ad56299dd2 100644 --- a/modules/@angular/compiler/src/compiler_util/expression_converter.ts +++ b/modules/@angular/compiler/src/compiler_util/expression_converter.ts @@ -71,7 +71,7 @@ export function convertPropertyBinding( } export class ConvertActionBindingResult { - constructor(public stmts: o.Statement[], public preventDefault: o.Expression) {} + constructor(public stmts: o.Statement[], public preventDefault: o.ReadVarExpr) {} } /** diff --git a/modules/@angular/compiler/src/compiler_util/render_util.ts b/modules/@angular/compiler/src/compiler_util/render_util.ts index e0beeef6ac..184922e31a 100644 --- a/modules/@angular/compiler/src/compiler_util/render_util.ts +++ b/modules/@angular/compiler/src/compiler_util/render_util.ts @@ -10,7 +10,8 @@ import {SecurityContext} from '@angular/core'; import {isPresent} from '../facade/lang'; import {Identifiers, resolveIdentifier} from '../identifiers'; import * as o from '../output/output_ast'; -import {BoundElementPropertyAst, PropertyBindingType} from '../template_parser/template_ast'; +import {EMPTY_STATE as EMPTY_ANIMATION_STATE} from '../private_import_core'; +import {BoundElementPropertyAst, BoundEventAst, PropertyBindingType} from '../template_parser/template_ast'; import {createEnumExpression} from './identifier_util'; @@ -87,3 +88,59 @@ function sanitizedValue( let args = [securityContextExpression, renderValue]; return ctx.callMethod('sanitize', args); } + +export function triggerAnimation( + view: o.Expression, componentView: o.Expression, boundProp: BoundElementPropertyAst, + eventListener: o.Expression, renderElement: o.Expression, renderValue: o.Expression, + lastRenderValue: o.Expression) { + const detachStmts: o.Statement[] = []; + const updateStmts: o.Statement[] = []; + + const animationName = boundProp.name; + + const animationFnExpr = + componentView.prop('componentType').prop('animations').key(o.literal(animationName)); + + // it's important to normalize the void value as `void` explicitly + // so that the styles data can be obtained from the stringmap + const emptyStateValue = o.literal(EMPTY_ANIMATION_STATE); + const unitializedValue = o.importExpr(resolveIdentifier(Identifiers.UNINITIALIZED)); + const animationTransitionVar = o.variable('animationTransition_' + animationName); + + updateStmts.push( + animationTransitionVar + .set(animationFnExpr.callFn([ + view, renderElement, + lastRenderValue.equals(unitializedValue).conditional(emptyStateValue, lastRenderValue), + renderValue.equals(unitializedValue).conditional(emptyStateValue, renderValue) + ])) + .toDeclStmt()); + + detachStmts.push( + animationTransitionVar + .set(animationFnExpr.callFn([view, renderElement, lastRenderValue, emptyStateValue])) + .toDeclStmt()); + + const registerStmts = [ + animationTransitionVar + .callMethod( + 'onStart', + [eventListener.callMethod( + o.BuiltinMethod.Bind, + [view, o.literal(BoundEventAst.calcFullName(animationName, null, 'start'))])]) + .toStmt(), + animationTransitionVar + .callMethod( + 'onDone', + [eventListener.callMethod( + o.BuiltinMethod.Bind, + [view, o.literal(BoundEventAst.calcFullName(animationName, null, 'done'))])]) + .toStmt(), + + ]; + + updateStmts.push(...registerStmts); + detachStmts.push(...registerStmts); + + return {updateStmts, detachStmts}; +} diff --git a/modules/@angular/compiler/src/directive_wrapper_compiler.ts b/modules/@angular/compiler/src/directive_wrapper_compiler.ts index 32c987b0eb..00729fe3c7 100644 --- a/modules/@angular/compiler/src/directive_wrapper_compiler.ts +++ b/modules/@angular/compiler/src/directive_wrapper_compiler.ts @@ -10,8 +10,8 @@ import {Injectable} from '@angular/core'; import {CompileDirectiveMetadata, CompileIdentifierMetadata} from './compile_metadata'; import {createCheckBindingField, createCheckBindingStmt} from './compiler_util/binding_util'; -import {convertPropertyBinding} from './compiler_util/expression_converter'; -import {writeToRenderer} from './compiler_util/render_util'; +import {EventHandlerVars, convertActionBinding, convertPropertyBinding} from './compiler_util/expression_converter'; +import {triggerAnimation, writeToRenderer} from './compiler_util/render_util'; import {CompilerConfig} from './config'; import {Parser} from './expression_parser/parser'; import {Identifiers, resolveIdentifier} from './identifiers'; @@ -31,12 +31,15 @@ export class DirectiveWrapperCompileResult { const CONTEXT_FIELD_NAME = 'context'; const CHANGES_FIELD_NAME = 'changes'; const CHANGED_FIELD_NAME = 'changed'; +const EVENT_HANDLER_FIELD_NAME = 'eventHandler'; const CURR_VALUE_VAR = o.variable('currValue'); const THROW_ON_CHANGE_VAR = o.variable('throwOnChange'); const FORCE_UPDATE_VAR = o.variable('forceUpdate'); const VIEW_VAR = o.variable('view'); +const COMPONENT_VIEW_VAR = o.variable('componentView'); const RENDER_EL_VAR = o.variable('el'); +const EVENT_NAME_VAR = o.variable('eventName'); const RESET_CHANGES_STMT = o.THIS_EXPR.prop(CHANGES_FIELD_NAME).set(o.literalMap([])).toStmt(); @@ -57,22 +60,17 @@ export class DirectiveWrapperCompiler { private _schemaRegistry: ElementSchemaRegistry, private _console: Console) {} compile(dirMeta: CompileDirectiveMetadata): DirectiveWrapperCompileResult { + const hostParseResult = parseHostBindings(dirMeta, this._exprParser, this._schemaRegistry); + reportParseErrors(hostParseResult.errors, this._console); + const builder = new DirectiveWrapperBuilder(this.compilerConfig, dirMeta); Object.keys(dirMeta.inputs).forEach((inputFieldName) => { addCheckInputMethod(inputFieldName, builder); }); - addDetectChangesInInputPropsMethod(builder); - - const hostParseResult = parseHostBindings(dirMeta, this._exprParser, this._schemaRegistry); - reportParseErrors(hostParseResult.errors, this._console); - // host properties are change detected by the DirectiveWrappers, - // except for the animation properties as they need close integration with animation events - // and DirectiveWrappers don't support - // event listeners right now. - addDetectChangesInHostPropsMethod( - hostParseResult.hostProps.filter(hostProp => !hostProp.isAnimation), builder); - - // TODO(tbosch): implement hostListeners via DirectiveWrapper as well! + addNgDoCheckMethod(builder); + addCheckHostMethod(hostParseResult.hostProps, builder); + addHandleEventMethod(hostParseResult.hostListeners, builder); + addSubscribeMethod(dirMeta, builder); const classStmt = builder.build(); return new DirectiveWrapperCompileResult([classStmt], classStmt.name); @@ -84,11 +82,14 @@ class DirectiveWrapperBuilder implements ClassBuilder { getters: o.ClassGetter[] = []; methods: o.ClassMethod[] = []; ctorStmts: o.Statement[] = []; + detachStmts: o.Statement[] = []; + destroyStmts: o.Statement[] = []; genChanges: boolean; ngOnChanges: boolean; ngOnInit: boolean; ngDoCheck: boolean; + ngOnDestroy: boolean; constructor(public compilerConfig: CompilerConfig, public dirMeta: CompileDirectiveMetadata) { const dirLifecycleHooks = dirMeta.type.lifecycleHooks; @@ -97,6 +98,11 @@ class DirectiveWrapperBuilder implements ClassBuilder { this.ngOnChanges = dirLifecycleHooks.indexOf(LifecycleHooks.OnChanges) !== -1; this.ngOnInit = dirLifecycleHooks.indexOf(LifecycleHooks.OnInit) !== -1; this.ngDoCheck = dirLifecycleHooks.indexOf(LifecycleHooks.DoCheck) !== -1; + this.ngOnDestroy = dirLifecycleHooks.indexOf(LifecycleHooks.OnDestroy) !== -1; + if (this.ngOnDestroy) { + this.destroyStmts.push( + o.THIS_EXPR.prop(CONTEXT_FIELD_NAME).callMethod('ngOnDestroy', []).toStmt()); + } } build(): o.ClassStmt { @@ -105,7 +111,25 @@ class DirectiveWrapperBuilder implements ClassBuilder { dirDepParamNames.push(`p${i}`); } + const methods = [ + new o.ClassMethod( + 'ngOnDetach', + [ + new o.FnParam( + VIEW_VAR.name, + o.importType(resolveIdentifier(Identifiers.AppView), [o.DYNAMIC_TYPE])), + new o.FnParam( + COMPONENT_VIEW_VAR.name, + o.importType(resolveIdentifier(Identifiers.AppView), [o.DYNAMIC_TYPE])), + new o.FnParam(RENDER_EL_VAR.name, o.DYNAMIC_TYPE), + ], + this.detachStmts), + new o.ClassMethod('ngOnDestroy', [], this.destroyStmts), + ]; + + const fields: o.ClassField[] = [ + new o.ClassField(EVENT_HANDLER_FIELD_NAME, o.FUNCTION_TYPE), new o.ClassField(CONTEXT_FIELD_NAME, o.importType(this.dirMeta.type)), new o.ClassField(CHANGED_FIELD_NAME, o.BOOL_TYPE), ]; @@ -125,12 +149,12 @@ class DirectiveWrapperBuilder implements ClassBuilder { return createClassStmt({ name: DirectiveWrapperCompiler.dirWrapperClassName(this.dirMeta.type), ctorParams: dirDepParamNames.map((paramName) => new o.FnParam(paramName, o.DYNAMIC_TYPE)), - builders: [{fields, ctorStmts}, this] + builders: [{fields, ctorStmts, methods}, this] }); } } -function addDetectChangesInInputPropsMethod(builder: DirectiveWrapperBuilder) { +function addNgDoCheckMethod(builder: DirectiveWrapperBuilder) { const changedVar = o.variable('changed'); const stmts: o.Statement[] = [ changedVar.set(o.THIS_EXPR.prop(CHANGED_FIELD_NAME)).toDeclStmt(), @@ -170,7 +194,7 @@ function addDetectChangesInInputPropsMethod(builder: DirectiveWrapperBuilder) { stmts.push(new o.ReturnStatement(changedVar)); builder.methods.push(new o.ClassMethod( - 'detectChangesInInputProps', + 'ngDoCheck', [ new o.FnParam( VIEW_VAR.name, o.importType(resolveIdentifier(Identifiers.AppView), [o.DYNAMIC_TYPE])), @@ -207,16 +231,19 @@ function addCheckInputMethod(input: string, builder: DirectiveWrapperBuilder) { methodBody)); } -function addDetectChangesInHostPropsMethod( +function addCheckHostMethod( hostProps: BoundElementPropertyAst[], builder: DirectiveWrapperBuilder) { const stmts: o.Statement[] = []; const methodParams: o.FnParam[] = [ new o.FnParam( VIEW_VAR.name, o.importType(resolveIdentifier(Identifiers.AppView), [o.DYNAMIC_TYPE])), + new o.FnParam( + COMPONENT_VIEW_VAR.name, + o.importType(resolveIdentifier(Identifiers.AppView), [o.DYNAMIC_TYPE])), new o.FnParam(RENDER_EL_VAR.name, o.DYNAMIC_TYPE), new o.FnParam(THROW_ON_CHANGE_VAR.name, o.BOOL_TYPE), ]; - hostProps.forEach((hostProp) => { + hostProps.forEach((hostProp, hostPropIdx) => { const field = createCheckBindingField(builder); const evalResult = convertPropertyBinding( builder, null, o.THIS_EXPR.prop(CONTEXT_FIELD_NAME), hostProp.value, field.bindingId); @@ -229,13 +256,80 @@ function addDetectChangesInHostPropsMethod( methodParams.push(new o.FnParam( securityContextExpr.name, o.importType(resolveIdentifier(Identifiers.SecurityContext)))); } + let checkBindingStmts: o.Statement[]; + if (hostProp.isAnimation) { + const {updateStmts, detachStmts} = triggerAnimation( + VIEW_VAR, COMPONENT_VIEW_VAR, hostProp, + o.THIS_EXPR.prop(EVENT_HANDLER_FIELD_NAME) + .or(o.importExpr(resolveIdentifier(Identifiers.noop))), + RENDER_EL_VAR, evalResult.currValExpr, field.expression); + checkBindingStmts = updateStmts; + builder.detachStmts.push(...detachStmts); + } else { + checkBindingStmts = writeToRenderer( + VIEW_VAR, hostProp, RENDER_EL_VAR, evalResult.currValExpr, + builder.compilerConfig.logBindingUpdate, securityContextExpr); + } + stmts.push(...createCheckBindingStmt( - evalResult, field.expression, THROW_ON_CHANGE_VAR, - writeToRenderer( - VIEW_VAR, hostProp, RENDER_EL_VAR, evalResult.currValExpr, - builder.compilerConfig.logBindingUpdate, securityContextExpr))); + evalResult, field.expression, THROW_ON_CHANGE_VAR, checkBindingStmts)); }); - builder.methods.push(new o.ClassMethod('detectChangesInHostProps', methodParams, stmts)); + builder.methods.push(new o.ClassMethod('checkHost', methodParams, stmts)); +} + +function addHandleEventMethod(hostListeners: BoundEventAst[], builder: DirectiveWrapperBuilder) { + const resultVar = o.variable(`result`); + const actionStmts: o.Statement[] = [resultVar.set(o.literal(true)).toDeclStmt(o.BOOL_TYPE)]; + hostListeners.forEach((hostListener, eventIdx) => { + const evalResult = convertActionBinding( + builder, null, o.THIS_EXPR.prop(CONTEXT_FIELD_NAME), hostListener.handler, + `sub_${eventIdx}`); + const trueStmts = evalResult.stmts; + if (evalResult.preventDefault) { + trueStmts.push(resultVar.set(evalResult.preventDefault.and(resultVar)).toStmt()); + } + // TODO(tbosch): convert this into a `switch` once our OutputAst supports it. + actionStmts.push( + new o.IfStmt(EVENT_NAME_VAR.equals(o.literal(hostListener.fullName)), trueStmts)); + }); + actionStmts.push(new o.ReturnStatement(resultVar)); + builder.methods.push(new o.ClassMethod( + 'handleEvent', + [ + new o.FnParam(EVENT_NAME_VAR.name, o.STRING_TYPE), + new o.FnParam(EventHandlerVars.event.name, o.DYNAMIC_TYPE) + ], + actionStmts, o.BOOL_TYPE)); +} + +function addSubscribeMethod(dirMeta: CompileDirectiveMetadata, builder: DirectiveWrapperBuilder) { + const methodParams: o.FnParam[] = [new o.FnParam(EVENT_HANDLER_FIELD_NAME, o.DYNAMIC_TYPE)]; + const stmts: o.Statement[] = [ + o.THIS_EXPR.prop(EVENT_HANDLER_FIELD_NAME).set(o.variable(EVENT_HANDLER_FIELD_NAME)).toStmt() + ]; + Object.keys(dirMeta.outputs).forEach((emitterPropName, emitterIdx) => { + const eventName = dirMeta.outputs[emitterPropName]; + const paramName = `emit${emitterIdx}`; + methodParams.push(new o.FnParam(paramName, o.BOOL_TYPE)); + const subscriptionFieldName = `subscription${emitterIdx}`; + builder.fields.push(new o.ClassField(subscriptionFieldName, o.DYNAMIC_TYPE)); + stmts.push(new o.IfStmt( + o.variable(paramName), + [o.THIS_EXPR.prop(subscriptionFieldName) + .set(o.THIS_EXPR.prop(CONTEXT_FIELD_NAME) + .prop(emitterPropName) + .callMethod( + o.BuiltinMethod.SubscribeObservable, + [o.variable(EVENT_HANDLER_FIELD_NAME) + .callMethod( + o.BuiltinMethod.Bind, [o.NULL_EXPR, o.literal(eventName)])])) + .toStmt()])); + builder.destroyStmts.push( + o.THIS_EXPR.prop(subscriptionFieldName) + .and(o.THIS_EXPR.prop(subscriptionFieldName).callMethod('unsubscribe', [])) + .toStmt()); + }); + builder.methods.push(new o.ClassMethod('subscribe', methodParams, stmts)); } class ParseResult { @@ -274,4 +368,85 @@ function reportParseErrors(parseErrors: ParseError[], console: Console) { if (errors.length > 0) { throw new Error(`Directive parse errors:\n${errors.join('\n')}`); } -} \ No newline at end of file +} + +export class DirectiveWrapperExpressions { + static create(dir: CompileIdentifierMetadata, depsExpr: o.Expression[]): o.Expression { + return o.importExpr(dir).instantiate(depsExpr, o.importType(dir)); + } + static context(dirWrapper: o.Expression): o.ReadPropExpr { + return dirWrapper.prop(CONTEXT_FIELD_NAME); + } + + static ngDoCheck( + dirWrapper: o.Expression, view: o.Expression, renderElement: o.Expression, + throwOnChange: o.Expression): o.Expression { + return dirWrapper.callMethod('ngDoCheck', [view, renderElement, throwOnChange]); + } + static checkHost( + hostProps: BoundElementPropertyAst[], dirWrapper: o.Expression, view: o.Expression, + componentView: o.Expression, renderElement: o.Expression, throwOnChange: o.Expression, + runtimeSecurityContexts: o.Expression[]): o.Statement[] { + if (hostProps.length) { + return [dirWrapper + .callMethod( + 'checkHost', [view, componentView, renderElement, throwOnChange].concat( + runtimeSecurityContexts)) + .toStmt()]; + } else { + return []; + } + } + static ngOnDetach( + hostProps: BoundElementPropertyAst[], dirWrapper: o.Expression, view: o.Expression, + componentView: o.Expression, renderEl: o.Expression): o.Statement[] { + if (hostProps.some(prop => prop.isAnimation)) { + return [dirWrapper + .callMethod( + 'ngOnDetach', + [ + view, + componentView, + renderEl, + ]) + .toStmt()]; + } else { + return []; + } + } + static ngOnDestroy(dir: CompileDirectiveMetadata, dirWrapper: o.Expression): o.Statement[] { + if (dir.type.lifecycleHooks.indexOf(LifecycleHooks.OnDestroy) !== -1 || + Object.keys(dir.outputs).length > 0) { + return [dirWrapper.callMethod('ngOnDestroy', []).toStmt()]; + } else { + return []; + } + } + static subscribe( + dirMeta: CompileDirectiveMetadata, hostProps: BoundElementPropertyAst[], usedEvents: string[], + dirWrapper: o.Expression, eventListener: o.Expression): o.Statement[] { + let needsSubscribe = false; + let eventFlags: o.Expression[] = []; + Object.keys(dirMeta.outputs).forEach((propName) => { + const eventName = dirMeta.outputs[propName]; + const eventUsed = usedEvents.indexOf(eventName) > -1; + needsSubscribe = needsSubscribe || eventUsed; + eventFlags.push(o.literal(eventUsed)); + }); + hostProps.forEach((hostProp) => { + if (hostProp.isAnimation && usedEvents.length > 0) { + needsSubscribe = true; + } + }); + if (needsSubscribe) { + return [dirWrapper.callMethod('subscribe', [eventListener].concat(eventFlags)).toStmt()]; + } else { + return []; + } + } + static handleEvent( + hostEvents: BoundEventAst[], dirWrapper: o.Expression, eventName: o.Expression, + event: o.Expression): o.Expression { + return dirWrapper.callMethod('handleEvent', [eventName, event]); + } +} diff --git a/modules/@angular/compiler/src/identifiers.ts b/modules/@angular/compiler/src/identifiers.ts index b33443e8b8..81bff855ac 100644 --- a/modules/@angular/compiler/src/identifiers.ts +++ b/modules/@angular/compiler/src/identifiers.ts @@ -313,6 +313,13 @@ export class Identifiers { moduleUrl: VIEW_UTILS_MODULE_URL, runtime: view_utils.InlineArrayDynamic }; + static subscribeToRenderElement: IdentifierSpec = { + name: 'subscribeToRenderElement', + moduleUrl: VIEW_UTILS_MODULE_URL, + runtime: view_utils.subscribeToRenderElement + }; + static noop: + IdentifierSpec = {name: 'noop', moduleUrl: VIEW_UTILS_MODULE_URL, runtime: view_utils.noop}; } export function assetUrl(pkg: string, path: string = null, type: string = 'src'): string { diff --git a/modules/@angular/compiler/src/output/output_interpreter.ts b/modules/@angular/compiler/src/output/output_interpreter.ts index 594fd6a27d..431546f2f5 100644 --- a/modules/@angular/compiler/src/output/output_interpreter.ts +++ b/modules/@angular/compiler/src/output/output_interpreter.ts @@ -152,13 +152,13 @@ class StatementInterpreter implements o.StatementVisitor, o.ExpressionVisitor { if (isPresent(expr.builtin)) { switch (expr.builtin) { case o.BuiltinMethod.ConcatArray: - result = receiver.concat(args[0]); + result = receiver.concat(...args); break; case o.BuiltinMethod.SubscribeObservable: result = receiver.subscribe({next: args[0]}); break; case o.BuiltinMethod.Bind: - result = receiver.bind(args[0]); + result = receiver.bind(...args); break; default: throw new Error(`Unknown builtin method ${expr.builtin}`); diff --git a/modules/@angular/compiler/src/template_parser/template_ast.ts b/modules/@angular/compiler/src/template_parser/template_ast.ts index c56d11177e..926b78fd3f 100644 --- a/modules/@angular/compiler/src/template_parser/template_ast.ts +++ b/modules/@angular/compiler/src/template_parser/template_ast.ts @@ -76,19 +76,23 @@ export class BoundElementPropertyAst implements TemplateAst { * `(@trigger.phase)="callback($event)"`). */ export class BoundEventAst implements TemplateAst { + static calcFullName(name: string, target: string, phase: string): string { + if (target) { + return `${target}:${name}`; + } else if (phase) { + return `@${name}.${phase}`; + } else { + return name; + } + } + constructor( public name: string, public target: string, public phase: string, public handler: AST, public sourceSpan: ParseSourceSpan) {} visit(visitor: TemplateAstVisitor, context: any): any { return visitor.visitEvent(this, context); } - get fullName() { - if (this.target) { - return `${this.target}:${this.name}`; - } else { - return this.name; - } - } + get fullName() { return BoundEventAst.calcFullName(this.name, this.target, this.phase); } get isAnimation(): boolean { return !!this.phase; } } diff --git a/modules/@angular/compiler/src/view_compiler/compile_element.ts b/modules/@angular/compiler/src/view_compiler/compile_element.ts index b1dda9d55d..5cb928cefa 100644 --- a/modules/@angular/compiler/src/view_compiler/compile_element.ts +++ b/modules/@angular/compiler/src/view_compiler/compile_element.ts @@ -9,7 +9,7 @@ import {CompileDiDependencyMetadata, CompileDirectiveMetadata, CompileIdentifierMetadata, CompileProviderMetadata, CompileQueryMetadata, CompileTokenMetadata} from '../compile_metadata'; import {createDiTokenExpression} from '../compiler_util/identifier_util'; -import {DirectiveWrapperCompiler} from '../directive_wrapper_compiler'; +import {DirectiveWrapperCompiler, DirectiveWrapperExpressions} from '../directive_wrapper_compiler'; import {MapWrapper} from '../facade/collection'; import {isPresent} from '../facade/lang'; import {Identifiers, identifierToken, resolveIdentifier, resolveIdentifierToken} from '../identifiers'; @@ -188,8 +188,7 @@ export class CompileElement extends CompileNode { {name: DirectiveWrapperCompiler.dirWrapperClassName(provider.useClass)}); this._targetDependencies.push( new DirectiveWrapperDependency(provider.useClass, directiveWrapperIdentifier)); - return o.importExpr(directiveWrapperIdentifier) - .instantiate(depsExpr, o.importType(directiveWrapperIdentifier)); + return DirectiveWrapperExpressions.create(directiveWrapperIdentifier, depsExpr); } else { return o.importExpr(provider.useClass) .instantiate(depsExpr, o.importType(provider.useClass)); @@ -204,7 +203,8 @@ export class CompileElement extends CompileNode { resolvedProvider.eager, this); if (isDirectiveWrapper) { this.directiveWrapperInstance.set(resolvedProvider.token.reference, instance); - this.instances.set(resolvedProvider.token.reference, instance.prop('context')); + this.instances.set( + resolvedProvider.token.reference, DirectiveWrapperExpressions.context(instance)); } else { this.instances.set(resolvedProvider.token.reference, instance); } diff --git a/modules/@angular/compiler/src/view_compiler/constants.ts b/modules/@angular/compiler/src/view_compiler/constants.ts index 03e242f9b7..b6c4d2cf9b 100644 --- a/modules/@angular/compiler/src/view_compiler/constants.ts +++ b/modules/@angular/compiler/src/view_compiler/constants.ts @@ -8,16 +8,11 @@ import {ChangeDetectionStrategy, ViewEncapsulation} from '@angular/core'; -import {CompileIdentifierMetadata} from '../compile_metadata'; import {createEnumExpression} from '../compiler_util/identifier_util'; -import {Identifiers, resolveEnumIdentifier} from '../identifiers'; +import {Identifiers} from '../identifiers'; import * as o from '../output/output_ast'; import {ChangeDetectorStatus, ViewType} from '../private_import_core'; -function _enumExpression(classIdentifier: CompileIdentifierMetadata, name: string): o.Expression { - return o.importExpr(resolveEnumIdentifier(classIdentifier, name)); -} - export class ViewTypeEnum { static fromValue(value: ViewType): o.Expression { return createEnumExpression(Identifiers.ViewType, value); diff --git a/modules/@angular/compiler/src/view_compiler/event_binder.ts b/modules/@angular/compiler/src/view_compiler/event_binder.ts index 0aa2d9de88..088bbedbcf 100644 --- a/modules/@angular/compiler/src/view_compiler/event_binder.ts +++ b/modules/@angular/compiler/src/view_compiler/event_binder.ts @@ -6,183 +6,136 @@ * found in the LICENSE file at https://angular.io/license */ -import {CompileDirectiveMetadata} from '../compile_metadata'; import {EventHandlerVars, convertActionBinding} from '../compiler_util/expression_converter'; +import {createInlineArray} from '../compiler_util/identifier_util'; +import {DirectiveWrapperExpressions} from '../directive_wrapper_compiler'; +import {MapWrapper} from '../facade/collection'; import {isPresent} from '../facade/lang'; -import {identifierToken} from '../identifiers'; +import {Identifiers, resolveIdentifier} from '../identifiers'; import * as o from '../output/output_ast'; import {BoundEventAst, DirectiveAst} from '../template_parser/template_ast'; import {CompileElement} from './compile_element'; import {CompileMethod} from './compile_method'; import {ViewProperties} from './constants'; +import {getHandleEventMethodName} from './util'; -export class CompileEventListener { - private _method: CompileMethod; - private _hasComponentHostListener: boolean = false; - private _methodName: string; - private _eventParam: o.FnParam; - private _actionResultExprs: o.Expression[] = []; - - static getOrCreate( - compileElement: CompileElement, eventTarget: string, eventName: string, eventPhase: string, - targetEventListeners: CompileEventListener[]): CompileEventListener { - var listener = targetEventListeners.find( - listener => listener.eventTarget == eventTarget && listener.eventName == eventName && - listener.eventPhase == eventPhase); - if (!listener) { - listener = new CompileEventListener( - compileElement, eventTarget, eventName, eventPhase, targetEventListeners.length); - targetEventListeners.push(listener); - } - return listener; +export function bindOutputs( + boundEvents: BoundEventAst[], directives: DirectiveAst[], compileElement: CompileElement, + bindToRenderer: boolean): boolean { + const usedEvents = collectEvents(boundEvents, directives); + if (!usedEvents.size) { + return false; } - - get methodName() { return this._methodName; } - get isAnimation() { return !!this.eventPhase; } - - constructor( - public compileElement: CompileElement, public eventTarget: string, public eventName: string, - public eventPhase: string, listenerIndex: number) { - this._method = new CompileMethod(compileElement.view); - this._methodName = - `_handle_${sanitizeEventName(eventName)}_${compileElement.nodeIndex}_${listenerIndex}`; - this._eventParam = new o.FnParam( - EventHandlerVars.event.name, - o.importType(this.compileElement.view.genConfig.renderTypes.renderEvent)); + if (bindToRenderer) { + subscribeToRenderEvents(usedEvents, compileElement); } + subscribeToDirectiveEvents(usedEvents, directives, compileElement); + generateHandleEventMethod(boundEvents, directives, compileElement); + return true; +} - addAction( - hostEvent: BoundEventAst, directive: CompileDirectiveMetadata, - directiveInstance: o.Expression) { - if (isPresent(directive) && directive.isComponent) { - this._hasComponentHostListener = true; +function collectEvents( + boundEvents: BoundEventAst[], directives: DirectiveAst[]): Map { + const usedEvents = new Map(); + boundEvents.forEach((event) => { usedEvents.set(event.fullName, event); }); + directives.forEach((dirAst) => { + dirAst.hostEvents.forEach((event) => { usedEvents.set(event.fullName, event); }); + }); + return usedEvents; +} + +function subscribeToRenderEvents( + usedEvents: Map, compileElement: CompileElement) { + const eventAndTargetExprs: o.Expression[] = []; + usedEvents.forEach((event) => { + if (!event.phase) { + eventAndTargetExprs.push(o.literal(event.name), o.literal(event.target)); } - this._method.resetDebugInfo(this.compileElement.nodeIndex, hostEvent); - var context = directiveInstance || this.compileElement.view.componentContext; - const view = this.compileElement.view; + }); + if (eventAndTargetExprs.length) { + const disposableVar = o.variable(`disposable_${compileElement.view.disposables.length}`); + compileElement.view.disposables.push(disposableVar); + compileElement.view.createMethod.addStmt( + disposableVar + .set(o.importExpr(resolveIdentifier(Identifiers.subscribeToRenderElement)).callFn([ + ViewProperties.renderer, compileElement.renderNode, + createInlineArray(eventAndTargetExprs), handleEventClosure(compileElement) + ])) + .toDeclStmt(o.FUNCTION_TYPE, [o.StmtModifier.Private])); + } +} + +function subscribeToDirectiveEvents( + usedEvents: Map, directives: DirectiveAst[], + compileElement: CompileElement) { + const usedEventNames = MapWrapper.keys(usedEvents); + directives.forEach((dirAst) => { + const dirWrapper = compileElement.directiveWrapperInstance.get(dirAst.directive.type.reference); + compileElement.view.createMethod.addStmts(DirectiveWrapperExpressions.subscribe( + dirAst.directive, dirAst.hostProperties, usedEventNames, dirWrapper, + handleEventClosure(compileElement))); + }); +} + +function generateHandleEventMethod( + boundEvents: BoundEventAst[], directives: DirectiveAst[], compileElement: CompileElement) { + const hasComponentHostListener = + directives.some((dirAst) => dirAst.hostEvents.some((event) => dirAst.directive.isComponent)); + + const markPathToRootStart = + hasComponentHostListener ? compileElement.appElement.prop('componentView') : o.THIS_EXPR; + const handleEventStmts = new CompileMethod(compileElement.view); + handleEventStmts.resetDebugInfo(compileElement.nodeIndex, compileElement.sourceAst); + handleEventStmts.push(markPathToRootStart.callMethod('markPathToRootAsCheckOnce', []).toStmt()); + const eventNameVar = o.variable('eventName'); + const resultVar = o.variable('result'); + handleEventStmts.push(resultVar.set(o.literal(true)).toDeclStmt(o.BOOL_TYPE)); + + directives.forEach((dirAst, dirIdx) => { + const dirWrapper = compileElement.directiveWrapperInstance.get(dirAst.directive.type.reference); + if (dirAst.hostEvents.length > 0) { + handleEventStmts.push( + resultVar + .set(DirectiveWrapperExpressions + .handleEvent( + dirAst.hostEvents, dirWrapper, eventNameVar, EventHandlerVars.event) + .and(resultVar)) + .toStmt()); + } + }); + boundEvents.forEach((renderEvent, renderEventIdx) => { const evalResult = convertActionBinding( - view, directive ? null : view, context, hostEvent.handler, - `${this.compileElement.nodeIndex}_${this._actionResultExprs.length}`); + compileElement.view, compileElement.view, compileElement.view.componentContext, + renderEvent.handler, `sub_${renderEventIdx}`); + const trueStmts = evalResult.stmts; if (evalResult.preventDefault) { - this._actionResultExprs.push(evalResult.preventDefault); + trueStmts.push(resultVar.set(evalResult.preventDefault.and(resultVar)).toStmt()); } - this._method.addStmts(evalResult.stmts); - } - - finishMethod() { - var markPathToRootStart = this._hasComponentHostListener ? - this.compileElement.appElement.prop('componentView') : - o.THIS_EXPR; - var resultExpr: o.Expression = o.literal(true); - this._actionResultExprs.forEach((expr) => { resultExpr = resultExpr.and(expr); }); - var stmts = - ([markPathToRootStart.callMethod('markPathToRootAsCheckOnce', []).toStmt()]) - .concat(this._method.finish()) - .concat([new o.ReturnStatement(resultExpr)]); - // private is fine here as no child view will reference the event handler... - this.compileElement.view.methods.push(new o.ClassMethod( - this._methodName, [this._eventParam], stmts, o.BOOL_TYPE, [o.StmtModifier.Private])); - } - - listenToRenderer() { - var listenExpr: o.Expression; - var eventListener = o.THIS_EXPR.callMethod( - 'eventHandler', - [o.THIS_EXPR.prop(this._methodName).callMethod(o.BuiltinMethod.Bind, [o.THIS_EXPR])]); - if (isPresent(this.eventTarget)) { - listenExpr = ViewProperties.renderer.callMethod( - 'listenGlobal', [o.literal(this.eventTarget), o.literal(this.eventName), eventListener]); - } else { - listenExpr = ViewProperties.renderer.callMethod( - 'listen', [this.compileElement.renderNode, o.literal(this.eventName), eventListener]); - } - var disposable = o.variable(`disposable_${this.compileElement.view.disposables.length}`); - this.compileElement.view.disposables.push(disposable); - // private is fine here as no child view will reference the event handler... - this.compileElement.view.createMethod.addStmt( - disposable.set(listenExpr).toDeclStmt(o.FUNCTION_TYPE, [o.StmtModifier.Private])); - } - - listenToAnimation(animationTransitionVar: o.ReadVarExpr): o.Statement { - const callbackMethod = this.eventPhase == 'start' ? 'onStart' : 'onDone'; - return animationTransitionVar - .callMethod( - callbackMethod, - [o.THIS_EXPR.prop(this.methodName).callMethod(o.BuiltinMethod.Bind, [o.THIS_EXPR])]) - .toStmt(); - } - - listenToDirective(directiveInstance: o.Expression, observablePropName: string) { - var subscription = o.variable(`subscription_${this.compileElement.view.subscriptions.length}`); - this.compileElement.view.subscriptions.push(subscription); - var eventListener = o.THIS_EXPR.callMethod( - 'eventHandler', - [o.THIS_EXPR.prop(this._methodName).callMethod(o.BuiltinMethod.Bind, [o.THIS_EXPR])]); - this.compileElement.view.createMethod.addStmt( - subscription - .set(directiveInstance.prop(observablePropName) - .callMethod(o.BuiltinMethod.SubscribeObservable, [eventListener])) - .toDeclStmt(null, [o.StmtModifier.Final])); - } -} - -export function collectEventListeners( - hostEvents: BoundEventAst[], dirs: DirectiveAst[], - compileElement: CompileElement): CompileEventListener[] { - const eventListeners: CompileEventListener[] = []; - - hostEvents.forEach((hostEvent) => { - var listener = CompileEventListener.getOrCreate( - compileElement, hostEvent.target, hostEvent.name, hostEvent.phase, eventListeners); - listener.addAction(hostEvent, null, null); + // TODO(tbosch): convert this into a `switch` once our OutputAst supports it. + handleEventStmts.push( + new o.IfStmt(eventNameVar.equals(o.literal(renderEvent.fullName)), trueStmts)); }); - dirs.forEach((directiveAst) => { - var directiveInstance = - compileElement.instances.get(identifierToken(directiveAst.directive.type).reference); - directiveAst.hostEvents.forEach((hostEvent) => { - var listener = CompileEventListener.getOrCreate( - compileElement, hostEvent.target, hostEvent.name, hostEvent.phase, eventListeners); - listener.addAction(hostEvent, directiveAst.directive, directiveInstance); - }); - }); - - eventListeners.forEach((listener) => listener.finishMethod()); - return eventListeners; + handleEventStmts.push(new o.ReturnStatement(resultVar)); + compileElement.view.methods.push(new o.ClassMethod( + getHandleEventMethodName(compileElement.nodeIndex), + [ + new o.FnParam(eventNameVar.name, o.STRING_TYPE), + new o.FnParam(EventHandlerVars.event.name, o.DYNAMIC_TYPE) + ], + handleEventStmts.finish(), o.BOOL_TYPE)); } -export function bindDirectiveOutputs( - directiveAst: DirectiveAst, directiveInstance: o.Expression, - eventListeners: CompileEventListener[]) { - Object.keys(directiveAst.directive.outputs).forEach(observablePropName => { - const eventName = directiveAst.directive.outputs[observablePropName]; - - eventListeners.filter(listener => listener.eventName == eventName).forEach((listener) => { - listener.listenToDirective(directiveInstance, observablePropName); - }); - }); +function handleEventClosure(compileElement: CompileElement) { + const handleEventMethodName = getHandleEventMethodName(compileElement.nodeIndex); + return o.THIS_EXPR.callMethod( + 'eventHandler', + [o.THIS_EXPR.prop(handleEventMethodName).callMethod(o.BuiltinMethod.Bind, [o.THIS_EXPR])]); } -export function bindRenderOutputs(eventListeners: CompileEventListener[]) { - eventListeners.forEach(listener => { - // the animation listeners are handled within property_binder.ts to - // allow them to be placed next to the animation factory statements - if (!listener.isAnimation) { - listener.listenToRenderer(); - } - }); -} - -function convertStmtIntoExpression(stmt: o.Statement): o.Expression { - if (stmt instanceof o.ExpressionStatement) { - return stmt.expr; - } else if (stmt instanceof o.ReturnStatement) { - return stmt.value; - } - return null; -} - -function sanitizeEventName(name: string): string { - return name.replace(/[^a-zA-Z_]/g, '_'); -} +type EventSummary = { + name: string, + target: string, + phase: string +} \ No newline at end of file diff --git a/modules/@angular/compiler/src/view_compiler/lifecycle_binder.ts b/modules/@angular/compiler/src/view_compiler/lifecycle_binder.ts index af364f029b..e3864349e6 100644 --- a/modules/@angular/compiler/src/view_compiler/lifecycle_binder.ts +++ b/modules/@angular/compiler/src/view_compiler/lifecycle_binder.ts @@ -7,16 +7,15 @@ */ import {CompileDirectiveMetadata, CompilePipeMetadata} from '../compile_metadata'; +import {DirectiveWrapperExpressions} from '../directive_wrapper_compiler'; import * as o from '../output/output_ast'; import {LifecycleHooks} from '../private_import_core'; -import {DirectiveAst, ProviderAst} from '../template_parser/template_ast'; +import {DirectiveAst, ProviderAst, ProviderAstType} from '../template_parser/template_ast'; import {CompileElement} from './compile_element'; import {CompileView} from './compile_view'; import {DetectChangesVars} from './constants'; - - var STATE_IS_NEVER_CHECKED = o.THIS_EXPR.prop('numberOfChecks').identical(new o.LiteralExpr(0)); var NOT_THROW_ON_CHANGES = o.not(DetectChangesVars.throwOnChange); @@ -56,11 +55,24 @@ export function bindDirectiveAfterViewLifecycleCallbacks( } } +export function bindDirectiveWrapperLifecycleCallbacks( + dir: DirectiveAst, directiveWrapperIntance: o.Expression, compileElement: CompileElement) { + compileElement.view.destroyMethod.addStmts( + DirectiveWrapperExpressions.ngOnDestroy(dir.directive, directiveWrapperIntance)); + compileElement.view.detachMethod.addStmts(DirectiveWrapperExpressions.ngOnDetach( + dir.hostProperties, directiveWrapperIntance, o.THIS_EXPR, + compileElement.component ? compileElement.appElement.prop('componentView') : o.THIS_EXPR, + compileElement.renderNode)); +} + + export function bindInjectableDestroyLifecycleCallbacks( provider: ProviderAst, providerInstance: o.Expression, compileElement: CompileElement) { var onDestroyMethod = compileElement.view.destroyMethod; onDestroyMethod.resetDebugInfo(compileElement.nodeIndex, compileElement.sourceAst); - if (provider.lifecycleHooks.indexOf(LifecycleHooks.OnDestroy) !== -1) { + if (provider.providerType !== ProviderAstType.Directive && + provider.providerType !== ProviderAstType.Component && + provider.lifecycleHooks.indexOf(LifecycleHooks.OnDestroy) !== -1) { onDestroyMethod.addStmt(providerInstance.callMethod('ngOnDestroy', []).toStmt()); } } diff --git a/modules/@angular/compiler/src/view_compiler/property_binder.ts b/modules/@angular/compiler/src/view_compiler/property_binder.ts index 510fcfa098..a9426893bc 100644 --- a/modules/@angular/compiler/src/view_compiler/property_binder.ts +++ b/modules/@angular/compiler/src/view_compiler/property_binder.ts @@ -11,21 +11,22 @@ import {SecurityContext} from '@angular/core'; import {createCheckBindingField, createCheckBindingStmt} from '../compiler_util/binding_util'; import {ConvertPropertyBindingResult, convertPropertyBinding} from '../compiler_util/expression_converter'; import {createEnumExpression} from '../compiler_util/identifier_util'; -import {writeToRenderer} from '../compiler_util/render_util'; +import {triggerAnimation, writeToRenderer} from '../compiler_util/render_util'; +import {DirectiveWrapperExpressions} from '../directive_wrapper_compiler'; import * as cdAst from '../expression_parser/ast'; import {isPresent} from '../facade/lang'; import {Identifiers, resolveIdentifier} from '../identifiers'; import * as o from '../output/output_ast'; import {EMPTY_STATE as EMPTY_ANIMATION_STATE, LifecycleHooks, isDefaultChangeDetectionStrategy} from '../private_import_core'; import {ElementSchemaRegistry} from '../schema/element_schema_registry'; -import {BoundElementPropertyAst, BoundTextAst, DirectiveAst, PropertyBindingType} from '../template_parser/template_ast'; +import {BoundElementPropertyAst, BoundEventAst, BoundTextAst, DirectiveAst, PropertyBindingType} from '../template_parser/template_ast'; import {camelCaseToDashCase} from '../util'; import {CompileElement, CompileNode} from './compile_element'; import {CompileMethod} from './compile_method'; import {CompileView} from './compile_view'; import {DetectChangesVars, ViewProperties} from './constants'; -import {CompileEventListener} from './event_binder'; +import {getHandleEventMethodName} from './util'; export function bindRenderText( boundText: BoundTextAst, compileNode: CompileNode, view: CompileView): void { @@ -44,118 +45,70 @@ export function bindRenderText( .toStmt()])); } -function bindAndWriteToRenderer( - boundProps: BoundElementPropertyAst[], context: o.Expression, compileElement: CompileElement, - isHostProp: boolean, eventListeners: CompileEventListener[]) { +export function bindRenderInputs( + boundProps: BoundElementPropertyAst[], hasEvents: boolean, compileElement: CompileElement) { var view = compileElement.view; var renderNode = compileElement.renderNode; boundProps.forEach((boundProp) => { const bindingField = createCheckBindingField(view); view.detectChangesRenderPropertiesMethod.resetDebugInfo(compileElement.nodeIndex, boundProp); const evalResult = convertPropertyBinding( - view, isHostProp ? null : view, context, boundProp.value, bindingField.bindingId); - var updateStmts: o.Statement[] = []; + view, view, compileElement.view.componentContext, boundProp.value, bindingField.bindingId); + var checkBindingStmts: o.Statement[] = []; var compileMethod = view.detectChangesRenderPropertiesMethod; switch (boundProp.type) { case PropertyBindingType.Property: case PropertyBindingType.Attribute: case PropertyBindingType.Class: case PropertyBindingType.Style: - updateStmts.push(...writeToRenderer( + checkBindingStmts.push(...writeToRenderer( o.THIS_EXPR, boundProp, renderNode, evalResult.currValExpr, view.genConfig.logBindingUpdate)); break; case PropertyBindingType.Animation: compileMethod = view.animationBindingsMethod; - const detachStmts: o.Statement[] = []; - - const animationName = boundProp.name; - const targetViewExpr: o.Expression = - isHostProp ? compileElement.appElement.prop('componentView') : o.THIS_EXPR; - - const animationFnExpr = - targetViewExpr.prop('componentType').prop('animations').key(o.literal(animationName)); - - // it's important to normalize the void value as `void` explicitly - // so that the styles data can be obtained from the stringmap - const emptyStateValue = o.literal(EMPTY_ANIMATION_STATE); - const unitializedValue = o.importExpr(resolveIdentifier(Identifiers.UNINITIALIZED)); - const animationTransitionVar = o.variable('animationTransition_' + animationName); - - updateStmts.push(animationTransitionVar - .set(animationFnExpr.callFn([ - o.THIS_EXPR, renderNode, - bindingField.expression.equals(unitializedValue) - .conditional(emptyStateValue, bindingField.expression), - evalResult.currValExpr.equals(unitializedValue) - .conditional(emptyStateValue, evalResult.currValExpr) - ])) - .toDeclStmt()); - - detachStmts.push( - animationTransitionVar - .set(animationFnExpr.callFn( - [o.THIS_EXPR, renderNode, bindingField.expression, emptyStateValue])) - .toDeclStmt()); - - eventListeners.forEach(listener => { - if (listener.isAnimation && listener.eventName === animationName) { - let animationStmt = listener.listenToAnimation(animationTransitionVar); - updateStmts.push(animationStmt); - detachStmts.push(animationStmt); - } - }); - + const {updateStmts, detachStmts} = triggerAnimation( + o.THIS_EXPR, o.THIS_EXPR, boundProp, + (hasEvents ? o.THIS_EXPR.prop(getHandleEventMethodName(compileElement.nodeIndex)) : + o.importExpr(resolveIdentifier(Identifiers.noop))) + .callMethod(o.BuiltinMethod.Bind, [o.THIS_EXPR]), + compileElement.renderNode, evalResult.currValExpr, bindingField.expression); + checkBindingStmts.push(...updateStmts); view.detachMethod.addStmts(detachStmts); - break; } compileMethod.addStmts(createCheckBindingStmt( - evalResult, bindingField.expression, DetectChangesVars.throwOnChange, updateStmts)); + evalResult, bindingField.expression, DetectChangesVars.throwOnChange, checkBindingStmts)); }); } -export function bindRenderInputs( - boundProps: BoundElementPropertyAst[], compileElement: CompileElement, - eventListeners: CompileEventListener[]): void { - bindAndWriteToRenderer( - boundProps, compileElement.view.componentContext, compileElement, false, eventListeners); -} - export function bindDirectiveHostProps( directiveAst: DirectiveAst, directiveWrapperInstance: o.Expression, - compileElement: CompileElement, eventListeners: CompileEventListener[], elementName: string, + compileElement: CompileElement, elementName: string, schemaRegistry: ElementSchemaRegistry): void { - // host properties are change detected by the DirectiveWrappers, - // except for the animation properties as they need close integration with animation events - // and DirectiveWrappers don't support - // event listeners right now. - bindAndWriteToRenderer( - directiveAst.hostProperties.filter(boundProp => boundProp.isAnimation), - directiveWrapperInstance.prop('context'), compileElement, true, eventListeners); - - - const methodArgs: o.Expression[] = - [o.THIS_EXPR, compileElement.renderNode, DetectChangesVars.throwOnChange]; // We need to provide the SecurityContext for properties that could need sanitization. - directiveAst.hostProperties.filter(boundProp => boundProp.needsRuntimeSecurityContext) - .forEach((boundProp) => { - let ctx: SecurityContext; - switch (boundProp.type) { - case PropertyBindingType.Property: - ctx = schemaRegistry.securityContext(elementName, boundProp.name, false); - break; - case PropertyBindingType.Attribute: - ctx = schemaRegistry.securityContext(elementName, boundProp.name, true); - break; - default: - throw new Error( - `Illegal state: Only property / attribute bindings can have an unknown security context! Binding ${boundProp.name}`); - } - methodArgs.push(createEnumExpression(Identifiers.SecurityContext, ctx)); - }); - compileElement.view.detectChangesRenderPropertiesMethod.addStmt( - directiveWrapperInstance.callMethod('detectChangesInHostProps', methodArgs).toStmt()); + const runtimeSecurityCtxExprs = + directiveAst.hostProperties.filter(boundProp => boundProp.needsRuntimeSecurityContext) + .map((boundProp) => { + let ctx: SecurityContext; + switch (boundProp.type) { + case PropertyBindingType.Property: + ctx = schemaRegistry.securityContext(elementName, boundProp.name, false); + break; + case PropertyBindingType.Attribute: + ctx = schemaRegistry.securityContext(elementName, boundProp.name, true); + break; + default: + throw new Error( + `Illegal state: Only property / attribute bindings can have an unknown security context! Binding ${boundProp.name}`); + } + return createEnumExpression(Identifiers.SecurityContext, ctx); + }); + compileElement.view.detectChangesRenderPropertiesMethod.addStmts( + DirectiveWrapperExpressions.checkHost( + directiveAst.hostProperties, directiveWrapperInstance, o.THIS_EXPR, + compileElement.component ? compileElement.appElement.prop('componentView') : o.THIS_EXPR, + compileElement.renderNode, DetectChangesVars.throwOnChange, runtimeSecurityCtxExprs)); } export function bindDirectiveInputs( @@ -187,9 +140,9 @@ export function bindDirectiveInputs( }); var isOnPushComp = directiveAst.directive.isComponent && !isDefaultChangeDetectionStrategy(directiveAst.directive.changeDetection); - let directiveDetectChangesExpr = directiveWrapperInstance.callMethod( - 'detectChangesInInputProps', - [o.THIS_EXPR, compileElement.renderNode, DetectChangesVars.throwOnChange]); + let directiveDetectChangesExpr = DirectiveWrapperExpressions.ngDoCheck( + directiveWrapperInstance, o.THIS_EXPR, compileElement.renderNode, + DetectChangesVars.throwOnChange); const directiveDetectChangesStmt = isOnPushComp ? new o.IfStmt(directiveDetectChangesExpr, [compileElement.appElement.prop('componentView') .callMethod('markAsCheckOnce', []) diff --git a/modules/@angular/compiler/src/view_compiler/util.ts b/modules/@angular/compiler/src/view_compiler/util.ts index 27b3dd51ae..da9f9e29d3 100644 --- a/modules/@angular/compiler/src/view_compiler/util.ts +++ b/modules/@angular/compiler/src/view_compiler/util.ts @@ -91,3 +91,7 @@ export function createFlatArray(expressions: o.Expression[]): o.Expression { } return result; } + +export function getHandleEventMethodName(elementIndex: number): string { + return `handleEvent_${elementIndex}`; +} \ No newline at end of file diff --git a/modules/@angular/compiler/src/view_compiler/view_binder.ts b/modules/@angular/compiler/src/view_compiler/view_binder.ts index c8444b00d4..3984fcdc43 100644 --- a/modules/@angular/compiler/src/view_compiler/view_binder.ts +++ b/modules/@angular/compiler/src/view_compiler/view_binder.ts @@ -11,8 +11,8 @@ import {AttrAst, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventA import {CompileElement} from './compile_element'; import {CompileView} from './compile_view'; -import {CompileEventListener, bindDirectiveOutputs, bindRenderOutputs, collectEventListeners} from './event_binder'; -import {bindDirectiveAfterContentLifecycleCallbacks, bindDirectiveAfterViewLifecycleCallbacks, bindInjectableDestroyLifecycleCallbacks, bindPipeDestroyLifecycleCallbacks} from './lifecycle_binder'; +import {bindOutputs} from './event_binder'; +import {bindDirectiveAfterContentLifecycleCallbacks, bindDirectiveAfterViewLifecycleCallbacks, bindDirectiveWrapperLifecycleCallbacks, bindInjectableDestroyLifecycleCallbacks, bindPipeDestroyLifecycleCallbacks} from './lifecycle_binder'; import {bindDirectiveHostProps, bindDirectiveInputs, bindRenderInputs, bindRenderText} from './property_binder'; export function bindView( @@ -42,32 +42,29 @@ class ViewBinderVisitor implements TemplateAstVisitor { visitElement(ast: ElementAst, parent: CompileElement): any { var compileElement = this.view.nodes[this._nodeIndex++]; - var eventListeners: CompileEventListener[] = []; - collectEventListeners(ast.outputs, ast.directives, compileElement).forEach(entry => { - eventListeners.push(entry); - }); - bindRenderInputs(ast.inputs, compileElement, eventListeners); - bindRenderOutputs(eventListeners); + const hasEvents = bindOutputs(ast.outputs, ast.directives, compileElement, true); + bindRenderInputs(ast.inputs, hasEvents, compileElement); ast.directives.forEach((directiveAst, dirIndex) => { var directiveInstance = compileElement.instances.get(directiveAst.directive.type.reference); var directiveWrapperInstance = compileElement.directiveWrapperInstance.get(directiveAst.directive.type.reference); bindDirectiveInputs(directiveAst, directiveWrapperInstance, dirIndex, compileElement); - bindDirectiveHostProps( - directiveAst, directiveWrapperInstance, compileElement, eventListeners, ast.name, - this._schemaRegistry); - bindDirectiveOutputs(directiveAst, directiveInstance, eventListeners); + directiveAst, directiveWrapperInstance, compileElement, ast.name, this._schemaRegistry); }); templateVisitAll(this, ast.children, compileElement); // afterContent and afterView lifecycles need to be called bottom up // so that children are notified before parents ast.directives.forEach((directiveAst) => { var directiveInstance = compileElement.instances.get(directiveAst.directive.type.reference); + var directiveWrapperInstance = + compileElement.directiveWrapperInstance.get(directiveAst.directive.type.reference); bindDirectiveAfterContentLifecycleCallbacks( directiveAst.directive, directiveInstance, compileElement); bindDirectiveAfterViewLifecycleCallbacks( directiveAst.directive, directiveInstance, compileElement); + bindDirectiveWrapperLifecycleCallbacks( + directiveAst, directiveWrapperInstance, compileElement); }); ast.providers.forEach((providerAst) => { var providerInstance = compileElement.instances.get(providerAst.token.reference); @@ -78,18 +75,19 @@ class ViewBinderVisitor implements TemplateAstVisitor { visitEmbeddedTemplate(ast: EmbeddedTemplateAst, parent: CompileElement): any { var compileElement = this.view.nodes[this._nodeIndex++]; - var eventListeners = collectEventListeners(ast.outputs, ast.directives, compileElement); + bindOutputs(ast.outputs, ast.directives, compileElement, false); ast.directives.forEach((directiveAst, dirIndex) => { var directiveInstance = compileElement.instances.get(directiveAst.directive.type.reference); var directiveWrapperInstance = compileElement.directiveWrapperInstance.get(directiveAst.directive.type.reference); bindDirectiveInputs(directiveAst, directiveWrapperInstance, dirIndex, compileElement); - bindDirectiveOutputs(directiveAst, directiveInstance, eventListeners); bindDirectiveAfterContentLifecycleCallbacks( directiveAst.directive, directiveInstance, compileElement); bindDirectiveAfterViewLifecycleCallbacks( directiveAst.directive, directiveInstance, compileElement); + bindDirectiveWrapperLifecycleCallbacks( + directiveAst, directiveWrapperInstance, compileElement); }); ast.providers.forEach((providerAst) => { var providerInstance = compileElement.instances.get(providerAst.token.reference); diff --git a/modules/@angular/core/src/linker/view.ts b/modules/@angular/core/src/linker/view.ts index 1ff412a50b..b86a00d980 100644 --- a/modules/@angular/core/src/linker/view.ts +++ b/modules/@angular/core/src/linker/view.ts @@ -285,7 +285,9 @@ export abstract class AppView { } } - eventHandler(cb: (event?: E) => R): (event?: E) => R { return cb; } + eventHandler(cb: (eventName: string, event?: E) => R): (eventName: string, event?: E) => R { + return cb; + } throwDestroyedError(details: string): void { throw new ViewDestroyedError(details); } } @@ -368,12 +370,12 @@ export class DebugAppView extends AppView { } } - eventHandler(cb: (event?: E) => R): (event?: E) => R { + eventHandler(cb: (eventName: string, event?: E) => R): (eventName: string, event?: E) => R { var superHandler = super.eventHandler(cb); - return (event?: any) => { + return (eventName: string, event?: any) => { this._resetDebug(); try { - return superHandler(event); + return superHandler(eventName, event); } catch (e) { this._rethrowWithContext(e); throw e; diff --git a/modules/@angular/core/src/linker/view_utils.ts b/modules/@angular/core/src/linker/view_utils.ts index c2ee91985d..44c3e49717 100644 --- a/modules/@angular/core/src/linker/view_utils.ts +++ b/modules/@angular/core/src/linker/view_utils.ts @@ -400,14 +400,59 @@ export function selectOrCreateRenderHostElement( return hostElement; } +export function subscribeToRenderElement( + renderer: Renderer, element: any, eventNamesAndTargets: InlineArray, + listener: (eventName: string, event: any) => any) { + const disposables = createEmptyInlineArray(eventNamesAndTargets.length / 2); + for (var i = 0; i < eventNamesAndTargets.length; i += 2) { + const eventName = eventNamesAndTargets.get(i); + const eventTarget = eventNamesAndTargets.get(i + 1); + let disposable: Function; + if (eventTarget) { + disposable = renderer.listenGlobal( + eventTarget, eventName, listener.bind(null, `${eventTarget}:${eventName}`)); + } else { + disposable = renderer.listen(element, eventName, listener.bind(null, eventName)); + } + disposables.set(i / 2, disposable); + } + return disposeInlineArray.bind(null, disposables); +} + +function disposeInlineArray(disposables: InlineArray) { + for (var i = 0; i < disposables.length; i++) { + disposables.get(i)(); + } +} + +export function noop() {} + export interface InlineArray { length: number; get(index: number): T; + set(index: number, value: T): void; +} + +function createEmptyInlineArray(length: number): InlineArray { + let ctor: any; + if (length <= 2) { + ctor = InlineArray2; + } else if (length <= 4) { + ctor = InlineArray4; + } else if (length <= 8) { + ctor = InlineArray8; + } else if (length <= 16) { + ctor = InlineArray16; + } else { + ctor = InlineArrayDynamic; + } + return new ctor(length); } class InlineArray0 implements InlineArray { length = 0; get(index: number): any { return undefined; } + set(index: number, value: any): void {} } export class InlineArray2 implements InlineArray { @@ -422,6 +467,16 @@ export class InlineArray2 implements InlineArray { return undefined; } } + set(index: number, value: T) { + switch (index) { + case 0: + this._v0 = value; + break; + case 1: + this._v1 = value; + break; + } + } } export class InlineArray4 implements InlineArray { @@ -441,6 +496,22 @@ export class InlineArray4 implements InlineArray { return undefined; } } + set(index: number, value: T) { + switch (index) { + case 0: + this._v0 = value; + break; + case 1: + this._v1 = value; + break; + case 2: + this._v2 = value; + break; + case 3: + this._v3 = value; + break; + } + } } export class InlineArray8 implements InlineArray { @@ -469,6 +540,34 @@ export class InlineArray8 implements InlineArray { return undefined; } } + set(index: number, value: T) { + switch (index) { + case 0: + this._v0 = value; + break; + case 1: + this._v1 = value; + break; + case 2: + this._v2 = value; + break; + case 3: + this._v3 = value; + break; + case 4: + this._v4 = value; + break; + case 5: + this._v5 = value; + break; + case 6: + this._v6 = value; + break; + case 7: + this._v7 = value; + break; + } + } } export class InlineArray16 implements InlineArray { @@ -515,6 +614,58 @@ export class InlineArray16 implements InlineArray { return undefined; } } + set(index: number, value: T) { + switch (index) { + case 0: + this._v0 = value; + break; + case 1: + this._v1 = value; + break; + case 2: + this._v2 = value; + break; + case 3: + this._v3 = value; + break; + case 4: + this._v4 = value; + break; + case 5: + this._v5 = value; + break; + case 6: + this._v6 = value; + break; + case 7: + this._v7 = value; + break; + case 8: + this._v8 = value; + break; + case 9: + this._v9 = value; + break; + case 10: + this._v10 = value; + break; + case 11: + this._v11 = value; + break; + case 12: + this._v12 = value; + break; + case 13: + this._v13 = value; + break; + case 14: + this._v14 = value; + break; + case 15: + this._v15 = value; + break; + } + } } export class InlineArrayDynamic implements InlineArray { @@ -524,6 +675,7 @@ export class InlineArrayDynamic implements InlineArray { constructor(public length: number, ...values: any[]) { this._values = values; } get(index: number) { return this._values[index]; } + set(index: number, value: T) { this._values[index] = value; } } export const EMPTY_INLINE_ARRAY: InlineArray = new InlineArray0();