angular-docs-cn/modules/@angular/compiler/src/directive_wrapper_compiler.ts
Tobias Bosch 2f7492c986 refactor(compiler): remove unneeded fields from metadata
Removes `CompileIdentifierMetadata.name` / `.moduleUrl`,
as well as `CompileTypeMetadata.name / moduleUrl` and
`CompileFactoryMetadata.name / moduleUrl`.
2016-11-28 15:19:23 -08:00

462 lines
19 KiB
TypeScript

/**
* @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]);
}
}