refactor(compiler): move host properties into DirectiveWrapper

Part of #11683
This commit is contained in:
Tobias Bosch
2016-10-24 09:58:52 -07:00
committed by vsavkin
parent 5a7a58b1e0
commit 178fb79b5c
19 changed files with 409 additions and 175 deletions

View File

@ -8,7 +8,7 @@
import {CompileTokenMetadata} from '../compile_metadata';
import {isPresent} from '../facade/lang';
import {Identifiers, resolveIdentifier} from '../identifiers';
import {IdentifierSpec, Identifiers, resolveEnumIdentifier, resolveIdentifier} from '../identifiers';
import * as o from '../output/output_ast';
export function createDiTokenExpression(token: CompileTokenMetadata): o.Expression {
@ -49,3 +49,12 @@ export function createPureProxy(
.set(o.importExpr(resolveIdentifier(pureProxyId)).callFn([fn]))
.toStmt());
}
export function createEnumExpression(enumType: IdentifierSpec, enumValue: any): o.Expression {
const enumName =
Object.keys(enumType.runtime).find((propName) => enumType.runtime[propName] === enumValue);
if (!enumName) {
throw new Error(`Unknown enum value ${enumValue} in ${enumType.name}`);
}
return o.importExpr(resolveEnumIdentifier(resolveIdentifier(enumType), enumName));
}

View File

@ -12,23 +12,27 @@ import {Identifiers, resolveIdentifier} from '../identifiers';
import * as o from '../output/output_ast';
import {BoundElementPropertyAst, PropertyBindingType} from '../template_parser/template_ast';
import {createEnumExpression} from './identifier_util';
export function writeToRenderer(
view: o.Expression, boundProp: BoundElementPropertyAst, renderNode: o.Expression,
renderValue: o.Expression, logBindingUpdate: boolean): o.Statement[] {
view: o.Expression, boundProp: BoundElementPropertyAst, renderElement: o.Expression,
renderValue: o.Expression, logBindingUpdate: boolean,
securityContextExpression?: o.Expression): o.Statement[] {
const updateStmts: o.Statement[] = [];
const renderer = view.prop('renderer');
renderValue = sanitizedValue(view, boundProp, renderValue);
renderValue = sanitizedValue(view, boundProp, renderValue, securityContextExpression);
switch (boundProp.type) {
case PropertyBindingType.Property:
if (logBindingUpdate) {
updateStmts.push(o.importExpr(resolveIdentifier(Identifiers.setBindingDebugInfo))
.callFn([renderer, renderNode, o.literal(boundProp.name), renderValue])
.toStmt());
updateStmts.push(
o.importExpr(resolveIdentifier(Identifiers.setBindingDebugInfo))
.callFn([renderer, renderElement, o.literal(boundProp.name), renderValue])
.toStmt());
}
updateStmts.push(
renderer
.callMethod(
'setElementProperty', [renderNode, o.literal(boundProp.name), renderValue])
'setElementProperty', [renderElement, o.literal(boundProp.name), renderValue])
.toStmt());
break;
case PropertyBindingType.Attribute:
@ -37,13 +41,14 @@ export function writeToRenderer(
updateStmts.push(
renderer
.callMethod(
'setElementAttribute', [renderNode, o.literal(boundProp.name), renderValue])
'setElementAttribute', [renderElement, o.literal(boundProp.name), renderValue])
.toStmt());
break;
case PropertyBindingType.Class:
updateStmts.push(
renderer
.callMethod('setElementClass', [renderNode, o.literal(boundProp.name), renderValue])
.callMethod(
'setElementClass', [renderElement, o.literal(boundProp.name), renderValue])
.toStmt());
break;
case PropertyBindingType.Style:
@ -55,7 +60,8 @@ export function writeToRenderer(
renderValue = renderValue.isBlank().conditional(o.NULL_EXPR, strValue);
updateStmts.push(
renderer
.callMethod('setElementStyle', [renderNode, o.literal(boundProp.name), renderValue])
.callMethod(
'setElementStyle', [renderElement, o.literal(boundProp.name), renderValue])
.toStmt());
break;
case PropertyBindingType.Animation:
@ -65,32 +71,19 @@ export function writeToRenderer(
}
function sanitizedValue(
view: o.Expression, boundProp: BoundElementPropertyAst,
renderValue: o.Expression): o.Expression {
let enumValue: string;
switch (boundProp.securityContext) {
case SecurityContext.NONE:
return renderValue; // No sanitization needed.
case SecurityContext.HTML:
enumValue = 'HTML';
break;
case SecurityContext.STYLE:
enumValue = 'STYLE';
break;
case SecurityContext.SCRIPT:
enumValue = 'SCRIPT';
break;
case SecurityContext.URL:
enumValue = 'URL';
break;
case SecurityContext.RESOURCE_URL:
enumValue = 'RESOURCE_URL';
break;
default:
throw new Error(`internal error, unexpected SecurityContext ${boundProp.securityContext}.`);
view: o.Expression, boundProp: BoundElementPropertyAst, renderValue: o.Expression,
securityContextExpression?: o.Expression): o.Expression {
if (boundProp.securityContext === SecurityContext.NONE) {
return renderValue; // No sanitization needed.
}
if (!boundProp.needsRuntimeSecurityContext) {
securityContextExpression =
createEnumExpression(Identifiers.SecurityContext, boundProp.securityContext);
}
if (!securityContextExpression) {
throw new Error(`internal error, no SecurityContext given ${boundProp.name}`);
}
let ctx = view.prop('viewUtils').prop('sanitizer');
let args =
[o.importExpr(resolveIdentifier(Identifiers.SecurityContext)).prop(enumValue), renderValue];
let args = [securityContextExpression, renderValue];
return ctx.callMethod('sanitize', args);
}

View File

@ -10,11 +10,19 @@ 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 {CompilerConfig} from './config';
import {Parser} from './expression_parser/parser';
import {Identifiers, resolveIdentifier} 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 {LifecycleHooks, isDefaultChangeDetectionStrategy} from './private_import_core';
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) {}
@ -44,14 +52,28 @@ const RESET_CHANGES_STMT = o.THIS_EXPR.prop(CHANGES_FIELD_NAME).set(o.literalMap
export class DirectiveWrapperCompiler {
static dirWrapperClassName(id: CompileIdentifierMetadata) { return `Wrapper_${id.name}`; }
constructor(private compilerConfig: CompilerConfig) {}
constructor(
private compilerConfig: CompilerConfig, private _exprParser: Parser,
private _schemaRegistry: ElementSchemaRegistry, private _console: Console) {}
compile(dirMeta: CompileDirectiveMetadata): DirectiveWrapperCompileResult {
const builder = new DirectiveWrapperBuilder(this.compilerConfig, dirMeta);
Object.keys(dirMeta.inputs).forEach((inputFieldName) => {
addCheckInputMethod(inputFieldName, builder);
});
addDetectChangesInternalMethod(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!
const classStmt = builder.build();
return new DirectiveWrapperCompileResult([classStmt], classStmt.name);
}
@ -108,7 +130,7 @@ class DirectiveWrapperBuilder implements ClassBuilder {
}
}
function addDetectChangesInternalMethod(builder: DirectiveWrapperBuilder) {
function addDetectChangesInInputPropsMethod(builder: DirectiveWrapperBuilder) {
const changedVar = o.variable('changed');
const stmts: o.Statement[] = [
changedVar.set(o.THIS_EXPR.prop(CHANGED_FIELD_NAME)).toDeclStmt(),
@ -148,7 +170,7 @@ function addDetectChangesInternalMethod(builder: DirectiveWrapperBuilder) {
stmts.push(new o.ReturnStatement(changedVar));
builder.methods.push(new o.ClassMethod(
'detectChangesInternal',
'detectChangesInInputProps',
[
new o.FnParam(
VIEW_VAR.name, o.importType(resolveIdentifier(Identifiers.AppView), [o.DYNAMIC_TYPE])),
@ -184,3 +206,72 @@ function addCheckInputMethod(input: string, builder: DirectiveWrapperBuilder) {
],
methodBody));
}
function addDetectChangesInHostPropsMethod(
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(RENDER_EL_VAR.name, o.DYNAMIC_TYPE),
new o.FnParam(THROW_ON_CHANGE_VAR.name, o.BOOL_TYPE),
];
hostProps.forEach((hostProp) => {
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(resolveIdentifier(Identifiers.SecurityContext))));
}
stmts.push(...createCheckBindingStmt(
evalResult, field.expression, THROW_ON_CHANGE_VAR,
writeToRenderer(
VIEW_VAR, hostProp, RENDER_EL_VAR, evalResult.currValExpr,
builder.compilerConfig.logBindingUpdate, securityContextExpr)));
});
builder.methods.push(new o.ClassMethod('detectChangesInHostProps', 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 sourceFileName = dirMeta.type.moduleUrl ?
`in Directive ${dirMeta.type.name} in ${dirMeta.type.moduleUrl}` :
`in Directive ${dirMeta.type.name}`;
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, sourceSpan);
const parsedHostListeners = parser.createDirectiveHostEventAsts(dirMeta, 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')}`);
}
}

View File

@ -328,7 +328,12 @@ export class DomElementSchemaRegistry extends ElementSchemaRegistry {
* 'NONE' security context, i.e. that they are safe inert string values. Only specific well known
* attack vectors are assigned their appropriate context.
*/
securityContext(tagName: string, propName: string): SecurityContext {
securityContext(tagName: string, propName: string, isAttribute: boolean): SecurityContext {
if (isAttribute) {
// NB: For security purposes, use the mapped property name, not the attribute name.
propName = this.getMappedPropName(propName);
}
// Make sure comparisons are case insensitive, so that case differences between attribute and
// property names do not have a security impact.
tagName = tagName.toLowerCase();
@ -366,4 +371,6 @@ export class DomElementSchemaRegistry extends ElementSchemaRegistry {
return {error: false};
}
}
allKnownElementNames(): string[] { return Object.keys(this._schema); }
}

View File

@ -6,12 +6,14 @@
* found in the LICENSE file at https://angular.io/license
*/
import {SchemaMetadata} from '@angular/core';
import {SchemaMetadata, SecurityContext} from '@angular/core';
export abstract class ElementSchemaRegistry {
abstract hasProperty(tagName: string, propName: string, schemaMetas: SchemaMetadata[]): boolean;
abstract hasElement(tagName: string, schemaMetas: SchemaMetadata[]): boolean;
abstract securityContext(tagName: string, propName: string): any;
abstract securityContext(elementName: string, propName: string, isAttribute: boolean):
SecurityContext;
abstract allKnownElementNames(): string[];
abstract getMappedPropName(propName: string): string;
abstract getDefaultComponentElementName(): string;
abstract validateProperty(name: string): {error: boolean, msg?: string};

View File

@ -6,17 +6,18 @@
* found in the LICENSE file at https://angular.io/license
*/
import {SchemaMetadata, SecurityContext} from '@angular/core';
import {SecurityContext} from '@angular/core';
import {CompilePipeMetadata} from '../compile_metadata';
import {CompileDirectiveMetadata, CompilePipeMetadata} from '../compile_metadata';
import {AST, ASTWithSource, BindingPipe, EmptyExpr, Interpolation, LiteralPrimitive, ParserError, RecursiveAstVisitor, TemplateBinding} from '../expression_parser/ast';
import {Parser} from '../expression_parser/parser';
import {isPresent} from '../facade/lang';
import {InterpolationConfig} from '../ml_parser/interpolation_config';
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../ml_parser/interpolation_config';
import {mergeNsAndName} from '../ml_parser/tags';
import {ParseError, ParseErrorLevel, ParseSourceSpan} from '../parse_util';
import {view_utils} from '../private_import_core';
import {ElementSchemaRegistry} from '../schema/element_schema_registry';
import {CssSelector} from '../selector';
import {splitAtColon, splitAtPeriod} from '../util';
import {BoundElementPropertyAst, BoundEventAst, PropertyBindingType, VariableAst} from './template_ast';
@ -55,18 +56,17 @@ export class BindingParser {
constructor(
private _exprParser: Parser, private _interpolationConfig: InterpolationConfig,
private _schemaRegistry: ElementSchemaRegistry, private _schemas: SchemaMetadata[],
pipes: CompilePipeMetadata[], private _targetErrors: ParseError[]) {
private _schemaRegistry: ElementSchemaRegistry, pipes: CompilePipeMetadata[],
private _targetErrors: ParseError[]) {
pipes.forEach(pipe => this.pipesByName.set(pipe.name, pipe));
}
createDirectiveHostPropertyAsts(
elementName: string, hostProps: {[key: string]: string},
sourceSpan: ParseSourceSpan): BoundElementPropertyAst[] {
if (hostProps) {
createDirectiveHostPropertyAsts(dirMeta: CompileDirectiveMetadata, sourceSpan: ParseSourceSpan):
BoundElementPropertyAst[] {
if (dirMeta.hostProperties) {
const boundProps: BoundProperty[] = [];
Object.keys(hostProps).forEach(propName => {
const expression = hostProps[propName];
Object.keys(dirMeta.hostProperties).forEach(propName => {
const expression = dirMeta.hostProperties[propName];
if (typeof expression === 'string') {
this.parsePropertyBinding(propName, expression, true, sourceSpan, [], boundProps);
} else {
@ -75,16 +75,16 @@ export class BindingParser {
sourceSpan);
}
});
return boundProps.map((prop) => this.createElementPropertyAst(elementName, prop));
return boundProps.map((prop) => this.createElementPropertyAst(dirMeta.selector, prop));
}
}
createDirectiveHostEventAsts(hostListeners: {[key: string]: string}, sourceSpan: ParseSourceSpan):
createDirectiveHostEventAsts(dirMeta: CompileDirectiveMetadata, sourceSpan: ParseSourceSpan):
BoundEventAst[] {
if (hostListeners) {
if (dirMeta.hostListeners) {
const targetEventAsts: BoundEventAst[] = [];
Object.keys(hostListeners).forEach(propName => {
const expression = hostListeners[propName];
Object.keys(dirMeta.hostListeners).forEach(propName => {
const expression = dirMeta.hostListeners[propName];
if (typeof expression === 'string') {
this.parseEvent(propName, expression, sourceSpan, [], targetEventAsts);
} else {
@ -240,42 +240,33 @@ export class BindingParser {
}
}
createElementPropertyAst(elementName: string, boundProp: BoundProperty): BoundElementPropertyAst {
createElementPropertyAst(elementSelector: string, boundProp: BoundProperty):
BoundElementPropertyAst {
if (boundProp.isAnimation) {
return new BoundElementPropertyAst(
boundProp.name, PropertyBindingType.Animation, SecurityContext.NONE, boundProp.expression,
null, boundProp.sourceSpan);
boundProp.name, PropertyBindingType.Animation, SecurityContext.NONE, false,
boundProp.expression, null, boundProp.sourceSpan);
}
let unit: string = null;
let bindingType: PropertyBindingType;
let boundPropertyName: string;
const parts = boundProp.name.split(PROPERTY_PARTS_SEPARATOR);
let securityContext: SecurityContext;
let securityContexts: SecurityContext[];
if (parts.length === 1) {
var partValue = parts[0];
boundPropertyName = this._schemaRegistry.getMappedPropName(partValue);
securityContext = this._schemaRegistry.securityContext(elementName, boundPropertyName);
securityContexts = calcPossibleSecurityContexts(
this._schemaRegistry, elementSelector, boundPropertyName, false);
bindingType = PropertyBindingType.Property;
this._validatePropertyOrAttributeName(boundPropertyName, boundProp.sourceSpan, false);
if (!this._schemaRegistry.hasProperty(elementName, boundPropertyName, this._schemas)) {
let errorMsg =
`Can't bind to '${boundPropertyName}' since it isn't a known property of '${elementName}'.`;
if (elementName.indexOf('-') > -1) {
errorMsg +=
`\n1. If '${elementName}' is an Angular component and it has '${boundPropertyName}' input, then verify that it is part of this module.` +
`\n2. If '${elementName}' is a Web Component then add "CUSTOM_ELEMENTS_SCHEMA" to the '@NgModule.schemas' of this component to suppress this message.\n`;
}
this._reportError(errorMsg, boundProp.sourceSpan);
}
} else {
if (parts[0] == ATTRIBUTE_PREFIX) {
boundPropertyName = parts[1];
this._validatePropertyOrAttributeName(boundPropertyName, boundProp.sourceSpan, true);
// NB: For security purposes, use the mapped property name, not the attribute name.
const mapPropName = this._schemaRegistry.getMappedPropName(boundPropertyName);
securityContext = this._schemaRegistry.securityContext(elementName, mapPropName);
securityContexts = calcPossibleSecurityContexts(
this._schemaRegistry, elementSelector, boundPropertyName, true);
const nsSeparatorIdx = boundPropertyName.indexOf(':');
if (nsSeparatorIdx > -1) {
@ -288,22 +279,21 @@ export class BindingParser {
} else if (parts[0] == CLASS_PREFIX) {
boundPropertyName = parts[1];
bindingType = PropertyBindingType.Class;
securityContext = SecurityContext.NONE;
securityContexts = [SecurityContext.NONE];
} else if (parts[0] == STYLE_PREFIX) {
unit = parts.length > 2 ? parts[2] : null;
boundPropertyName = parts[1];
bindingType = PropertyBindingType.Style;
securityContext = SecurityContext.STYLE;
securityContexts = [SecurityContext.STYLE];
} else {
this._reportError(`Invalid property name '${boundProp.name}'`, boundProp.sourceSpan);
bindingType = null;
securityContext = null;
securityContexts = [];
}
}
return new BoundElementPropertyAst(
boundPropertyName, bindingType, securityContext, boundProp.expression, unit,
boundProp.sourceSpan);
boundPropertyName, bindingType, securityContexts.length === 1 ? securityContexts[0] : null,
securityContexts.length > 1, boundProp.expression, unit, boundProp.sourceSpan);
}
parseEvent(
@ -429,3 +419,21 @@ export class PipeCollector extends RecursiveAstVisitor {
function _isAnimationLabel(name: string): boolean {
return name[0] == '@';
}
export function calcPossibleSecurityContexts(
registry: ElementSchemaRegistry, selector: string, propName: string,
isAttribute: boolean): SecurityContext[] {
const ctxs: SecurityContext[] = [];
CssSelector.parse(selector).forEach((selector) => {
const elementNames = selector.element ? [selector.element] : registry.allKnownElementNames();
const notElementNames =
new Set(selector.notSelectors.filter(selector => selector.isElementSelector())
.map((selector) => selector.element));
const possibleElementNames =
elementNames.filter(elementName => !notElementNames.has(elementName));
ctxs.push(...possibleElementNames.map(
elementName => registry.securityContext(elementName, propName, isAttribute)));
});
return ctxs.length === 0 ? [SecurityContext.NONE] : Array.from(new Set(ctxs)).sort();
}

View File

@ -63,8 +63,8 @@ export class AttrAst implements TemplateAst {
export class BoundElementPropertyAst implements TemplateAst {
constructor(
public name: string, public type: PropertyBindingType,
public securityContext: SecurityContext, public value: AST, public unit: string,
public sourceSpan: ParseSourceSpan) {}
public securityContext: SecurityContext, public needsRuntimeSecurityContext: boolean,
public value: AST, public unit: string, public sourceSpan: ParseSourceSpan) {}
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitElementProperty(this, context);
}

View File

@ -137,7 +137,7 @@ export class TemplateParser {
};
}
const bindingParser = new BindingParser(
this._exprParser, interpolationConfig, this._schemaRegistry, schemas, uniqPipes, errors);
this._exprParser, interpolationConfig, this._schemaRegistry, uniqPipes, errors);
const parseVisitor = new TemplateParseVisitor(
providerViewContext, uniqDirectives, bindingParser, this._schemaRegistry, schemas,
errors);
@ -549,10 +549,12 @@ class TemplateParseVisitor implements html.Visitor {
component = directive;
}
const directiveProperties: BoundDirectivePropertyAst[] = [];
const hostProperties = this._bindingParser.createDirectiveHostPropertyAsts(
elementName, directive.hostProperties, sourceSpan);
const hostEvents =
this._bindingParser.createDirectiveHostEventAsts(directive.hostListeners, sourceSpan);
const hostProperties =
this._bindingParser.createDirectiveHostPropertyAsts(directive, sourceSpan);
// Note: We need to check the host properties here as well,
// as we don't know the element name in the DirectiveWrapperCompiler yet.
this._checkPropertiesInSchema(elementName, hostProperties);
const hostEvents = this._bindingParser.createDirectiveHostEventAsts(directive, sourceSpan);
this._createDirectivePropertyAsts(directive.inputs, props, directiveProperties);
elementOrDirectiveRefs.forEach((elOrDirRef) => {
if ((elOrDirRef.value.length === 0 && directive.isComponent) ||
@ -626,6 +628,7 @@ class TemplateParseVisitor implements html.Visitor {
boundElementProps.push(this._bindingParser.createElementPropertyAst(elementName, prop));
}
});
this._checkPropertiesInSchema(elementName, boundElementProps);
return boundElementProps;
}
@ -700,6 +703,22 @@ class TemplateParseVisitor implements html.Visitor {
});
}
private _checkPropertiesInSchema(elementName: string, boundProps: BoundElementPropertyAst[]) {
boundProps.forEach((boundProp) => {
if (boundProp.type === PropertyBindingType.Property &&
!this._schemaRegistry.hasProperty(elementName, boundProp.name, this._schemas)) {
let errorMsg =
`Can't bind to '${boundProp.name}' since it isn't a known property of '${elementName}'.`;
if (elementName.indexOf('-') > -1) {
errorMsg +=
`\n1. If '${elementName}' is an Angular component and it has '${boundProp.name}' input, then verify that it is part of this module.` +
`\n2. If '${elementName}' is a Web Component then add "CUSTOM_ELEMENTS_SCHEMA" to the '@NgModule.schemas' of this component to suppress this message.\n`;
}
this._reportError(errorMsg, boundProp.sourceSpan);
}
});
}
private _reportError(
message: string, sourceSpan: ParseSourceSpan,
level: ParseErrorLevel = ParseErrorLevel.FATAL) {

View File

@ -9,9 +9,9 @@
import {ChangeDetectionStrategy, ViewEncapsulation} from '@angular/core';
import {CompileIdentifierMetadata} from '../compile_metadata';
import {Identifiers, resolveEnumIdentifier, resolveIdentifier} from '../identifiers';
import {createEnumExpression} from '../compiler_util/identifier_util';
import {Identifiers, resolveEnumIdentifier} from '../identifiers';
import * as o from '../output/output_ast';
import {ChangeDetectorStatus, ViewType} from '../private_import_core';
function _enumExpression(classIdentifier: CompileIdentifierMetadata, name: string): o.Expression {
@ -20,69 +20,25 @@ function _enumExpression(classIdentifier: CompileIdentifierMetadata, name: strin
export class ViewTypeEnum {
static fromValue(value: ViewType): o.Expression {
const viewType = resolveIdentifier(Identifiers.ViewType);
switch (value) {
case ViewType.HOST:
return _enumExpression(viewType, 'HOST');
case ViewType.COMPONENT:
return _enumExpression(viewType, 'COMPONENT');
case ViewType.EMBEDDED:
return _enumExpression(viewType, 'EMBEDDED');
default:
throw Error(`Inavlid ViewType value: ${value}`);
}
return createEnumExpression(Identifiers.ViewType, value);
}
}
export class ViewEncapsulationEnum {
static fromValue(value: ViewEncapsulation): o.Expression {
const viewEncapsulation = resolveIdentifier(Identifiers.ViewEncapsulation);
switch (value) {
case ViewEncapsulation.Emulated:
return _enumExpression(viewEncapsulation, 'Emulated');
case ViewEncapsulation.Native:
return _enumExpression(viewEncapsulation, 'Native');
case ViewEncapsulation.None:
return _enumExpression(viewEncapsulation, 'None');
default:
throw Error(`Inavlid ViewEncapsulation value: ${value}`);
}
return createEnumExpression(Identifiers.ViewEncapsulation, value);
}
}
export class ChangeDetectionStrategyEnum {
static fromValue(value: ChangeDetectionStrategy): o.Expression {
const changeDetectionStrategy = resolveIdentifier(Identifiers.ChangeDetectionStrategy);
switch (value) {
case ChangeDetectionStrategy.OnPush:
return _enumExpression(changeDetectionStrategy, 'OnPush');
case ChangeDetectionStrategy.Default:
return _enumExpression(changeDetectionStrategy, 'Default');
default:
throw Error(`Inavlid ChangeDetectionStrategy value: ${value}`);
}
return createEnumExpression(Identifiers.ChangeDetectionStrategy, value);
}
}
export class ChangeDetectorStatusEnum {
static fromValue(value: ChangeDetectorStatusEnum): o.Expression {
const changeDetectorStatus = resolveIdentifier(Identifiers.ChangeDetectorStatus);
switch (value) {
case ChangeDetectorStatus.CheckOnce:
return _enumExpression(changeDetectorStatus, 'CheckOnce');
case ChangeDetectorStatus.Checked:
return _enumExpression(changeDetectorStatus, 'Checked');
case ChangeDetectorStatus.CheckAlways:
return _enumExpression(changeDetectorStatus, 'CheckAlways');
case ChangeDetectorStatus.Detached:
return _enumExpression(changeDetectorStatus, 'Detached');
case ChangeDetectorStatus.Errored:
return _enumExpression(changeDetectorStatus, 'Errored');
case ChangeDetectorStatus.Destroyed:
return _enumExpression(changeDetectorStatus, 'Destroyed');
default:
throw Error(`Inavlid ChangeDetectorStatus value: ${value}`);
}
return createEnumExpression(Identifiers.ChangeDetectorStatus, value);
}
}

View File

@ -10,12 +10,14 @@ 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 * 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 {camelCaseToDashCase} from '../util';
@ -121,10 +123,39 @@ export function bindRenderInputs(
}
export function bindDirectiveHostProps(
directiveAst: DirectiveAst, directiveInstance: o.Expression, compileElement: CompileElement,
eventListeners: CompileEventListener[]): void {
directiveAst: DirectiveAst, directiveWrapperInstance: o.Expression,
compileElement: CompileElement, eventListeners: CompileEventListener[], 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, directiveInstance, compileElement, true, eventListeners);
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());
}
export function bindDirectiveInputs(
@ -157,7 +188,7 @@ export function bindDirectiveInputs(
var isOnPushComp = directiveAst.directive.isComponent &&
!isDefaultChangeDetectionStrategy(directiveAst.directive.changeDetection);
let directiveDetectChangesExpr = directiveWrapperInstance.callMethod(
'detectChangesInternal',
'detectChangesInInputProps',
[o.THIS_EXPR, compileElement.renderNode, DetectChangesVars.throwOnChange]);
const directiveDetectChangesStmt = isOnPushComp ?
new o.IfStmt(directiveDetectChangesExpr, [compileElement.appElement.prop('componentView')

View File

@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ElementSchemaRegistry} from '../schema/element_schema_registry';
import {AttrAst, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, DirectiveAst, ElementAst, EmbeddedTemplateAst, NgContentAst, ReferenceAst, TemplateAst, TemplateAstVisitor, TextAst, VariableAst, templateVisitAll} from '../template_parser/template_ast';
import {CompileElement} from './compile_element';
@ -14,8 +15,9 @@ import {CompileEventListener, bindDirectiveOutputs, bindRenderOutputs, collectEv
import {bindDirectiveAfterContentLifecycleCallbacks, bindDirectiveAfterViewLifecycleCallbacks, bindInjectableDestroyLifecycleCallbacks, bindPipeDestroyLifecycleCallbacks} from './lifecycle_binder';
import {bindDirectiveHostProps, bindDirectiveInputs, bindRenderInputs, bindRenderText} from './property_binder';
export function bindView(view: CompileView, parsedTemplate: TemplateAst[]): void {
var visitor = new ViewBinderVisitor(view);
export function bindView(
view: CompileView, parsedTemplate: TemplateAst[], schemaRegistry: ElementSchemaRegistry): void {
var visitor = new ViewBinderVisitor(view, schemaRegistry);
templateVisitAll(visitor, parsedTemplate);
view.pipes.forEach(
(pipe) => { bindPipeDestroyLifecycleCallbacks(pipe.meta, pipe.instance, pipe.view); });
@ -24,7 +26,7 @@ export function bindView(view: CompileView, parsedTemplate: TemplateAst[]): void
class ViewBinderVisitor implements TemplateAstVisitor {
private _nodeIndex: number = 0;
constructor(public view: CompileView) {}
constructor(public view: CompileView, private _schemaRegistry: ElementSchemaRegistry) {}
visitBoundText(ast: BoundTextAst, parent: CompileElement): any {
var node = this.view.nodes[this._nodeIndex++];
@ -52,7 +54,9 @@ class ViewBinderVisitor implements TemplateAstVisitor {
compileElement.directiveWrapperInstance.get(directiveAst.directive.type.reference);
bindDirectiveInputs(directiveAst, directiveWrapperInstance, dirIndex, compileElement);
bindDirectiveHostProps(directiveAst, directiveInstance, compileElement, eventListeners);
bindDirectiveHostProps(
directiveAst, directiveWrapperInstance, compileElement, eventListeners, ast.name,
this._schemaRegistry);
bindDirectiveOutputs(directiveAst, directiveInstance, eventListeners);
});
templateVisitAll(this, ast.children, compileElement);
@ -91,7 +95,7 @@ class ViewBinderVisitor implements TemplateAstVisitor {
var providerInstance = compileElement.instances.get(providerAst.token.reference);
bindInjectableDestroyLifecycleCallbacks(providerAst, providerInstance, compileElement);
});
bindView(compileElement.embeddedView, ast.children);
bindView(compileElement.embeddedView, ast.children, this._schemaRegistry);
return null;
}

View File

@ -12,6 +12,7 @@ import {AnimationEntryCompileResult} from '../animation/animation_compiler';
import {CompileDirectiveMetadata, CompilePipeMetadata} from '../compile_metadata';
import {CompilerConfig} from '../config';
import * as o from '../output/output_ast';
import {ElementSchemaRegistry} from '../schema/element_schema_registry';
import {TemplateAst} from '../template_parser/template_ast';
import {CompileElement} from './compile_element';
@ -31,7 +32,7 @@ export class ViewCompileResult {
@Injectable()
export class ViewCompiler {
constructor(private _genConfig: CompilerConfig) {}
constructor(private _genConfig: CompilerConfig, private _schemaRegistry: ElementSchemaRegistry) {}
compileComponent(
component: CompileDirectiveMetadata, template: TemplateAst[], styles: o.Expression,
@ -47,7 +48,7 @@ export class ViewCompiler {
buildView(view, template, dependencies);
// Need to separate binding from creation to be able to refer to
// variables that have been declared after usage.
bindView(view, template);
bindView(view, template, this._schemaRegistry);
finishView(view, statements);
return new ViewCompileResult(statements, view.viewFactory.name, dependencies);