refactor(compiler): extract BindingParser
Needed so that we can parse directive host bindings independent of templates. Part of #11683
This commit is contained in:
parent
c9f58cf78c
commit
bc3f4bc816
|
@ -35,7 +35,7 @@ export enum ParseErrorLevel {
|
||||||
FATAL
|
FATAL
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class ParseError {
|
export class ParseError {
|
||||||
constructor(
|
constructor(
|
||||||
public span: ParseSourceSpan, public msg: string,
|
public span: ParseSourceSpan, public msg: string,
|
||||||
public level: ParseErrorLevel = ParseErrorLevel.FATAL) {}
|
public level: ParseErrorLevel = ParseErrorLevel.FATAL) {}
|
||||||
|
|
|
@ -0,0 +1,435 @@
|
||||||
|
/**
|
||||||
|
* @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 {SchemaMetadata, SecurityContext} from '@angular/core';
|
||||||
|
|
||||||
|
import {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 {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 {splitAtColon, splitAtPeriod} from '../util';
|
||||||
|
|
||||||
|
import {BoundElementPropertyAst, BoundEventAst, PropertyBindingType, VariableAst} from './template_ast';
|
||||||
|
|
||||||
|
const PROPERTY_PARTS_SEPARATOR = '.';
|
||||||
|
const ATTRIBUTE_PREFIX = 'attr';
|
||||||
|
const CLASS_PREFIX = 'class';
|
||||||
|
const STYLE_PREFIX = 'style';
|
||||||
|
|
||||||
|
const ANIMATE_PROP_PREFIX = 'animate-';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of a parsed property
|
||||||
|
*/
|
||||||
|
export enum BoundPropertyType {
|
||||||
|
DEFAULT,
|
||||||
|
LITERAL_ATTR,
|
||||||
|
ANIMATION
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a parsed property.
|
||||||
|
*/
|
||||||
|
export class BoundProperty {
|
||||||
|
constructor(
|
||||||
|
public name: string, public expression: ASTWithSource, public type: BoundPropertyType,
|
||||||
|
public sourceSpan: ParseSourceSpan) {}
|
||||||
|
|
||||||
|
get isLiteral() { return this.type === BoundPropertyType.LITERAL_ATTR; }
|
||||||
|
|
||||||
|
get isAnimation() { return this.type === BoundPropertyType.ANIMATION; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses bindings in templates and in the directive host area.
|
||||||
|
*/
|
||||||
|
export class BindingParser {
|
||||||
|
pipesByName: Map<string, CompilePipeMetadata> = new Map();
|
||||||
|
errors: ParseError[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private _exprParser: Parser, private _interpolationConfig: InterpolationConfig,
|
||||||
|
protected _schemaRegistry: ElementSchemaRegistry, protected _schemas: SchemaMetadata[],
|
||||||
|
pipes: CompilePipeMetadata[]) {
|
||||||
|
pipes.forEach(pipe => this.pipesByName.set(pipe.name, pipe));
|
||||||
|
}
|
||||||
|
|
||||||
|
createDirectiveHostPropertyAsts(
|
||||||
|
elementName: string, hostProps: {[key: string]: string}, sourceSpan: ParseSourceSpan,
|
||||||
|
targetPropertyAsts: BoundElementPropertyAst[]) {
|
||||||
|
if (hostProps) {
|
||||||
|
const boundProps: BoundProperty[] = [];
|
||||||
|
Object.keys(hostProps).forEach(propName => {
|
||||||
|
const expression = hostProps[propName];
|
||||||
|
if (typeof expression === 'string') {
|
||||||
|
this.parsePropertyBinding(propName, expression, true, sourceSpan, [], boundProps);
|
||||||
|
} else {
|
||||||
|
this.reportError(
|
||||||
|
`Value of the host property binding "${propName}" needs to be a string representing an expression but got "${expression}" (${typeof expression})`,
|
||||||
|
sourceSpan);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
boundProps.forEach(
|
||||||
|
(prop) => { targetPropertyAsts.push(this.createElementPropertyAst(elementName, prop)); });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createDirectiveHostEventAsts(
|
||||||
|
hostListeners: {[key: string]: string}, sourceSpan: ParseSourceSpan,
|
||||||
|
targetEventAsts: BoundEventAst[]) {
|
||||||
|
if (hostListeners) {
|
||||||
|
Object.keys(hostListeners).forEach(propName => {
|
||||||
|
const expression = hostListeners[propName];
|
||||||
|
if (typeof expression === 'string') {
|
||||||
|
this.parseEvent(propName, expression, sourceSpan, [], targetEventAsts);
|
||||||
|
} else {
|
||||||
|
this.reportError(
|
||||||
|
`Value of the host listener "${propName}" needs to be a string representing an expression but got "${expression}" (${typeof expression})`,
|
||||||
|
sourceSpan);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parseInterpolation(value: string, sourceSpan: ParseSourceSpan): ASTWithSource {
|
||||||
|
const sourceInfo = sourceSpan.start.toString();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const ast = this._exprParser.parseInterpolation(value, sourceInfo, this._interpolationConfig);
|
||||||
|
if (ast) this._reportExpressionParserErrors(ast.errors, sourceSpan);
|
||||||
|
this._checkPipes(ast, sourceSpan);
|
||||||
|
if (ast &&
|
||||||
|
(<Interpolation>ast.ast).expressions.length > view_utils.MAX_INTERPOLATION_VALUES) {
|
||||||
|
throw new Error(
|
||||||
|
`Only support at most ${view_utils.MAX_INTERPOLATION_VALUES} interpolation values!`);
|
||||||
|
}
|
||||||
|
return ast;
|
||||||
|
} catch (e) {
|
||||||
|
this.reportError(`${e}`, sourceSpan);
|
||||||
|
return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parseInlineTemplateBinding(
|
||||||
|
name: string, value: string, sourceSpan: ParseSourceSpan, targetMatchableAttrs: string[][],
|
||||||
|
targetProps: BoundProperty[], targetVars: VariableAst[]) {
|
||||||
|
const bindings = this._parseTemplateBindings(value, sourceSpan);
|
||||||
|
for (let i = 0; i < bindings.length; i++) {
|
||||||
|
const binding = bindings[i];
|
||||||
|
if (binding.keyIsVar) {
|
||||||
|
targetVars.push(new VariableAst(binding.key, binding.name, sourceSpan));
|
||||||
|
} else if (isPresent(binding.expression)) {
|
||||||
|
this._parsePropertyAst(
|
||||||
|
binding.key, binding.expression, sourceSpan, targetMatchableAttrs, targetProps);
|
||||||
|
} else {
|
||||||
|
targetMatchableAttrs.push([binding.key, '']);
|
||||||
|
this.parseLiteralAttr(binding.key, null, sourceSpan, targetMatchableAttrs, targetProps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _parseTemplateBindings(value: string, sourceSpan: ParseSourceSpan): TemplateBinding[] {
|
||||||
|
const sourceInfo = sourceSpan.start.toString();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const bindingsResult = this._exprParser.parseTemplateBindings(value, sourceInfo);
|
||||||
|
this._reportExpressionParserErrors(bindingsResult.errors, sourceSpan);
|
||||||
|
bindingsResult.templateBindings.forEach((binding) => {
|
||||||
|
if (isPresent(binding.expression)) {
|
||||||
|
this._checkPipes(binding.expression, sourceSpan);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
bindingsResult.warnings.forEach(
|
||||||
|
(warning) => { this.reportError(warning, sourceSpan, ParseErrorLevel.WARNING); });
|
||||||
|
return bindingsResult.templateBindings;
|
||||||
|
} catch (e) {
|
||||||
|
this.reportError(`${e}`, sourceSpan);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parseLiteralAttr(
|
||||||
|
name: string, value: string, sourceSpan: ParseSourceSpan, targetMatchableAttrs: string[][],
|
||||||
|
targetProps: BoundProperty[]) {
|
||||||
|
if (_isAnimationLabel(name)) {
|
||||||
|
name = name.substring(1);
|
||||||
|
if (isPresent(value) && value.length > 0) {
|
||||||
|
this.reportError(
|
||||||
|
`Assigning animation triggers via @prop="exp" attributes with an expression is invalid.` +
|
||||||
|
` Use property bindings (e.g. [@prop]="exp") or use an attribute without a value (e.g. @prop) instead.`,
|
||||||
|
sourceSpan, ParseErrorLevel.FATAL);
|
||||||
|
}
|
||||||
|
this._parseAnimation(name, value, sourceSpan, targetMatchableAttrs, targetProps);
|
||||||
|
} else {
|
||||||
|
targetProps.push(new BoundProperty(
|
||||||
|
name, this._exprParser.wrapLiteralPrimitive(value, ''), BoundPropertyType.LITERAL_ATTR,
|
||||||
|
sourceSpan));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parsePropertyBinding(
|
||||||
|
name: string, expression: string, isHost: boolean, sourceSpan: ParseSourceSpan,
|
||||||
|
targetMatchableAttrs: string[][], targetProps: BoundProperty[]) {
|
||||||
|
let isAnimationProp = false;
|
||||||
|
if (name.startsWith(ANIMATE_PROP_PREFIX)) {
|
||||||
|
isAnimationProp = true;
|
||||||
|
name = name.substring(ANIMATE_PROP_PREFIX.length);
|
||||||
|
} else if (_isAnimationLabel(name)) {
|
||||||
|
isAnimationProp = true;
|
||||||
|
name = name.substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAnimationProp) {
|
||||||
|
this._parseAnimation(name, expression, sourceSpan, targetMatchableAttrs, targetProps);
|
||||||
|
} else {
|
||||||
|
this._parsePropertyAst(
|
||||||
|
name, this._parseBinding(expression, isHost, sourceSpan), sourceSpan,
|
||||||
|
targetMatchableAttrs, targetProps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parsePropertyInterpolation(
|
||||||
|
name: string, value: string, sourceSpan: ParseSourceSpan, targetMatchableAttrs: string[][],
|
||||||
|
targetProps: BoundProperty[]): boolean {
|
||||||
|
const expr = this.parseInterpolation(value, sourceSpan);
|
||||||
|
if (isPresent(expr)) {
|
||||||
|
this._parsePropertyAst(name, expr, sourceSpan, targetMatchableAttrs, targetProps);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _parsePropertyAst(
|
||||||
|
name: string, ast: ASTWithSource, sourceSpan: ParseSourceSpan,
|
||||||
|
targetMatchableAttrs: string[][], targetProps: BoundProperty[]) {
|
||||||
|
targetMatchableAttrs.push([name, ast.source]);
|
||||||
|
targetProps.push(new BoundProperty(name, ast, BoundPropertyType.DEFAULT, sourceSpan));
|
||||||
|
}
|
||||||
|
|
||||||
|
private _parseAnimation(
|
||||||
|
name: string, expression: string, sourceSpan: ParseSourceSpan,
|
||||||
|
targetMatchableAttrs: string[][], targetProps: BoundProperty[]) {
|
||||||
|
// This will occur when a @trigger is not paired with an expression.
|
||||||
|
// For animations it is valid to not have an expression since */void
|
||||||
|
// states will be applied by angular when the element is attached/detached
|
||||||
|
const ast = this._parseBinding(expression || 'null', false, sourceSpan);
|
||||||
|
targetMatchableAttrs.push([name, ast.source]);
|
||||||
|
targetProps.push(new BoundProperty(name, ast, BoundPropertyType.ANIMATION, sourceSpan));
|
||||||
|
}
|
||||||
|
|
||||||
|
private _parseBinding(value: string, isHostBinding: boolean, sourceSpan: ParseSourceSpan):
|
||||||
|
ASTWithSource {
|
||||||
|
const sourceInfo = sourceSpan.start.toString();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const ast = isHostBinding ?
|
||||||
|
this._exprParser.parseSimpleBinding(value, sourceInfo, this._interpolationConfig) :
|
||||||
|
this._exprParser.parseBinding(value, sourceInfo, this._interpolationConfig);
|
||||||
|
if (ast) this._reportExpressionParserErrors(ast.errors, sourceSpan);
|
||||||
|
this._checkPipes(ast, sourceSpan);
|
||||||
|
return ast;
|
||||||
|
} catch (e) {
|
||||||
|
this.reportError(`${e}`, sourceSpan);
|
||||||
|
return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createElementPropertyAst(elementName: string, boundProp: BoundProperty): BoundElementPropertyAst {
|
||||||
|
if (boundProp.isAnimation) {
|
||||||
|
return new BoundElementPropertyAst(
|
||||||
|
boundProp.name, PropertyBindingType.Animation, SecurityContext.NONE, 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;
|
||||||
|
|
||||||
|
if (parts.length === 1) {
|
||||||
|
var partValue = parts[0];
|
||||||
|
boundPropertyName = this._schemaRegistry.getMappedPropName(partValue);
|
||||||
|
securityContext = this._schemaRegistry.securityContext(elementName, boundPropertyName);
|
||||||
|
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);
|
||||||
|
|
||||||
|
const nsSeparatorIdx = boundPropertyName.indexOf(':');
|
||||||
|
if (nsSeparatorIdx > -1) {
|
||||||
|
const ns = boundPropertyName.substring(0, nsSeparatorIdx);
|
||||||
|
const name = boundPropertyName.substring(nsSeparatorIdx + 1);
|
||||||
|
boundPropertyName = mergeNsAndName(ns, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
bindingType = PropertyBindingType.Attribute;
|
||||||
|
} else if (parts[0] == CLASS_PREFIX) {
|
||||||
|
boundPropertyName = parts[1];
|
||||||
|
bindingType = PropertyBindingType.Class;
|
||||||
|
securityContext = SecurityContext.NONE;
|
||||||
|
} else if (parts[0] == STYLE_PREFIX) {
|
||||||
|
unit = parts.length > 2 ? parts[2] : null;
|
||||||
|
boundPropertyName = parts[1];
|
||||||
|
bindingType = PropertyBindingType.Style;
|
||||||
|
securityContext = SecurityContext.STYLE;
|
||||||
|
} else {
|
||||||
|
this.reportError(`Invalid property name '${boundProp.name}'`, boundProp.sourceSpan);
|
||||||
|
bindingType = null;
|
||||||
|
securityContext = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new BoundElementPropertyAst(
|
||||||
|
boundPropertyName, bindingType, securityContext, boundProp.expression, unit,
|
||||||
|
boundProp.sourceSpan);
|
||||||
|
}
|
||||||
|
|
||||||
|
parseEvent(
|
||||||
|
name: string, expression: string, sourceSpan: ParseSourceSpan,
|
||||||
|
targetMatchableAttrs: string[][], targetEvents: BoundEventAst[]) {
|
||||||
|
if (_isAnimationLabel(name)) {
|
||||||
|
name = name.substr(1);
|
||||||
|
this._parseAnimationEvent(name, expression, sourceSpan, targetEvents);
|
||||||
|
} else {
|
||||||
|
this._parseEvent(name, expression, sourceSpan, targetMatchableAttrs, targetEvents);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _parseAnimationEvent(
|
||||||
|
name: string, expression: string, sourceSpan: ParseSourceSpan,
|
||||||
|
targetEvents: BoundEventAst[]) {
|
||||||
|
const matches = splitAtPeriod(name, [name, '']);
|
||||||
|
const eventName = matches[0];
|
||||||
|
const phase = matches[1].toLowerCase();
|
||||||
|
if (phase) {
|
||||||
|
switch (phase) {
|
||||||
|
case 'start':
|
||||||
|
case 'done':
|
||||||
|
const ast = this._parseAction(expression, sourceSpan);
|
||||||
|
targetEvents.push(new BoundEventAst(eventName, null, phase, ast, sourceSpan));
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
this.reportError(
|
||||||
|
`The provided animation output phase value "${phase}" for "@${eventName}" is not supported (use start or done)`,
|
||||||
|
sourceSpan);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.reportError(
|
||||||
|
`The animation trigger output event (@${eventName}) is missing its phase value name (start or done are currently supported)`,
|
||||||
|
sourceSpan);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _parseEvent(
|
||||||
|
name: string, expression: string, sourceSpan: ParseSourceSpan,
|
||||||
|
targetMatchableAttrs: string[][], targetEvents: BoundEventAst[]) {
|
||||||
|
// long format: 'target: eventName'
|
||||||
|
const [target, eventName] = splitAtColon(name, [null, name]);
|
||||||
|
const ast = this._parseAction(expression, sourceSpan);
|
||||||
|
targetMatchableAttrs.push([name, ast.source]);
|
||||||
|
targetEvents.push(new BoundEventAst(eventName, target, null, ast, sourceSpan));
|
||||||
|
// Don't detect directives for event names for now,
|
||||||
|
// so don't add the event name to the matchableAttrs
|
||||||
|
}
|
||||||
|
|
||||||
|
private _parseAction(value: string, sourceSpan: ParseSourceSpan): ASTWithSource {
|
||||||
|
const sourceInfo = sourceSpan.start.toString();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const ast = this._exprParser.parseAction(value, sourceInfo, this._interpolationConfig);
|
||||||
|
if (ast) {
|
||||||
|
this._reportExpressionParserErrors(ast.errors, sourceSpan);
|
||||||
|
}
|
||||||
|
if (!ast || ast.ast instanceof EmptyExpr) {
|
||||||
|
this.reportError(`Empty expressions are not allowed`, sourceSpan);
|
||||||
|
return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo);
|
||||||
|
}
|
||||||
|
this._checkPipes(ast, sourceSpan);
|
||||||
|
return ast;
|
||||||
|
} catch (e) {
|
||||||
|
this.reportError(`${e}`, sourceSpan);
|
||||||
|
return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reportError(
|
||||||
|
message: string, sourceSpan: ParseSourceSpan,
|
||||||
|
level: ParseErrorLevel = ParseErrorLevel.FATAL) {
|
||||||
|
this.errors.push(new ParseError(sourceSpan, message, level));
|
||||||
|
}
|
||||||
|
|
||||||
|
private _reportExpressionParserErrors(errors: ParserError[], sourceSpan: ParseSourceSpan) {
|
||||||
|
for (const error of errors) {
|
||||||
|
this.reportError(error.message, sourceSpan);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _checkPipes(ast: ASTWithSource, sourceSpan: ParseSourceSpan) {
|
||||||
|
if (isPresent(ast)) {
|
||||||
|
const collector = new PipeCollector();
|
||||||
|
ast.visit(collector);
|
||||||
|
collector.pipes.forEach((pipeName) => {
|
||||||
|
if (!this.pipesByName.has(pipeName)) {
|
||||||
|
this.reportError(`The pipe '${pipeName}' could not be found`, sourceSpan);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param propName the name of the property / attribute
|
||||||
|
* @param sourceSpan
|
||||||
|
* @param isAttr true when binding to an attribute
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private _validatePropertyOrAttributeName(
|
||||||
|
propName: string, sourceSpan: ParseSourceSpan, isAttr: boolean): void {
|
||||||
|
const report = isAttr ? this._schemaRegistry.validateAttribute(propName) :
|
||||||
|
this._schemaRegistry.validateProperty(propName);
|
||||||
|
if (report.error) {
|
||||||
|
this.reportError(report.msg, sourceSpan, ParseErrorLevel.FATAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PipeCollector extends RecursiveAstVisitor {
|
||||||
|
pipes: Set<string> = new Set<string>();
|
||||||
|
visitPipe(ast: BindingPipe, context: any): any {
|
||||||
|
this.pipes.add(ast.name);
|
||||||
|
ast.exp.visit(this);
|
||||||
|
this.visitAll(ast.args, context);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _isAnimationLabel(name: string): boolean {
|
||||||
|
return name[0] == '@';
|
||||||
|
}
|
|
@ -25,8 +25,8 @@ import {ProviderElementContext, ProviderViewContext} from '../provider_analyzer'
|
||||||
import {ElementSchemaRegistry} from '../schema/element_schema_registry';
|
import {ElementSchemaRegistry} from '../schema/element_schema_registry';
|
||||||
import {CssSelector, SelectorMatcher} from '../selector';
|
import {CssSelector, SelectorMatcher} from '../selector';
|
||||||
import {isStyleUrlResolvable} from '../style_url_resolver';
|
import {isStyleUrlResolvable} from '../style_url_resolver';
|
||||||
import {splitAtColon, splitAtPeriod} from '../util';
|
|
||||||
|
|
||||||
|
import {BindingParser, BoundProperty} from './binding_parser';
|
||||||
import {AttrAst, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, DirectiveAst, ElementAst, EmbeddedTemplateAst, NgContentAst, PropertyBindingType, ReferenceAst, TemplateAst, TemplateAstVisitor, TextAst, VariableAst, templateVisitAll} from './template_ast';
|
import {AttrAst, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, DirectiveAst, ElementAst, EmbeddedTemplateAst, NgContentAst, PropertyBindingType, ReferenceAst, TemplateAst, TemplateAstVisitor, TextAst, VariableAst, templateVisitAll} from './template_ast';
|
||||||
import {PreparsedElementType, preparseElement} from './template_preparser';
|
import {PreparsedElementType, preparseElement} from './template_preparser';
|
||||||
|
|
||||||
|
@ -56,17 +56,11 @@ const IDENT_BANANA_BOX_IDX = 8;
|
||||||
const IDENT_PROPERTY_IDX = 9;
|
const IDENT_PROPERTY_IDX = 9;
|
||||||
const IDENT_EVENT_IDX = 10;
|
const IDENT_EVENT_IDX = 10;
|
||||||
|
|
||||||
const ANIMATE_PROP_PREFIX = 'animate-';
|
|
||||||
const TEMPLATE_ELEMENT = 'template';
|
const TEMPLATE_ELEMENT = 'template';
|
||||||
const TEMPLATE_ATTR = 'template';
|
const TEMPLATE_ATTR = 'template';
|
||||||
const TEMPLATE_ATTR_PREFIX = '*';
|
const TEMPLATE_ATTR_PREFIX = '*';
|
||||||
const CLASS_ATTR = 'class';
|
const CLASS_ATTR = 'class';
|
||||||
|
|
||||||
const PROPERTY_PARTS_SEPARATOR = '.';
|
|
||||||
const ATTRIBUTE_PREFIX = 'attr';
|
|
||||||
const CLASS_PREFIX = 'class';
|
|
||||||
const STYLE_PREFIX = 'style';
|
|
||||||
|
|
||||||
const TEXT_CSS_SELECTOR = CssSelector.parse('*')[0];
|
const TEXT_CSS_SELECTOR = CssSelector.parse('*')[0];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -135,9 +129,16 @@ export class TemplateParser {
|
||||||
const uniqPipes = removeIdentifierDuplicates(pipes);
|
const uniqPipes = removeIdentifierDuplicates(pipes);
|
||||||
const providerViewContext =
|
const providerViewContext =
|
||||||
new ProviderViewContext(component, htmlAstWithErrors.rootNodes[0].sourceSpan);
|
new ProviderViewContext(component, htmlAstWithErrors.rootNodes[0].sourceSpan);
|
||||||
|
let interpolationConfig: InterpolationConfig;
|
||||||
|
if (component.template && component.template.interpolation) {
|
||||||
|
interpolationConfig = {
|
||||||
|
start: component.template.interpolation[0],
|
||||||
|
end: component.template.interpolation[1]
|
||||||
|
};
|
||||||
|
}
|
||||||
const parseVisitor = new TemplateParseVisitor(
|
const parseVisitor = new TemplateParseVisitor(
|
||||||
providerViewContext, uniqDirectives, uniqPipes, schemas, this._exprParser,
|
providerViewContext, uniqDirectives, uniqPipes, this._exprParser, interpolationConfig,
|
||||||
this._schemaRegistry);
|
this._schemaRegistry, schemas);
|
||||||
result = html.visitAll(parseVisitor, htmlAstWithErrors.rootNodes, EMPTY_ELEMENT_CONTEXT);
|
result = html.visitAll(parseVisitor, htmlAstWithErrors.rootNodes, EMPTY_ELEMENT_CONTEXT);
|
||||||
errors.push(...parseVisitor.errors, ...providerViewContext.errors);
|
errors.push(...parseVisitor.errors, ...providerViewContext.errors);
|
||||||
} else {
|
} else {
|
||||||
|
@ -195,135 +196,23 @@ export class TemplateParser {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TemplateParseVisitor implements html.Visitor {
|
class TemplateParseVisitor extends BindingParser implements html.Visitor {
|
||||||
selectorMatcher = new SelectorMatcher();
|
selectorMatcher = new SelectorMatcher();
|
||||||
errors: TemplateParseError[] = [];
|
errors: TemplateParseError[] = [];
|
||||||
directivesIndex = new Map<CompileDirectiveMetadata, number>();
|
directivesIndex = new Map<CompileDirectiveMetadata, number>();
|
||||||
ngContentCount: number = 0;
|
ngContentCount: number = 0;
|
||||||
pipesByName: Map<string, CompilePipeMetadata> = new Map();
|
|
||||||
|
|
||||||
private _interpolationConfig: InterpolationConfig;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public providerViewContext: ProviderViewContext, directives: CompileDirectiveMetadata[],
|
public providerViewContext: ProviderViewContext, directives: CompileDirectiveMetadata[],
|
||||||
pipes: CompilePipeMetadata[], private _schemas: SchemaMetadata[], private _exprParser: Parser,
|
pipes: CompilePipeMetadata[], _exprParser: Parser, interpolationConfig: InterpolationConfig,
|
||||||
private _schemaRegistry: ElementSchemaRegistry) {
|
_schemaRegistry: ElementSchemaRegistry, schemas: SchemaMetadata[]) {
|
||||||
const tempMeta = providerViewContext.component.template;
|
super(_exprParser, interpolationConfig, _schemaRegistry, schemas, pipes);
|
||||||
|
|
||||||
if (tempMeta && tempMeta.interpolation) {
|
|
||||||
this._interpolationConfig = {
|
|
||||||
start: tempMeta.interpolation[0],
|
|
||||||
end: tempMeta.interpolation[1]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
directives.forEach((directive: CompileDirectiveMetadata, index: number) => {
|
directives.forEach((directive: CompileDirectiveMetadata, index: number) => {
|
||||||
const selector = CssSelector.parse(directive.selector);
|
const selector = CssSelector.parse(directive.selector);
|
||||||
this.selectorMatcher.addSelectables(selector, directive);
|
this.selectorMatcher.addSelectables(selector, directive);
|
||||||
this.directivesIndex.set(directive, index);
|
this.directivesIndex.set(directive, index);
|
||||||
});
|
});
|
||||||
|
|
||||||
pipes.forEach(pipe => this.pipesByName.set(pipe.name, pipe));
|
|
||||||
}
|
|
||||||
|
|
||||||
private _reportError(
|
|
||||||
message: string, sourceSpan: ParseSourceSpan,
|
|
||||||
level: ParseErrorLevel = ParseErrorLevel.FATAL) {
|
|
||||||
this.errors.push(new TemplateParseError(message, sourceSpan, level));
|
|
||||||
}
|
|
||||||
|
|
||||||
private _reportParserErrors(errors: ParserError[], sourceSpan: ParseSourceSpan) {
|
|
||||||
for (const error of errors) {
|
|
||||||
this._reportError(error.message, sourceSpan);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _parseInterpolation(value: string, sourceSpan: ParseSourceSpan): ASTWithSource {
|
|
||||||
const sourceInfo = sourceSpan.start.toString();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const ast = this._exprParser.parseInterpolation(value, sourceInfo, this._interpolationConfig);
|
|
||||||
if (ast) this._reportParserErrors(ast.errors, sourceSpan);
|
|
||||||
this._checkPipes(ast, sourceSpan);
|
|
||||||
if (isPresent(ast) &&
|
|
||||||
(<Interpolation>ast.ast).expressions.length > view_utils.MAX_INTERPOLATION_VALUES) {
|
|
||||||
throw new Error(
|
|
||||||
`Only support at most ${view_utils.MAX_INTERPOLATION_VALUES} interpolation values!`);
|
|
||||||
}
|
|
||||||
return ast;
|
|
||||||
} catch (e) {
|
|
||||||
this._reportError(`${e}`, sourceSpan);
|
|
||||||
return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _parseAction(value: string, sourceSpan: ParseSourceSpan): ASTWithSource {
|
|
||||||
const sourceInfo = sourceSpan.start.toString();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const ast = this._exprParser.parseAction(value, sourceInfo, this._interpolationConfig);
|
|
||||||
if (ast) {
|
|
||||||
this._reportParserErrors(ast.errors, sourceSpan);
|
|
||||||
}
|
|
||||||
if (!ast || ast.ast instanceof EmptyExpr) {
|
|
||||||
this._reportError(`Empty expressions are not allowed`, sourceSpan);
|
|
||||||
return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo);
|
|
||||||
}
|
|
||||||
this._checkPipes(ast, sourceSpan);
|
|
||||||
return ast;
|
|
||||||
} catch (e) {
|
|
||||||
this._reportError(`${e}`, sourceSpan);
|
|
||||||
return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _parseBinding(value: string, isHostBinding: boolean, sourceSpan: ParseSourceSpan):
|
|
||||||
ASTWithSource {
|
|
||||||
const sourceInfo = sourceSpan.start.toString();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const ast = isHostBinding ?
|
|
||||||
this._exprParser.parseSimpleBinding(value, sourceInfo, this._interpolationConfig) :
|
|
||||||
this._exprParser.parseBinding(value, sourceInfo, this._interpolationConfig);
|
|
||||||
if (ast) this._reportParserErrors(ast.errors, sourceSpan);
|
|
||||||
this._checkPipes(ast, sourceSpan);
|
|
||||||
return ast;
|
|
||||||
} catch (e) {
|
|
||||||
this._reportError(`${e}`, sourceSpan);
|
|
||||||
return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _parseTemplateBindings(value: string, sourceSpan: ParseSourceSpan): TemplateBinding[] {
|
|
||||||
const sourceInfo = sourceSpan.start.toString();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const bindingsResult = this._exprParser.parseTemplateBindings(value, sourceInfo);
|
|
||||||
this._reportParserErrors(bindingsResult.errors, sourceSpan);
|
|
||||||
bindingsResult.templateBindings.forEach((binding) => {
|
|
||||||
if (isPresent(binding.expression)) {
|
|
||||||
this._checkPipes(binding.expression, sourceSpan);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
bindingsResult.warnings.forEach(
|
|
||||||
(warning) => { this._reportError(warning, sourceSpan, ParseErrorLevel.WARNING); });
|
|
||||||
return bindingsResult.templateBindings;
|
|
||||||
} catch (e) {
|
|
||||||
this._reportError(`${e}`, sourceSpan);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _checkPipes(ast: ASTWithSource, sourceSpan: ParseSourceSpan) {
|
|
||||||
if (isPresent(ast)) {
|
|
||||||
const collector = new PipeCollector();
|
|
||||||
ast.visit(collector);
|
|
||||||
collector.pipes.forEach((pipeName) => {
|
|
||||||
if (!this.pipesByName.has(pipeName)) {
|
|
||||||
this._reportError(`The pipe '${pipeName}' could not be found`, sourceSpan);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
visitExpansion(expansion: html.Expansion, context: any): any { return null; }
|
visitExpansion(expansion: html.Expansion, context: any): any { return null; }
|
||||||
|
@ -332,7 +221,7 @@ class TemplateParseVisitor implements html.Visitor {
|
||||||
|
|
||||||
visitText(text: html.Text, parent: ElementContext): any {
|
visitText(text: html.Text, parent: ElementContext): any {
|
||||||
const ngContentIndex = parent.findNgContentIndex(TEXT_CSS_SELECTOR);
|
const ngContentIndex = parent.findNgContentIndex(TEXT_CSS_SELECTOR);
|
||||||
const expr = this._parseInterpolation(text.value, text.sourceSpan);
|
const expr = this.parseInterpolation(text.value, text.sourceSpan);
|
||||||
if (isPresent(expr)) {
|
if (isPresent(expr)) {
|
||||||
return new BoundTextAst(expr, ngContentIndex, text.sourceSpan);
|
return new BoundTextAst(expr, ngContentIndex, text.sourceSpan);
|
||||||
} else {
|
} else {
|
||||||
|
@ -364,13 +253,12 @@ class TemplateParseVisitor implements html.Visitor {
|
||||||
}
|
}
|
||||||
|
|
||||||
const matchableAttrs: string[][] = [];
|
const matchableAttrs: string[][] = [];
|
||||||
const elementOrDirectiveProps: BoundElementOrDirectiveProperty[] = [];
|
const elementOrDirectiveProps: BoundProperty[] = [];
|
||||||
const elementOrDirectiveRefs: ElementOrDirectiveRef[] = [];
|
const elementOrDirectiveRefs: ElementOrDirectiveRef[] = [];
|
||||||
const elementVars: VariableAst[] = [];
|
const elementVars: VariableAst[] = [];
|
||||||
const animationProps: BoundElementPropertyAst[] = [];
|
|
||||||
const events: BoundEventAst[] = [];
|
const events: BoundEventAst[] = [];
|
||||||
|
|
||||||
const templateElementOrDirectiveProps: BoundElementOrDirectiveProperty[] = [];
|
const templateElementOrDirectiveProps: BoundProperty[] = [];
|
||||||
const templateMatchableAttrs: string[][] = [];
|
const templateMatchableAttrs: string[][] = [];
|
||||||
const templateElementVars: VariableAst[] = [];
|
const templateElementVars: VariableAst[] = [];
|
||||||
|
|
||||||
|
@ -381,16 +269,27 @@ class TemplateParseVisitor implements html.Visitor {
|
||||||
|
|
||||||
element.attrs.forEach(attr => {
|
element.attrs.forEach(attr => {
|
||||||
const hasBinding = this._parseAttr(
|
const hasBinding = this._parseAttr(
|
||||||
isTemplateElement, attr, matchableAttrs, elementOrDirectiveProps, animationProps, events,
|
isTemplateElement, attr, matchableAttrs, elementOrDirectiveProps, events,
|
||||||
elementOrDirectiveRefs, elementVars);
|
elementOrDirectiveRefs, elementVars);
|
||||||
|
|
||||||
const hasTemplateBinding = this._parseInlineTemplateBinding(
|
let templateBindingsSource: string;
|
||||||
attr, templateMatchableAttrs, templateElementOrDirectiveProps, templateElementVars);
|
if (this._normalizeAttributeName(attr.name) == TEMPLATE_ATTR) {
|
||||||
|
templateBindingsSource = attr.value;
|
||||||
if (hasTemplateBinding && hasInlineTemplates) {
|
} else if (attr.name.startsWith(TEMPLATE_ATTR_PREFIX)) {
|
||||||
this._reportError(
|
const key = attr.name.substring(TEMPLATE_ATTR_PREFIX.length); // remove the star
|
||||||
`Can't have multiple template bindings on one element. Use only one attribute named 'template' or prefixed with *`,
|
templateBindingsSource = (attr.value.length == 0) ? key : key + ' ' + attr.value;
|
||||||
attr.sourceSpan);
|
}
|
||||||
|
const hasTemplateBinding = isPresent(templateBindingsSource);
|
||||||
|
if (hasTemplateBinding) {
|
||||||
|
if (hasInlineTemplates) {
|
||||||
|
this.reportError(
|
||||||
|
`Can't have multiple template bindings on one element. Use only one attribute named 'template' or prefixed with *`,
|
||||||
|
attr.sourceSpan);
|
||||||
|
}
|
||||||
|
hasInlineTemplates = true;
|
||||||
|
this.parseInlineTemplateBinding(
|
||||||
|
attr.name, templateBindingsSource, attr.sourceSpan, templateMatchableAttrs,
|
||||||
|
templateElementOrDirectiveProps, templateElementVars);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasBinding && !hasTemplateBinding) {
|
if (!hasBinding && !hasTemplateBinding) {
|
||||||
|
@ -398,10 +297,6 @@ class TemplateParseVisitor implements html.Visitor {
|
||||||
attrs.push(this.visitAttribute(attr, null));
|
attrs.push(this.visitAttribute(attr, null));
|
||||||
matchableAttrs.push([attr.name, attr.value]);
|
matchableAttrs.push([attr.name, attr.value]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasTemplateBinding) {
|
|
||||||
hasInlineTemplates = true;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const elementCssSelector = createElementCssSelector(nodeName, matchableAttrs);
|
const elementCssSelector = createElementCssSelector(nodeName, matchableAttrs);
|
||||||
|
@ -412,8 +307,7 @@ class TemplateParseVisitor implements html.Visitor {
|
||||||
isTemplateElement, element.name, directiveMetas, elementOrDirectiveProps,
|
isTemplateElement, element.name, directiveMetas, elementOrDirectiveProps,
|
||||||
elementOrDirectiveRefs, element.sourceSpan, references);
|
elementOrDirectiveRefs, element.sourceSpan, references);
|
||||||
const elementProps: BoundElementPropertyAst[] =
|
const elementProps: BoundElementPropertyAst[] =
|
||||||
this._createElementPropertyAsts(element.name, elementOrDirectiveProps, directiveAsts)
|
this._createElementPropertyAsts(element.name, elementOrDirectiveProps, directiveAsts);
|
||||||
.concat(animationProps);
|
|
||||||
const isViewRoot = parent.isTemplateElement || hasInlineTemplates;
|
const isViewRoot = parent.isTemplateElement || hasInlineTemplates;
|
||||||
const providerContext = new ProviderElementContext(
|
const providerContext = new ProviderElementContext(
|
||||||
this.providerViewContext, parent.providerContext, isViewRoot, directiveAsts, attrs,
|
this.providerViewContext, parent.providerContext, isViewRoot, directiveAsts, attrs,
|
||||||
|
@ -433,7 +327,7 @@ class TemplateParseVisitor implements html.Visitor {
|
||||||
|
|
||||||
if (preparsedElement.type === PreparsedElementType.NG_CONTENT) {
|
if (preparsedElement.type === PreparsedElementType.NG_CONTENT) {
|
||||||
if (element.children && !element.children.every(_isEmptyTextNode)) {
|
if (element.children && !element.children.every(_isEmptyTextNode)) {
|
||||||
this._reportError(`<ng-content> element cannot have content.`, element.sourceSpan);
|
this.reportError(`<ng-content> element cannot have content.`, element.sourceSpan);
|
||||||
}
|
}
|
||||||
|
|
||||||
parsedElement = new NgContentAst(
|
parsedElement = new NgContentAst(
|
||||||
|
@ -506,7 +400,7 @@ class TemplateParseVisitor implements html.Visitor {
|
||||||
animationInputs.forEach(input => {
|
animationInputs.forEach(input => {
|
||||||
const name = input.name;
|
const name = input.name;
|
||||||
if (!triggerLookup.has(name)) {
|
if (!triggerLookup.has(name)) {
|
||||||
this._reportError(`Couldn't find an animation entry for "${name}"`, input.sourceSpan);
|
this.reportError(`Couldn't find an animation entry for "${name}"`, input.sourceSpan);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -514,7 +408,7 @@ class TemplateParseVisitor implements html.Visitor {
|
||||||
if (output.isAnimation) {
|
if (output.isAnimation) {
|
||||||
const found = animationInputs.find(input => input.name == output.name);
|
const found = animationInputs.find(input => input.name == output.name);
|
||||||
if (!found) {
|
if (!found) {
|
||||||
this._reportError(
|
this.reportError(
|
||||||
`Unable to listen on (@${output.name}.${output.phase}) because the animation trigger [@${output.name}] isn't being used on the same element`,
|
`Unable to listen on (@${output.name}.${output.phase}) because the animation trigger [@${output.name}] isn't being used on the same element`,
|
||||||
output.sourceSpan);
|
output.sourceSpan);
|
||||||
}
|
}
|
||||||
|
@ -522,39 +416,9 @@ class TemplateParseVisitor implements html.Visitor {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _parseInlineTemplateBinding(
|
|
||||||
attr: html.Attribute, targetMatchableAttrs: string[][],
|
|
||||||
targetProps: BoundElementOrDirectiveProperty[], targetVars: VariableAst[]): boolean {
|
|
||||||
let templateBindingsSource: string = null;
|
|
||||||
if (this._normalizeAttributeName(attr.name) == TEMPLATE_ATTR) {
|
|
||||||
templateBindingsSource = attr.value;
|
|
||||||
} else if (attr.name.startsWith(TEMPLATE_ATTR_PREFIX)) {
|
|
||||||
const key = attr.name.substring(TEMPLATE_ATTR_PREFIX.length); // remove the star
|
|
||||||
templateBindingsSource = (attr.value.length == 0) ? key : key + ' ' + attr.value;
|
|
||||||
}
|
|
||||||
if (isPresent(templateBindingsSource)) {
|
|
||||||
const bindings = this._parseTemplateBindings(templateBindingsSource, attr.sourceSpan);
|
|
||||||
for (let i = 0; i < bindings.length; i++) {
|
|
||||||
const binding = bindings[i];
|
|
||||||
if (binding.keyIsVar) {
|
|
||||||
targetVars.push(new VariableAst(binding.key, binding.name, attr.sourceSpan));
|
|
||||||
} else if (isPresent(binding.expression)) {
|
|
||||||
this._parsePropertyAst(
|
|
||||||
binding.key, binding.expression, attr.sourceSpan, targetMatchableAttrs, targetProps);
|
|
||||||
} else {
|
|
||||||
targetMatchableAttrs.push([binding.key, '']);
|
|
||||||
this._parseLiteralAttr(binding.key, null, attr.sourceSpan, targetProps);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _parseAttr(
|
private _parseAttr(
|
||||||
isTemplateElement: boolean, attr: html.Attribute, targetMatchableAttrs: string[][],
|
isTemplateElement: boolean, attr: html.Attribute, targetMatchableAttrs: string[][],
|
||||||
targetProps: BoundElementOrDirectiveProperty[],
|
targetProps: BoundProperty[], targetEvents: BoundEventAst[],
|
||||||
targetAnimationProps: BoundElementPropertyAst[], targetEvents: BoundEventAst[],
|
|
||||||
targetRefs: ElementOrDirectiveRef[], targetVars: VariableAst[]): boolean {
|
targetRefs: ElementOrDirectiveRef[], targetVars: VariableAst[]): boolean {
|
||||||
const name = this._normalizeAttributeName(attr.name);
|
const name = this._normalizeAttributeName(attr.name);
|
||||||
const value = attr.value;
|
const value = attr.value;
|
||||||
|
@ -566,16 +430,15 @@ class TemplateParseVisitor implements html.Visitor {
|
||||||
if (bindParts !== null) {
|
if (bindParts !== null) {
|
||||||
hasBinding = true;
|
hasBinding = true;
|
||||||
if (isPresent(bindParts[KW_BIND_IDX])) {
|
if (isPresent(bindParts[KW_BIND_IDX])) {
|
||||||
this._parsePropertyOrAnimation(
|
this.parsePropertyBinding(
|
||||||
bindParts[IDENT_KW_IDX], value, srcSpan, targetMatchableAttrs, targetProps,
|
bindParts[IDENT_KW_IDX], value, false, srcSpan, targetMatchableAttrs, targetProps);
|
||||||
targetAnimationProps);
|
|
||||||
|
|
||||||
} else if (bindParts[KW_LET_IDX]) {
|
} else if (bindParts[KW_LET_IDX]) {
|
||||||
if (isTemplateElement) {
|
if (isTemplateElement) {
|
||||||
const identifier = bindParts[IDENT_KW_IDX];
|
const identifier = bindParts[IDENT_KW_IDX];
|
||||||
this._parseVariable(identifier, value, srcSpan, targetVars);
|
this._parseVariable(identifier, value, srcSpan, targetVars);
|
||||||
} else {
|
} else {
|
||||||
this._reportError(`"let-" is only supported on template elements.`, srcSpan);
|
this.reportError(`"let-" is only supported on template elements.`, srcSpan);
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (bindParts[KW_REF_IDX]) {
|
} else if (bindParts[KW_REF_IDX]) {
|
||||||
|
@ -583,48 +446,41 @@ class TemplateParseVisitor implements html.Visitor {
|
||||||
this._parseReference(identifier, value, srcSpan, targetRefs);
|
this._parseReference(identifier, value, srcSpan, targetRefs);
|
||||||
|
|
||||||
} else if (bindParts[KW_ON_IDX]) {
|
} else if (bindParts[KW_ON_IDX]) {
|
||||||
this._parseEventOrAnimationEvent(
|
this.parseEvent(
|
||||||
bindParts[IDENT_KW_IDX], value, srcSpan, targetMatchableAttrs, targetEvents);
|
bindParts[IDENT_KW_IDX], value, srcSpan, targetMatchableAttrs, targetEvents);
|
||||||
|
|
||||||
} else if (bindParts[KW_BINDON_IDX]) {
|
} else if (bindParts[KW_BINDON_IDX]) {
|
||||||
this._parsePropertyOrAnimation(
|
this.parsePropertyBinding(
|
||||||
bindParts[IDENT_KW_IDX], value, srcSpan, targetMatchableAttrs, targetProps,
|
bindParts[IDENT_KW_IDX], value, false, srcSpan, targetMatchableAttrs, targetProps);
|
||||||
targetAnimationProps);
|
|
||||||
this._parseAssignmentEvent(
|
this._parseAssignmentEvent(
|
||||||
bindParts[IDENT_KW_IDX], value, srcSpan, targetMatchableAttrs, targetEvents);
|
bindParts[IDENT_KW_IDX], value, srcSpan, targetMatchableAttrs, targetEvents);
|
||||||
|
|
||||||
} else if (bindParts[KW_AT_IDX]) {
|
} else if (bindParts[KW_AT_IDX]) {
|
||||||
if (_isAnimationLabel(name) && isPresent(value) && value.length > 0) {
|
this.parseLiteralAttr(name, value, srcSpan, targetMatchableAttrs, targetProps);
|
||||||
this._reportError(
|
|
||||||
`Assigning animation triggers via @prop="exp" attributes with an expression is invalid.` +
|
|
||||||
` Use property bindings (e.g. [@prop]="exp") or use an attribute without a value (e.g. @prop) instead.`,
|
|
||||||
srcSpan, ParseErrorLevel.FATAL);
|
|
||||||
}
|
|
||||||
this._parseAnimation(
|
|
||||||
bindParts[IDENT_KW_IDX], value, srcSpan, targetMatchableAttrs, targetAnimationProps);
|
|
||||||
} else if (bindParts[IDENT_BANANA_BOX_IDX]) {
|
} else if (bindParts[IDENT_BANANA_BOX_IDX]) {
|
||||||
this._parsePropertyOrAnimation(
|
this.parsePropertyBinding(
|
||||||
bindParts[IDENT_BANANA_BOX_IDX], value, srcSpan, targetMatchableAttrs, targetProps,
|
bindParts[IDENT_BANANA_BOX_IDX], value, false, srcSpan, targetMatchableAttrs,
|
||||||
targetAnimationProps);
|
targetProps);
|
||||||
this._parseAssignmentEvent(
|
this._parseAssignmentEvent(
|
||||||
bindParts[IDENT_BANANA_BOX_IDX], value, srcSpan, targetMatchableAttrs, targetEvents);
|
bindParts[IDENT_BANANA_BOX_IDX], value, srcSpan, targetMatchableAttrs, targetEvents);
|
||||||
|
|
||||||
} else if (bindParts[IDENT_PROPERTY_IDX]) {
|
} else if (bindParts[IDENT_PROPERTY_IDX]) {
|
||||||
this._parsePropertyOrAnimation(
|
this.parsePropertyBinding(
|
||||||
bindParts[IDENT_PROPERTY_IDX], value, srcSpan, targetMatchableAttrs, targetProps,
|
bindParts[IDENT_PROPERTY_IDX], value, false, srcSpan, targetMatchableAttrs,
|
||||||
targetAnimationProps);
|
targetProps);
|
||||||
|
|
||||||
} else if (bindParts[IDENT_EVENT_IDX]) {
|
} else if (bindParts[IDENT_EVENT_IDX]) {
|
||||||
this._parseEventOrAnimationEvent(
|
this.parseEvent(
|
||||||
bindParts[IDENT_EVENT_IDX], value, srcSpan, targetMatchableAttrs, targetEvents);
|
bindParts[IDENT_EVENT_IDX], value, srcSpan, targetMatchableAttrs, targetEvents);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
hasBinding =
|
hasBinding =
|
||||||
this._parsePropertyInterpolation(name, value, srcSpan, targetMatchableAttrs, targetProps);
|
this.parsePropertyInterpolation(name, value, srcSpan, targetMatchableAttrs, targetProps);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasBinding) {
|
if (!hasBinding) {
|
||||||
this._parseLiteralAttr(name, value, srcSpan, targetProps);
|
this.parseLiteralAttr(name, value, srcSpan, targetMatchableAttrs, targetProps);
|
||||||
}
|
}
|
||||||
|
|
||||||
return hasBinding;
|
return hasBinding;
|
||||||
|
@ -637,7 +493,7 @@ class TemplateParseVisitor implements html.Visitor {
|
||||||
private _parseVariable(
|
private _parseVariable(
|
||||||
identifier: string, value: string, sourceSpan: ParseSourceSpan, targetVars: VariableAst[]) {
|
identifier: string, value: string, sourceSpan: ParseSourceSpan, targetVars: VariableAst[]) {
|
||||||
if (identifier.indexOf('-') > -1) {
|
if (identifier.indexOf('-') > -1) {
|
||||||
this._reportError(`"-" is not allowed in variable names`, sourceSpan);
|
this.reportError(`"-" is not allowed in variable names`, sourceSpan);
|
||||||
}
|
}
|
||||||
|
|
||||||
targetVars.push(new VariableAst(identifier, value, sourceSpan));
|
targetVars.push(new VariableAst(identifier, value, sourceSpan));
|
||||||
|
@ -647,133 +503,19 @@ class TemplateParseVisitor implements html.Visitor {
|
||||||
identifier: string, value: string, sourceSpan: ParseSourceSpan,
|
identifier: string, value: string, sourceSpan: ParseSourceSpan,
|
||||||
targetRefs: ElementOrDirectiveRef[]) {
|
targetRefs: ElementOrDirectiveRef[]) {
|
||||||
if (identifier.indexOf('-') > -1) {
|
if (identifier.indexOf('-') > -1) {
|
||||||
this._reportError(`"-" is not allowed in reference names`, sourceSpan);
|
this.reportError(`"-" is not allowed in reference names`, sourceSpan);
|
||||||
}
|
}
|
||||||
|
|
||||||
targetRefs.push(new ElementOrDirectiveRef(identifier, value, sourceSpan));
|
targetRefs.push(new ElementOrDirectiveRef(identifier, value, sourceSpan));
|
||||||
}
|
}
|
||||||
|
|
||||||
private _parsePropertyOrAnimation(
|
|
||||||
name: string, expression: string, sourceSpan: ParseSourceSpan,
|
|
||||||
targetMatchableAttrs: string[][], targetProps: BoundElementOrDirectiveProperty[],
|
|
||||||
targetAnimationProps: BoundElementPropertyAst[]) {
|
|
||||||
const animatePropLength = ANIMATE_PROP_PREFIX.length;
|
|
||||||
var isAnimationProp = _isAnimationLabel(name);
|
|
||||||
var animationPrefixLength = 1;
|
|
||||||
if (name.substring(0, animatePropLength) == ANIMATE_PROP_PREFIX) {
|
|
||||||
isAnimationProp = true;
|
|
||||||
animationPrefixLength = animatePropLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isAnimationProp) {
|
|
||||||
this._parseAnimation(
|
|
||||||
name.substr(animationPrefixLength), expression, sourceSpan, targetMatchableAttrs,
|
|
||||||
targetAnimationProps);
|
|
||||||
} else {
|
|
||||||
this._parsePropertyAst(
|
|
||||||
name, this._parseBinding(expression, false, sourceSpan), sourceSpan, targetMatchableAttrs,
|
|
||||||
targetProps);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _parseAnimation(
|
|
||||||
name: string, expression: string, sourceSpan: ParseSourceSpan,
|
|
||||||
targetMatchableAttrs: string[][], targetAnimationProps: BoundElementPropertyAst[]) {
|
|
||||||
// This will occur when a @trigger is not paired with an expression.
|
|
||||||
// For animations it is valid to not have an expression since */void
|
|
||||||
// states will be applied by angular when the element is attached/detached
|
|
||||||
if (!isPresent(expression) || expression.length == 0) {
|
|
||||||
expression = 'null';
|
|
||||||
}
|
|
||||||
|
|
||||||
const ast = this._parseBinding(expression, false, sourceSpan);
|
|
||||||
targetMatchableAttrs.push([name, ast.source]);
|
|
||||||
targetAnimationProps.push(new BoundElementPropertyAst(
|
|
||||||
name, PropertyBindingType.Animation, SecurityContext.NONE, ast, null, sourceSpan));
|
|
||||||
}
|
|
||||||
|
|
||||||
private _parsePropertyInterpolation(
|
|
||||||
name: string, value: string, sourceSpan: ParseSourceSpan, targetMatchableAttrs: string[][],
|
|
||||||
targetProps: BoundElementOrDirectiveProperty[]): boolean {
|
|
||||||
const expr = this._parseInterpolation(value, sourceSpan);
|
|
||||||
if (isPresent(expr)) {
|
|
||||||
this._parsePropertyAst(name, expr, sourceSpan, targetMatchableAttrs, targetProps);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _parsePropertyAst(
|
|
||||||
name: string, ast: ASTWithSource, sourceSpan: ParseSourceSpan,
|
|
||||||
targetMatchableAttrs: string[][], targetProps: BoundElementOrDirectiveProperty[]) {
|
|
||||||
targetMatchableAttrs.push([name, ast.source]);
|
|
||||||
targetProps.push(new BoundElementOrDirectiveProperty(name, ast, false, sourceSpan));
|
|
||||||
}
|
|
||||||
|
|
||||||
private _parseAssignmentEvent(
|
private _parseAssignmentEvent(
|
||||||
name: string, expression: string, sourceSpan: ParseSourceSpan,
|
name: string, expression: string, sourceSpan: ParseSourceSpan,
|
||||||
targetMatchableAttrs: string[][], targetEvents: BoundEventAst[]) {
|
targetMatchableAttrs: string[][], targetEvents: BoundEventAst[]) {
|
||||||
this._parseEventOrAnimationEvent(
|
this.parseEvent(
|
||||||
`${name}Change`, `${expression}=$event`, sourceSpan, targetMatchableAttrs, targetEvents);
|
`${name}Change`, `${expression}=$event`, sourceSpan, targetMatchableAttrs, targetEvents);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _parseEventOrAnimationEvent(
|
|
||||||
name: string, expression: string, sourceSpan: ParseSourceSpan,
|
|
||||||
targetMatchableAttrs: string[][], targetEvents: BoundEventAst[]) {
|
|
||||||
if (_isAnimationLabel(name)) {
|
|
||||||
name = name.substr(1);
|
|
||||||
this._parseAnimationEvent(name, expression, sourceSpan, targetEvents);
|
|
||||||
} else {
|
|
||||||
this._parseEvent(name, expression, sourceSpan, targetMatchableAttrs, targetEvents);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _parseAnimationEvent(
|
|
||||||
name: string, expression: string, sourceSpan: ParseSourceSpan,
|
|
||||||
targetEvents: BoundEventAst[]) {
|
|
||||||
const matches = splitAtPeriod(name, [name, '']);
|
|
||||||
const eventName = matches[0];
|
|
||||||
const phase = matches[1].toLowerCase();
|
|
||||||
if (phase) {
|
|
||||||
switch (phase) {
|
|
||||||
case 'start':
|
|
||||||
case 'done':
|
|
||||||
const ast = this._parseAction(expression, sourceSpan);
|
|
||||||
targetEvents.push(new BoundEventAst(eventName, null, phase, ast, sourceSpan));
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
this._reportError(
|
|
||||||
`The provided animation output phase value "${phase}" for "@${eventName}" is not supported (use start or done)`,
|
|
||||||
sourceSpan);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this._reportError(
|
|
||||||
`The animation trigger output event (@${eventName}) is missing its phase value name (start or done are currently supported)`,
|
|
||||||
sourceSpan);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _parseEvent(
|
|
||||||
name: string, expression: string, sourceSpan: ParseSourceSpan,
|
|
||||||
targetMatchableAttrs: string[][], targetEvents: BoundEventAst[]) {
|
|
||||||
// long format: 'target: eventName'
|
|
||||||
const [target, eventName] = splitAtColon(name, [null, name]);
|
|
||||||
const ast = this._parseAction(expression, sourceSpan);
|
|
||||||
targetMatchableAttrs.push([name, ast.source]);
|
|
||||||
targetEvents.push(new BoundEventAst(eventName, target, null, ast, sourceSpan));
|
|
||||||
// Don't detect directives for event names for now,
|
|
||||||
// so don't add the event name to the matchableAttrs
|
|
||||||
}
|
|
||||||
|
|
||||||
private _parseLiteralAttr(
|
|
||||||
name: string, value: string, sourceSpan: ParseSourceSpan,
|
|
||||||
targetProps: BoundElementOrDirectiveProperty[]) {
|
|
||||||
targetProps.push(new BoundElementOrDirectiveProperty(
|
|
||||||
name, this._exprParser.wrapLiteralPrimitive(value, ''), true, sourceSpan));
|
|
||||||
}
|
|
||||||
|
|
||||||
private _parseDirectives(selectorMatcher: SelectorMatcher, elementCssSelector: CssSelector):
|
private _parseDirectives(selectorMatcher: SelectorMatcher, elementCssSelector: CssSelector):
|
||||||
{directives: CompileDirectiveMetadata[], matchElement: boolean} {
|
{directives: CompileDirectiveMetadata[], matchElement: boolean} {
|
||||||
// Need to sort the directives so that we get consistent results throughout,
|
// Need to sort the directives so that we get consistent results throughout,
|
||||||
|
@ -796,7 +538,7 @@ class TemplateParseVisitor implements html.Visitor {
|
||||||
|
|
||||||
private _createDirectiveAsts(
|
private _createDirectiveAsts(
|
||||||
isTemplateElement: boolean, elementName: string, directives: CompileDirectiveMetadata[],
|
isTemplateElement: boolean, elementName: string, directives: CompileDirectiveMetadata[],
|
||||||
props: BoundElementOrDirectiveProperty[], elementOrDirectiveRefs: ElementOrDirectiveRef[],
|
props: BoundProperty[], elementOrDirectiveRefs: ElementOrDirectiveRef[],
|
||||||
elementSourceSpan: ParseSourceSpan, targetReferences: ReferenceAst[]): DirectiveAst[] {
|
elementSourceSpan: ParseSourceSpan, targetReferences: ReferenceAst[]): DirectiveAst[] {
|
||||||
const matchedReferences = new Set<string>();
|
const matchedReferences = new Set<string>();
|
||||||
let component: CompileDirectiveMetadata = null;
|
let component: CompileDirectiveMetadata = null;
|
||||||
|
@ -809,9 +551,9 @@ class TemplateParseVisitor implements html.Visitor {
|
||||||
const hostProperties: BoundElementPropertyAst[] = [];
|
const hostProperties: BoundElementPropertyAst[] = [];
|
||||||
const hostEvents: BoundEventAst[] = [];
|
const hostEvents: BoundEventAst[] = [];
|
||||||
const directiveProperties: BoundDirectivePropertyAst[] = [];
|
const directiveProperties: BoundDirectivePropertyAst[] = [];
|
||||||
this._createDirectiveHostPropertyAsts(
|
this.createDirectiveHostPropertyAsts(
|
||||||
elementName, directive.hostProperties, sourceSpan, hostProperties);
|
elementName, directive.hostProperties, sourceSpan, hostProperties);
|
||||||
this._createDirectiveHostEventAsts(directive.hostListeners, sourceSpan, hostEvents);
|
this.createDirectiveHostEventAsts(directive.hostListeners, sourceSpan, hostEvents);
|
||||||
this._createDirectivePropertyAsts(directive.inputs, props, directiveProperties);
|
this._createDirectivePropertyAsts(directive.inputs, props, directiveProperties);
|
||||||
elementOrDirectiveRefs.forEach((elOrDirRef) => {
|
elementOrDirectiveRefs.forEach((elOrDirRef) => {
|
||||||
if ((elOrDirRef.value.length === 0 && directive.isComponent) ||
|
if ((elOrDirRef.value.length === 0 && directive.isComponent) ||
|
||||||
|
@ -827,7 +569,7 @@ class TemplateParseVisitor implements html.Visitor {
|
||||||
elementOrDirectiveRefs.forEach((elOrDirRef) => {
|
elementOrDirectiveRefs.forEach((elOrDirRef) => {
|
||||||
if (elOrDirRef.value.length > 0) {
|
if (elOrDirRef.value.length > 0) {
|
||||||
if (!matchedReferences.has(elOrDirRef.name)) {
|
if (!matchedReferences.has(elOrDirRef.name)) {
|
||||||
this._reportError(
|
this.reportError(
|
||||||
`There is no directive with "exportAs" set to "${elOrDirRef.value}"`,
|
`There is no directive with "exportAs" set to "${elOrDirRef.value}"`,
|
||||||
elOrDirRef.sourceSpan);
|
elOrDirRef.sourceSpan);
|
||||||
}
|
}
|
||||||
|
@ -842,47 +584,11 @@ class TemplateParseVisitor implements html.Visitor {
|
||||||
return directiveAsts;
|
return directiveAsts;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _createDirectiveHostPropertyAsts(
|
|
||||||
elementName: string, hostProps: {[key: string]: string}, sourceSpan: ParseSourceSpan,
|
|
||||||
targetPropertyAsts: BoundElementPropertyAst[]) {
|
|
||||||
if (hostProps) {
|
|
||||||
Object.keys(hostProps).forEach(propName => {
|
|
||||||
const expression = hostProps[propName];
|
|
||||||
if (typeof expression === 'string') {
|
|
||||||
const exprAst = this._parseBinding(expression, true, sourceSpan);
|
|
||||||
targetPropertyAsts.push(
|
|
||||||
this._createElementPropertyAst(elementName, propName, exprAst, sourceSpan));
|
|
||||||
} else {
|
|
||||||
this._reportError(
|
|
||||||
`Value of the host property binding "${propName}" needs to be a string representing an expression but got "${expression}" (${typeof expression})`,
|
|
||||||
sourceSpan);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _createDirectiveHostEventAsts(
|
|
||||||
hostListeners: {[key: string]: string}, sourceSpan: ParseSourceSpan,
|
|
||||||
targetEventAsts: BoundEventAst[]) {
|
|
||||||
if (hostListeners) {
|
|
||||||
Object.keys(hostListeners).forEach(propName => {
|
|
||||||
const expression = hostListeners[propName];
|
|
||||||
if (typeof expression === 'string') {
|
|
||||||
this._parseEventOrAnimationEvent(propName, expression, sourceSpan, [], targetEventAsts);
|
|
||||||
} else {
|
|
||||||
this._reportError(
|
|
||||||
`Value of the host listener "${propName}" needs to be a string representing an expression but got "${expression}" (${typeof expression})`,
|
|
||||||
sourceSpan);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _createDirectivePropertyAsts(
|
private _createDirectivePropertyAsts(
|
||||||
directiveProperties: {[key: string]: string}, boundProps: BoundElementOrDirectiveProperty[],
|
directiveProperties: {[key: string]: string}, boundProps: BoundProperty[],
|
||||||
targetBoundDirectiveProps: BoundDirectivePropertyAst[]) {
|
targetBoundDirectiveProps: BoundDirectivePropertyAst[]) {
|
||||||
if (directiveProperties) {
|
if (directiveProperties) {
|
||||||
const boundPropsByName = new Map<string, BoundElementOrDirectiveProperty>();
|
const boundPropsByName = new Map<string, BoundProperty>();
|
||||||
boundProps.forEach(boundProp => {
|
boundProps.forEach(boundProp => {
|
||||||
const prevValue = boundPropsByName.get(boundProp.name);
|
const prevValue = boundPropsByName.get(boundProp.name);
|
||||||
if (!prevValue || prevValue.isLiteral) {
|
if (!prevValue || prevValue.isLiteral) {
|
||||||
|
@ -905,7 +611,7 @@ class TemplateParseVisitor implements html.Visitor {
|
||||||
}
|
}
|
||||||
|
|
||||||
private _createElementPropertyAsts(
|
private _createElementPropertyAsts(
|
||||||
elementName: string, props: BoundElementOrDirectiveProperty[],
|
elementName: string, props: BoundProperty[],
|
||||||
directives: DirectiveAst[]): BoundElementPropertyAst[] {
|
directives: DirectiveAst[]): BoundElementPropertyAst[] {
|
||||||
const boundElementProps: BoundElementPropertyAst[] = [];
|
const boundElementProps: BoundElementPropertyAst[] = [];
|
||||||
const boundDirectivePropsIndex = new Map<string, BoundDirectivePropertyAst>();
|
const boundDirectivePropsIndex = new Map<string, BoundDirectivePropertyAst>();
|
||||||
|
@ -916,98 +622,14 @@ class TemplateParseVisitor implements html.Visitor {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
props.forEach((prop: BoundElementOrDirectiveProperty) => {
|
props.forEach((prop: BoundProperty) => {
|
||||||
if (!prop.isLiteral && !boundDirectivePropsIndex.get(prop.name)) {
|
if (!prop.isLiteral && !boundDirectivePropsIndex.get(prop.name)) {
|
||||||
boundElementProps.push(this._createElementPropertyAst(
|
boundElementProps.push(this.createElementPropertyAst(elementName, prop));
|
||||||
elementName, prop.name, prop.expression, prop.sourceSpan));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return boundElementProps;
|
return boundElementProps;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _createElementPropertyAst(
|
|
||||||
elementName: string, name: string, ast: AST,
|
|
||||||
sourceSpan: ParseSourceSpan): BoundElementPropertyAst {
|
|
||||||
let unit: string = null;
|
|
||||||
let bindingType: PropertyBindingType;
|
|
||||||
let boundPropertyName: string;
|
|
||||||
const parts = name.split(PROPERTY_PARTS_SEPARATOR);
|
|
||||||
let securityContext: SecurityContext;
|
|
||||||
|
|
||||||
if (parts.length === 1) {
|
|
||||||
var partValue = parts[0];
|
|
||||||
if (_isAnimationLabel(partValue)) {
|
|
||||||
boundPropertyName = partValue.substr(1);
|
|
||||||
bindingType = PropertyBindingType.Animation;
|
|
||||||
securityContext = SecurityContext.NONE;
|
|
||||||
} else {
|
|
||||||
boundPropertyName = this._schemaRegistry.getMappedPropName(partValue);
|
|
||||||
securityContext = this._schemaRegistry.securityContext(elementName, boundPropertyName);
|
|
||||||
bindingType = PropertyBindingType.Property;
|
|
||||||
this._validatePropertyOrAttributeName(boundPropertyName, 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, sourceSpan);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (parts[0] == ATTRIBUTE_PREFIX) {
|
|
||||||
boundPropertyName = parts[1];
|
|
||||||
this._validatePropertyOrAttributeName(boundPropertyName, 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);
|
|
||||||
|
|
||||||
const nsSeparatorIdx = boundPropertyName.indexOf(':');
|
|
||||||
if (nsSeparatorIdx > -1) {
|
|
||||||
const ns = boundPropertyName.substring(0, nsSeparatorIdx);
|
|
||||||
const name = boundPropertyName.substring(nsSeparatorIdx + 1);
|
|
||||||
boundPropertyName = mergeNsAndName(ns, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
bindingType = PropertyBindingType.Attribute;
|
|
||||||
} else if (parts[0] == CLASS_PREFIX) {
|
|
||||||
boundPropertyName = parts[1];
|
|
||||||
bindingType = PropertyBindingType.Class;
|
|
||||||
securityContext = SecurityContext.NONE;
|
|
||||||
} else if (parts[0] == STYLE_PREFIX) {
|
|
||||||
unit = parts.length > 2 ? parts[2] : null;
|
|
||||||
boundPropertyName = parts[1];
|
|
||||||
bindingType = PropertyBindingType.Style;
|
|
||||||
securityContext = SecurityContext.STYLE;
|
|
||||||
} else {
|
|
||||||
this._reportError(`Invalid property name '${name}'`, sourceSpan);
|
|
||||||
bindingType = null;
|
|
||||||
securityContext = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new BoundElementPropertyAst(
|
|
||||||
boundPropertyName, bindingType, securityContext, ast, unit, sourceSpan);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param propName the name of the property / attribute
|
|
||||||
* @param sourceSpan
|
|
||||||
* @param isAttr true when binding to an attribute
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private _validatePropertyOrAttributeName(
|
|
||||||
propName: string, sourceSpan: ParseSourceSpan, isAttr: boolean): void {
|
|
||||||
const report = isAttr ? this._schemaRegistry.validateAttribute(propName) :
|
|
||||||
this._schemaRegistry.validateProperty(propName);
|
|
||||||
if (report.error) {
|
|
||||||
this._reportError(report.msg, sourceSpan, ParseErrorLevel.FATAL);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _findComponentDirectives(directives: DirectiveAst[]): DirectiveAst[] {
|
private _findComponentDirectives(directives: DirectiveAst[]): DirectiveAst[] {
|
||||||
return directives.filter(directive => directive.directive.isComponent);
|
return directives.filter(directive => directive.directive.isComponent);
|
||||||
}
|
}
|
||||||
|
@ -1020,7 +642,7 @@ class TemplateParseVisitor implements html.Visitor {
|
||||||
private _assertOnlyOneComponent(directives: DirectiveAst[], sourceSpan: ParseSourceSpan) {
|
private _assertOnlyOneComponent(directives: DirectiveAst[], sourceSpan: ParseSourceSpan) {
|
||||||
const componentTypeNames = this._findComponentDirectiveNames(directives);
|
const componentTypeNames = this._findComponentDirectiveNames(directives);
|
||||||
if (componentTypeNames.length > 1) {
|
if (componentTypeNames.length > 1) {
|
||||||
this._reportError(`More than one component: ${componentTypeNames.join(',')}`, sourceSpan);
|
this.reportError(`More than one component: ${componentTypeNames.join(',')}`, sourceSpan);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1040,7 +662,7 @@ class TemplateParseVisitor implements html.Visitor {
|
||||||
const errorMsg = `'${elName}' is not a known element:\n` +
|
const errorMsg = `'${elName}' is not a known element:\n` +
|
||||||
`1. If '${elName}' is an Angular component, then verify that it is part of this module.\n` +
|
`1. If '${elName}' is an Angular component, then verify that it is part of this module.\n` +
|
||||||
`2. If '${elName}' is a Web Component then add "CUSTOM_ELEMENTS_SCHEMA" to the '@NgModule.schemas' of this component to suppress this message.`;
|
`2. If '${elName}' is a Web Component then add "CUSTOM_ELEMENTS_SCHEMA" to the '@NgModule.schemas' of this component to suppress this message.`;
|
||||||
this._reportError(errorMsg, element.sourceSpan);
|
this.reportError(errorMsg, element.sourceSpan);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1049,11 +671,11 @@ class TemplateParseVisitor implements html.Visitor {
|
||||||
sourceSpan: ParseSourceSpan) {
|
sourceSpan: ParseSourceSpan) {
|
||||||
const componentTypeNames: string[] = this._findComponentDirectiveNames(directives);
|
const componentTypeNames: string[] = this._findComponentDirectiveNames(directives);
|
||||||
if (componentTypeNames.length > 0) {
|
if (componentTypeNames.length > 0) {
|
||||||
this._reportError(
|
this.reportError(
|
||||||
`Components on an embedded template: ${componentTypeNames.join(',')}`, sourceSpan);
|
`Components on an embedded template: ${componentTypeNames.join(',')}`, sourceSpan);
|
||||||
}
|
}
|
||||||
elementProps.forEach(prop => {
|
elementProps.forEach(prop => {
|
||||||
this._reportError(
|
this.reportError(
|
||||||
`Property binding ${prop.name} not used by any directive on an embedded template. Make sure that the property name is spelled correctly and all directives are listed in the "directives" section.`,
|
`Property binding ${prop.name} not used by any directive on an embedded template. Make sure that the property name is spelled correctly and all directives are listed in the "directives" section.`,
|
||||||
sourceSpan);
|
sourceSpan);
|
||||||
});
|
});
|
||||||
|
@ -1072,7 +694,7 @@ class TemplateParseVisitor implements html.Visitor {
|
||||||
|
|
||||||
events.forEach(event => {
|
events.forEach(event => {
|
||||||
if (isPresent(event.target) || !allDirectiveEvents.has(event.name)) {
|
if (isPresent(event.target) || !allDirectiveEvents.has(event.name)) {
|
||||||
this._reportError(
|
this.reportError(
|
||||||
`Event binding ${event.fullName} not emitted by any directive on an embedded template. Make sure that the event name is spelled correctly and all directives are listed in the "directives" section.`,
|
`Event binding ${event.fullName} not emitted by any directive on an embedded template. Make sure that the event name is spelled correctly and all directives are listed in the "directives" section.`,
|
||||||
event.sourceSpan);
|
event.sourceSpan);
|
||||||
}
|
}
|
||||||
|
@ -1116,12 +738,6 @@ class NonBindableVisitor implements html.Visitor {
|
||||||
visitExpansionCase(expansionCase: html.ExpansionCase, context: any): any { return expansionCase; }
|
visitExpansionCase(expansionCase: html.ExpansionCase, context: any): any { return expansionCase; }
|
||||||
}
|
}
|
||||||
|
|
||||||
class BoundElementOrDirectiveProperty {
|
|
||||||
constructor(
|
|
||||||
public name: string, public expression: AST, public isLiteral: boolean,
|
|
||||||
public sourceSpan: ParseSourceSpan) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ElementOrDirectiveRef {
|
class ElementOrDirectiveRef {
|
||||||
constructor(public name: string, public value: string, public sourceSpan: ParseSourceSpan) {}
|
constructor(public name: string, public value: string, public sourceSpan: ParseSourceSpan) {}
|
||||||
}
|
}
|
||||||
|
@ -1189,21 +805,6 @@ function createElementCssSelector(elementName: string, matchableAttrs: string[][
|
||||||
const EMPTY_ELEMENT_CONTEXT = new ElementContext(true, new SelectorMatcher(), null, null);
|
const EMPTY_ELEMENT_CONTEXT = new ElementContext(true, new SelectorMatcher(), null, null);
|
||||||
const NON_BINDABLE_VISITOR = new NonBindableVisitor();
|
const NON_BINDABLE_VISITOR = new NonBindableVisitor();
|
||||||
|
|
||||||
|
|
||||||
export class PipeCollector extends RecursiveAstVisitor {
|
|
||||||
pipes: Set<string> = new Set<string>();
|
|
||||||
visitPipe(ast: BindingPipe, context: any): any {
|
|
||||||
this.pipes.add(ast.name);
|
|
||||||
ast.exp.visit(this);
|
|
||||||
this.visitAll(ast.args, context);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function _isAnimationLabel(name: string): boolean {
|
|
||||||
return name[0] == '@';
|
|
||||||
}
|
|
||||||
|
|
||||||
function _isEmptyTextNode(node: html.Node): boolean {
|
function _isEmptyTextNode(node: html.Node): boolean {
|
||||||
return node instanceof html.Text && node.value.trim().length == 0;
|
return node instanceof html.Text && node.value.trim().length == 0;
|
||||||
}
|
}
|
Loading…
Reference in New Issue