feat(compiler): detect dangling property bindings

BREAKING CHANGE: compiler will throw on binding to non-existing properties.

Till now it was possible to have a binding to a non-existing property,
ex.: `<div [foo]="exp">`. From now on this is compilation error - any
property binding needs to have at least one associated property:
eaither on an HTML element or on any directive associated with a
given element (directives' properites need to be declared using the
`properties` field in the `@Directive` / `@Component` annotation).

Closes #2598
This commit is contained in:
Pawel Kozlowski
2015-06-17 16:05:35 +02:00
parent f158fbd131
commit d7b9345b6d
10 changed files with 61 additions and 41 deletions

View File

@ -149,6 +149,7 @@ export class DirectiveParser implements CompileStep {
if (isPresent(bindingAst)) {
directiveBinderBuilder.bindProperty(dirProperty, bindingAst);
}
compileElement.bindElement().bindPropertyToDirective(dashCaseToCamelCase(elProp));
}
_bindDirectiveEvent(eventName, action, compileElement, directiveBinderBuilder) {

View File

@ -18,7 +18,7 @@ const CLASS_PREFIX = 'class.';
const STYLE_PREFIX = 'style.';
export class PropertySetterFactory {
private static _noopSetter(el, value) {}
static noopSetter(el, value) {}
private _lazyPropertySettersCache: StringMap<string, Function> = StringMapWrapper.create();
private _eagerPropertySettersCache: StringMap<string, Function> = StringMapWrapper.create();
@ -69,7 +69,7 @@ export class PropertySetterFactory {
if (DOM.hasProperty(protoElement, property)) {
setterFn = reflector.setter(property);
} else {
setterFn = PropertySetterFactory._noopSetter;
setterFn = PropertySetterFactory.noopSetter;
}
StringMapWrapper.set(this._eagerPropertySettersCache, property, setterFn);
}

View File

@ -75,10 +75,17 @@ export class ProtoViewBuilder {
});
MapWrapper.forEach(ebb.propertyBindings, (_, propertyName) => {
var propSetter =
setterFactory.createSetter(ebb.element, isPresent(ebb.componentId), propertyName);
propertySetters.set(
propertyName,
setterFactory.createSetter(ebb.element, isPresent(ebb.componentId), propertyName));
if (propSetter === PropertySetterFactory.noopSetter) {
if (!SetWrapper.has(ebb.propertyBindingsToDirectives, propertyName)) {
throw new BaseException(
`Can't bind to '${propertyName}' since it isn't a know property of the '${DOM.tagName(ebb.element).toLowerCase()}' element and there are no matching directives with a corresponding property`);
}
}
propertySetters.set(propertyName, propSetter);
});
var nestedProtoView =
@ -170,6 +177,7 @@ export class ElementBinderBuilder {
nestedProtoView: ProtoViewBuilder = null;
propertyBindings: Map<string, ASTWithSource> = new Map();
variableBindings: Map<string, string> = new Map();
propertyBindingsToDirectives: Set<string> = new Set();
eventBindings: List<api.EventBinding> = [];
eventBuilder: EventBuilder = new EventBuilder();
textBindingNodes: List</*node*/ any> = [];
@ -210,6 +218,12 @@ export class ElementBinderBuilder {
bindProperty(name, expression) { this.propertyBindings.set(name, expression); }
bindPropertyToDirective(name: string) {
// we are filling in a set of property names that are bound to a property
// of at least one directive. This allows us to report "dangling" bindings.
this.propertyBindingsToDirectives.add(name);
}
bindVariable(name, value) {
// When current is a view root, the variable bindings are set to the *nested* proto view.
// The root view conceptually signifies a new "block scope" (the nested view), to which