/** * @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 */ import {Injectable} from '@angular/core'; import {CompileDirectiveMetadata, CompileDirectiveSummary, CompileIdentifierMetadata, identifierModuleUrl, identifierName} from './compile_metadata'; import {createCheckBindingField, createCheckBindingStmt} from './compiler_util/binding_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, createIdentifier} from './identifiers'; import {DEFAULT_INTERPOLATION_CONFIG} from './ml_parser/interpolation_config'; import {ClassBuilder, createClassStmt} from './output/class_builder'; import * as o from './output/output_ast'; import {ParseError, ParseErrorLevel, ParseLocation, ParseSourceFile, ParseSourceSpan} from './parse_util'; import {Console, LifecycleHooks, isDefaultChangeDetectionStrategy} from './private_import_core'; import {ElementSchemaRegistry} from './schema/element_schema_registry'; import {BindingParser} from './template_parser/binding_parser'; import {BoundElementPropertyAst, BoundEventAst} from './template_parser/template_ast'; export class DirectiveWrapperCompileResult { constructor(public statements: o.Statement[], public dirWrapperClassVar: string) {} } 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(); /** * We generate directive wrappers to prevent code bloat when a directive is used. * A directive wrapper encapsulates * the dirty checking for `@Input`, the handling of `@HostListener` / `@HostBinding` * and calling the lifecyclehooks `ngOnInit`, `ngOnChanges`, `ngDoCheck`. * * So far, only `@Input` and the lifecycle hooks have been implemented. */ @Injectable() export class DirectiveWrapperCompiler { static dirWrapperClassName(id: CompileIdentifierMetadata) { return `Wrapper_${identifierName(id)}`; } constructor( private compilerConfig: CompilerConfig, private _exprParser: Parser, 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); }); addNgDoCheckMethod(builder); addCheckHostMethod(hostParseResult.hostProps, builder); addHandleEventMethod(hostParseResult.hostListeners, builder); addSubscribeMethod(dirMeta, builder); const classStmt = builder.build(); return new DirectiveWrapperCompileResult([classStmt], classStmt.name); } } class DirectiveWrapperBuilder implements ClassBuilder { fields: o.ClassField[] = []; 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; this.genChanges = dirLifecycleHooks.indexOf(LifecycleHooks.OnChanges) !== -1 || this.compilerConfig.logBindingUpdate; 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 { const dirDepParamNames: string[] = []; for (let i = 0; i < this.dirMeta.type.diDeps.length; i++) { dirDepParamNames.push(`p${i}`); } const methods = [ new o.ClassMethod( 'ngOnDetach', [ new o.FnParam( VIEW_VAR.name, o.importType(createIdentifier(Identifiers.AppView), [o.DYNAMIC_TYPE])), new o.FnParam( COMPONENT_VIEW_VAR.name, o.importType(createIdentifier(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, [o.StmtModifier.Private]), new o.ClassField(CONTEXT_FIELD_NAME, o.importType(this.dirMeta.type)), new o.ClassField(CHANGED_FIELD_NAME, o.BOOL_TYPE, [o.StmtModifier.Private]), ]; const ctorStmts: o.Statement[] = [o.THIS_EXPR.prop(CHANGED_FIELD_NAME).set(o.literal(false)).toStmt()]; if (this.genChanges) { fields.push(new o.ClassField( CHANGES_FIELD_NAME, new o.MapType(o.DYNAMIC_TYPE), [o.StmtModifier.Private])); ctorStmts.push(RESET_CHANGES_STMT); } ctorStmts.push( o.THIS_EXPR.prop(CONTEXT_FIELD_NAME) .set(o.importExpr(this.dirMeta.type) .instantiate(dirDepParamNames.map((paramName) => o.variable(paramName)))) .toStmt()); return createClassStmt({ name: DirectiveWrapperCompiler.dirWrapperClassName(this.dirMeta.type), ctorParams: dirDepParamNames.map((paramName) => new o.FnParam(paramName, o.DYNAMIC_TYPE)), builders: [{fields, ctorStmts, methods}, this] }); } } function addNgDoCheckMethod(builder: DirectiveWrapperBuilder) { const changedVar = o.variable('changed'); const stmts: o.Statement[] = [ changedVar.set(o.THIS_EXPR.prop(CHANGED_FIELD_NAME)).toDeclStmt(), o.THIS_EXPR.prop(CHANGED_FIELD_NAME).set(o.literal(false)).toStmt(), ]; const lifecycleStmts: o.Statement[] = []; if (builder.genChanges) { const onChangesStmts: o.Statement[] = []; if (builder.ngOnChanges) { onChangesStmts.push(o.THIS_EXPR.prop(CONTEXT_FIELD_NAME) .callMethod('ngOnChanges', [o.THIS_EXPR.prop(CHANGES_FIELD_NAME)]) .toStmt()); } if (builder.compilerConfig.logBindingUpdate) { onChangesStmts.push( o.importExpr(createIdentifier(Identifiers.setBindingDebugInfoForChanges)) .callFn( [VIEW_VAR.prop('renderer'), RENDER_EL_VAR, o.THIS_EXPR.prop(CHANGES_FIELD_NAME)]) .toStmt()); } onChangesStmts.push(RESET_CHANGES_STMT); lifecycleStmts.push(new o.IfStmt(changedVar, onChangesStmts)); } if (builder.ngOnInit) { lifecycleStmts.push(new o.IfStmt( VIEW_VAR.prop('numberOfChecks').identical(new o.LiteralExpr(0)), [o.THIS_EXPR.prop(CONTEXT_FIELD_NAME).callMethod('ngOnInit', []).toStmt()])); } if (builder.ngDoCheck) { lifecycleStmts.push(o.THIS_EXPR.prop(CONTEXT_FIELD_NAME).callMethod('ngDoCheck', []).toStmt()); } if (lifecycleStmts.length > 0) { stmts.push(new o.IfStmt(o.not(THROW_ON_CHANGE_VAR), lifecycleStmts)); } stmts.push(new o.ReturnStatement(changedVar)); builder.methods.push(new o.ClassMethod( 'ngDoCheck', [ new o.FnParam( VIEW_VAR.name, o.importType(createIdentifier(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), ], stmts, o.BOOL_TYPE)); } function addCheckInputMethod(input: string, builder: DirectiveWrapperBuilder) { const field = createCheckBindingField(builder); const onChangeStatements: o.Statement[] = [ o.THIS_EXPR.prop(CHANGED_FIELD_NAME).set(o.literal(true)).toStmt(), o.THIS_EXPR.prop(CONTEXT_FIELD_NAME).prop(input).set(CURR_VALUE_VAR).toStmt(), ]; if (builder.genChanges) { onChangeStatements.push(o.THIS_EXPR.prop(CHANGES_FIELD_NAME) .key(o.literal(input)) .set(o.importExpr(createIdentifier(Identifiers.SimpleChange)) .instantiate([field.expression, CURR_VALUE_VAR])) .toStmt()); } const methodBody: o.Statement[] = createCheckBindingStmt( {currValExpr: CURR_VALUE_VAR, forceUpdate: FORCE_UPDATE_VAR, stmts: []}, field.expression, THROW_ON_CHANGE_VAR, onChangeStatements); builder.methods.push(new o.ClassMethod( `check_${input}`, [ new o.FnParam(CURR_VALUE_VAR.name, o.DYNAMIC_TYPE), new o.FnParam(THROW_ON_CHANGE_VAR.name, o.BOOL_TYPE), new o.FnParam(FORCE_UPDATE_VAR.name, o.BOOL_TYPE), ], methodBody)); } function addCheckHostMethod( hostProps: BoundElementPropertyAst[], builder: DirectiveWrapperBuilder) { const stmts: o.Statement[] = []; const methodParams: o.FnParam[] = [ new o.FnParam( VIEW_VAR.name, o.importType(createIdentifier(Identifiers.AppView), [o.DYNAMIC_TYPE])), new o.FnParam( COMPONENT_VIEW_VAR.name, o.importType(createIdentifier(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, hostPropIdx) => { const field = createCheckBindingField(builder); const evalResult = convertPropertyBinding( builder, null, o.THIS_EXPR.prop(CONTEXT_FIELD_NAME), hostProp.value, field.bindingId); if (!evalResult) { return; } let securityContextExpr: o.ReadVarExpr; if (hostProp.needsRuntimeSecurityContext) { securityContextExpr = o.variable(`secCtx_${methodParams.length}`); methodParams.push(new o.FnParam( securityContextExpr.name, o.importType(createIdentifier(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(createIdentifier(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, checkBindingStmts)); }); 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( VIEW_VAR.name, o.importType(createIdentifier(Identifiers.AppView), [o.DYNAMIC_TYPE])), 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, [VIEW_VAR, 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 { constructor( public hostProps: BoundElementPropertyAst[], public hostListeners: BoundEventAst[], public errors: ParseError[]) {} } function parseHostBindings( dirMeta: CompileDirectiveMetadata, exprParser: Parser, schemaRegistry: ElementSchemaRegistry): ParseResult { const errors: ParseError[] = []; const parser = new BindingParser(exprParser, DEFAULT_INTERPOLATION_CONFIG, schemaRegistry, [], errors); const moduleUrl = identifierModuleUrl(dirMeta.type); const sourceFileName = moduleUrl ? `in Directive ${identifierName(dirMeta.type)} in ${moduleUrl}` : `in Directive ${identifierName(dirMeta.type)}`; const sourceFile = new ParseSourceFile('', sourceFileName); const sourceSpan = new ParseSourceSpan( new ParseLocation(sourceFile, null, null, null), new ParseLocation(sourceFile, null, null, null)); const parsedHostProps = parser.createDirectiveHostPropertyAsts(dirMeta.toSummary(), sourceSpan); const parsedHostListeners = parser.createDirectiveHostEventAsts(dirMeta.toSummary(), sourceSpan); return new ParseResult(parsedHostProps, parsedHostListeners, errors); } function reportParseErrors(parseErrors: ParseError[], console: Console) { const warnings = parseErrors.filter(error => error.level === ParseErrorLevel.WARNING); const errors = parseErrors.filter(error => error.level === ParseErrorLevel.FATAL); if (warnings.length > 0) { this._console.warn(`Directive parse warnings:\n${warnings.join('\n')}`); } if (errors.length > 0) { throw new Error(`Directive parse errors:\n${errors.join('\n')}`); } } 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: CompileDirectiveSummary, 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: CompileDirectiveSummary, hostProps: BoundElementPropertyAst[], usedEvents: string[], dirWrapper: o.Expression, view: o.Expression, eventListener: o.Expression): o.Statement[] { let needsSubscribe = false; const 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', [view, 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]); } }