Skip to content

Commit d7b9345

Browse files
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 angular#2598
1 parent f158fbd commit d7b9345

File tree

10 files changed

+61
-41
lines changed

10 files changed

+61
-41
lines changed

modules/angular2/src/render/dom/compiler/directive_parser.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ export class DirectiveParser implements CompileStep {
149149
if (isPresent(bindingAst)) {
150150
directiveBinderBuilder.bindProperty(dirProperty, bindingAst);
151151
}
152+
compileElement.bindElement().bindPropertyToDirective(dashCaseToCamelCase(elProp));
152153
}
153154

154155
_bindDirectiveEvent(eventName, action, compileElement, directiveBinderBuilder) {

modules/angular2/src/render/dom/view/property_setter_factory.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const CLASS_PREFIX = 'class.';
1818
const STYLE_PREFIX = 'style.';
1919

2020
export class PropertySetterFactory {
21-
private static _noopSetter(el, value) {}
21+
static noopSetter(el, value) {}
2222

2323
private _lazyPropertySettersCache: StringMap<string, Function> = StringMapWrapper.create();
2424
private _eagerPropertySettersCache: StringMap<string, Function> = StringMapWrapper.create();
@@ -69,7 +69,7 @@ export class PropertySetterFactory {
6969
if (DOM.hasProperty(protoElement, property)) {
7070
setterFn = reflector.setter(property);
7171
} else {
72-
setterFn = PropertySetterFactory._noopSetter;
72+
setterFn = PropertySetterFactory.noopSetter;
7373
}
7474
StringMapWrapper.set(this._eagerPropertySettersCache, property, setterFn);
7575
}

modules/angular2/src/render/dom/view/proto_view_builder.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,17 @@ export class ProtoViewBuilder {
7575
});
7676

7777
MapWrapper.forEach(ebb.propertyBindings, (_, propertyName) => {
78+
var propSetter =
79+
setterFactory.createSetter(ebb.element, isPresent(ebb.componentId), propertyName);
7880

79-
propertySetters.set(
80-
propertyName,
81-
setterFactory.createSetter(ebb.element, isPresent(ebb.componentId), propertyName));
81+
if (propSetter === PropertySetterFactory.noopSetter) {
82+
if (!SetWrapper.has(ebb.propertyBindingsToDirectives, propertyName)) {
83+
throw new BaseException(
84+
`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`);
85+
}
86+
}
87+
88+
propertySetters.set(propertyName, propSetter);
8289
});
8390

8491
var nestedProtoView =
@@ -170,6 +177,7 @@ export class ElementBinderBuilder {
170177
nestedProtoView: ProtoViewBuilder = null;
171178
propertyBindings: Map<string, ASTWithSource> = new Map();
172179
variableBindings: Map<string, string> = new Map();
180+
propertyBindingsToDirectives: Set<string> = new Set();
173181
eventBindings: List<api.EventBinding> = [];
174182
eventBuilder: EventBuilder = new EventBuilder();
175183
textBindingNodes: List</*node*/ any> = [];
@@ -210,6 +218,12 @@ export class ElementBinderBuilder {
210218

211219
bindProperty(name, expression) { this.propertyBindings.set(name, expression); }
212220

221+
bindPropertyToDirective(name: string) {
222+
// we are filling in a set of property names that are bound to a property
223+
// of at least one directive. This allows us to report "dangling" bindings.
224+
this.propertyBindingsToDirectives.add(name);
225+
}
226+
213227
bindVariable(name, value) {
214228
// When current is a view root, the variable bindings are set to the *nested* proto view.
215229
// The root view conceptually signifies a new "block scope" (the nested view), to which

modules/angular2/test/core/compiler/integration_spec.ts

Lines changed: 32 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -192,22 +192,6 @@ export function main() {
192192
});
193193
}));
194194

195-
it('should ignore bindings to unknown properties',
196-
inject([TestBed, AsyncTestCompleter], (tb: TestBed, async) => {
197-
tb.overrideView(MyComp,
198-
new viewAnn.View({template: '<div unknown="{{ctxProp}}"></div>'}));
199-
200-
tb.createView(MyComp, {context: ctx})
201-
.then((view) => {
202-
203-
ctx.ctxProp = 'Some value';
204-
view.detectChanges();
205-
expect(DOM.hasProperty(view.rootNodes[0], 'unknown')).toBeFalsy();
206-
207-
async.done();
208-
});
209-
}));
210-
211195
it('should consume directive watch expression change.',
212196
inject([TestBed, AsyncTestCompleter], (tb: TestBed, async) => {
213197
var tpl = '<div>' +
@@ -247,7 +231,7 @@ export function main() {
247231
it("should support pipes in bindings",
248232
inject([TestBed, AsyncTestCompleter], (tb: TestBed, async) => {
249233
tb.overrideView(MyComp, new viewAnn.View({
250-
template: '<div [my-dir] #dir="mydir" [elprop]="ctxProp | double"></div>',
234+
template: '<div my-dir #dir="mydir" [elprop]="ctxProp | double"></div>',
251235
directives: [MyDir]
252236
}));
253237

@@ -442,7 +426,7 @@ export function main() {
442426
it('should assign a directive to a var-',
443427
inject([TestBed, AsyncTestCompleter], (tb: TestBed, async) => {
444428
tb.overrideView(MyComp, new viewAnn.View({
445-
template: '<p><div [export-dir] #localdir="dir"></div></p>',
429+
template: '<p><div export-dir #localdir="dir"></div></p>',
446430
directives: [ExportDir]
447431
}));
448432

@@ -1144,7 +1128,7 @@ export function main() {
11441128
it('should specify a location of an error that happened during change detection (element property)',
11451129
inject([TestBed, AsyncTestCompleter], (tb: TestBed, async) => {
11461130

1147-
tb.overrideView(MyComp, new viewAnn.View({template: '<div [prop]="a.b"></div>'}));
1131+
tb.overrideView(MyComp, new viewAnn.View({template: '<div [title]="a.b"></div>'}));
11481132

11491133
tb.createView(MyComp, {context: ctx})
11501134
.then((view) => {
@@ -1157,10 +1141,10 @@ export function main() {
11571141
it('should specify a location of an error that happened during change detection (directive property)',
11581142
inject([TestBed, AsyncTestCompleter], (tb: TestBed, async) => {
11591143

1160-
tb.overrideView(
1161-
MyComp,
1162-
new viewAnn.View(
1163-
{template: '<child-cmp [prop]="a.b"></child-cmp>', directives: [ChildComp]}));
1144+
tb.overrideView(MyComp, new viewAnn.View({
1145+
template: '<child-cmp [dir-prop]="a.b"></child-cmp>',
1146+
directives: [ChildComp]
1147+
}));
11641148

11651149
tb.createView(MyComp, {context: ctx})
11661150
.then((view) => {
@@ -1205,6 +1189,30 @@ export function main() {
12051189
});
12061190
}));
12071191

1192+
describe('Missing property bindings', () => {
1193+
it('should throw on bindings to unknown properties',
1194+
inject([TestBed, AsyncTestCompleter], (tb: TestBed, async) => {
1195+
tb.overrideView(MyComp,
1196+
new viewAnn.View({template: '<div unknown="{{ctxProp}}"></div>'}));
1197+
1198+
PromiseWrapper.catchError(tb.createView(MyComp, {context: ctx}), (e) => {
1199+
expect(e.message).toEqual(
1200+
`Can't bind to 'unknown' since it isn't a know property of the 'div' element and there are no matching directives with a corresponding property`);
1201+
async.done();
1202+
return null;
1203+
});
1204+
}));
1205+
1206+
it('should not throw for property binding to a non-existing property when there is a matching directive property',
1207+
inject([TestBed, AsyncTestCompleter], (tb: TestBed, async) => {
1208+
tb.overrideView(
1209+
MyComp,
1210+
new viewAnn.View(
1211+
{template: '<div my-dir [elprop]="ctxProp"></div>', directives: [MyDir]}));
1212+
tb.createView(MyComp, {context: ctx}).then((val) => { async.done(); });
1213+
}));
1214+
});
1215+
12081216
// Disabled until a solution is found, refs:
12091217
// - https://github.com/angular/angular/issues/776
12101218
// - https://github.com/angular/angular/commit/81f3f32
@@ -1353,10 +1361,7 @@ class ComponentWithPipes {
13531361
prop: string;
13541362
}
13551363

1356-
@Component({
1357-
selector: 'child-cmp',
1358-
appInjector: [MyService],
1359-
})
1364+
@Component({selector: 'child-cmp', properties: ['dirProp'], appInjector: [MyService]})
13601365
@View({directives: [MyDir], template: '{{ctxProp}}'})
13611366
@Injectable()
13621367
class ChildComp {

modules/angular2/test/core/directive_lifecycle_integration_spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export function main() {
3636
tb.overrideView(
3737
MyComp,
3838
new viewAnn.View(
39-
{template: '<div [field]="123" [lifecycle]></div>', directives: [LifecycleDir]}));
39+
{template: '<div [field]="123" lifecycle></div>', directives: [LifecycleDir]}));
4040

4141
tb.createView(MyComp, {context: ctx})
4242
.then((view) => {

modules/angular2/test/directives/ng_for_spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ export function main() {
205205

206206
it('should repeat over nested arrays with no intermediate element',
207207
inject([TestBed, AsyncTestCompleter], (tb: TestBed, async) => {
208-
var template = '<div><template [ng-for] #item [ng-for-of]="items">' +
208+
var template = '<div><template ng-for #item [ng-for-of]="items">' +
209209
'<div template="ng-for #subitem of item">' +
210210
'{{subitem}}-{{item.length}};' +
211211
'</div></template></div>';

modules/angular2/test/directives/ng_switch_spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,8 @@ export function main() {
7979
'<template [ng-switch-when]="\'b\'"><li>when b1;</li></template>' +
8080
'<template [ng-switch-when]="\'a\'"><li>when a2;</li></template>' +
8181
'<template [ng-switch-when]="\'b\'"><li>when b2;</li></template>' +
82-
'<template [ng-switch-default]><li>when default1;</li></template>' +
83-
'<template [ng-switch-default]><li>when default2;</li></template>' +
82+
'<template ng-switch-default><li>when default1;</li></template>' +
83+
'<template ng-switch-default><li>when default2;</li></template>' +
8484
'</ul></div>';
8585

8686
tb.createView(TestComponent, {html: template})
@@ -108,7 +108,7 @@ export function main() {
108108
'<ul [ng-switch]="switchValue">' +
109109
'<template [ng-switch-when]="when1"><li>when 1;</li></template>' +
110110
'<template [ng-switch-when]="when2"><li>when 2;</li></template>' +
111-
'<template [ng-switch-default]><li>when default;</li></template>' +
111+
'<template ng-switch-default><li>when default;</li></template>' +
112112
'</ul></div>';
113113

114114
tb.createView(TestComponent, {html: template})

modules/angular2/test/render/dom/shadow_dom_emulation_integration_spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -511,7 +511,7 @@ var conditionalContentComponent = DirectiveMetadata.create({
511511
});
512512

513513
var autoViewportDirective = DirectiveMetadata.create(
514-
{selector: '[auto]', id: '[auto]', type: DirectiveMetadata.DIRECTIVE_TYPE});
514+
{selector: '[auto]', id: 'auto', properties: ['auto'], type: DirectiveMetadata.DIRECTIVE_TYPE});
515515

516516
var tabComponent =
517517
DirectiveMetadata.create({selector: 'tab', id: 'tab', type: DirectiveMetadata.COMPONENT_TYPE});

modules/benchmarks/src/costs/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ class DynamicDummy {
7777
</div>
7878
7979
<div *ng-if="testingWithDirectives">
80-
<dummy [dummy-decorator] *ng-for="#i of list"></dummy>
80+
<dummy dummy-decorator *ng-for="#i of list"></dummy>
8181
</div>
8282
8383
<div *ng-if="testingDynamicComponents">

modules/benchmarks/src/largetable/largetable_benchmark.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ class CellData {
232232
</tbody>
233233
<tbody template="ng-switch-when 'interpolationAttr'">
234234
<tr template="ng-for #row of data">
235-
<td template="ng-for #column of row" i="{{column.i}}" j="{{column.j}}">
235+
<td template="ng-for #column of row" attr.i="{{column.i}}" attr.j="{{column.j}}">
236236
i,j attrs
237237
</td>
238238
</tr>
@@ -269,7 +269,7 @@ class LargetableComponent {
269269
@Component({selector: 'app'})
270270
@View({
271271
directives: [LargetableComponent],
272-
template: `<largetable [data]='data' [benchmarkType]='benchmarkType'></largetable>`
272+
template: `<largetable [data]='data' [benchmark-type]='benchmarkType'></largetable>`
273273
})
274274
class AppComponent {
275275
data;

0 commit comments

Comments
 (0)