Skip to content

Commit 92ffc46

Browse files
committed
feat(host): limits host properties to renames
1 parent c1a494b commit 92ffc46

File tree

8 files changed

+177
-24
lines changed

8 files changed

+177
-24
lines changed

modules/angular2/src/change_detection/parser/parser.ts

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ import {
4545
SafeMethodCall,
4646
FunctionCall,
4747
TemplateBinding,
48-
ASTWithSource
48+
ASTWithSource,
49+
AstVisitor
4950
} from './ast';
5051

5152

@@ -73,6 +74,12 @@ export class Parser {
7374
return new ASTWithSource(ast, input, location);
7475
}
7576

77+
parseSimpleBinding(input: string, location: string): ASTWithSource {
78+
var tokens = this._lexer.tokenize(input);
79+
var ast = new _ParseAST(input, location, tokens, this._reflector, false).parseSimpleBinding();
80+
return new ASTWithSource(ast, input, location);
81+
}
82+
7683
parseTemplateBindings(input: string, location: any): List<TemplateBinding> {
7784
var tokens = this._lexer.tokenize(input);
7885
return new _ParseAST(input, location, tokens, this._reflector, false).parseTemplateBindings();
@@ -202,6 +209,14 @@ class _ParseAST {
202209
return new Chain(exprs);
203210
}
204211

212+
parseSimpleBinding(): AST {
213+
var ast = this.parseChain();
214+
if (!SimpleExpressionChecker.check(ast)) {
215+
this.error(`Simple binding expression can only contain field access and constants'`);
216+
}
217+
return ast;
218+
}
219+
205220
parsePipe() {
206221
var result = this.parseExpression();
207222
if (this.optionalOperator("|")) {
@@ -590,3 +605,57 @@ class _ParseAST {
590605
`Parser Error: ${message} ${location} [${this.input}] in ${this.location}`);
591606
}
592607
}
608+
609+
class SimpleExpressionChecker implements AstVisitor {
610+
static check(ast: AST) {
611+
var s = new SimpleExpressionChecker();
612+
ast.visit(s);
613+
return s.simple;
614+
}
615+
616+
simple = true;
617+
618+
visitImplicitReceiver(ast: ImplicitReceiver) {}
619+
620+
visitInterpolation(ast: Interpolation) { this.simple = false; }
621+
622+
visitLiteralPrimitive(ast: LiteralPrimitive) {}
623+
624+
visitAccessMember(ast: AccessMember) {}
625+
626+
visitSafeAccessMember(ast: SafeAccessMember) { this.simple = false; }
627+
628+
visitMethodCall(ast: MethodCall) { this.simple = false; }
629+
630+
visitSafeMethodCall(ast: SafeMethodCall) { this.simple = false; }
631+
632+
visitFunctionCall(ast: FunctionCall) { this.simple = false; }
633+
634+
visitLiteralArray(ast: LiteralArray) { this.visitAll(ast.expressions); }
635+
636+
visitLiteralMap(ast: LiteralMap) { this.visitAll(ast.values); }
637+
638+
visitBinary(ast: Binary) { this.simple = false; }
639+
640+
visitPrefixNot(ast: PrefixNot) { this.simple = false; }
641+
642+
visitConditional(ast: Conditional) { this.simple = false; }
643+
644+
visitPipe(ast: BindingPipe) { this.simple = false; }
645+
646+
visitKeyedAccess(ast: KeyedAccess) { this.simple = false; }
647+
648+
visitAll(asts: List<any>) {
649+
var res = ListWrapper.createFixedSize(asts.length);
650+
for (var i = 0; i < asts.length; ++i) {
651+
res[i] = asts[i].visit(this);
652+
}
653+
return res;
654+
}
655+
656+
visitChain(ast: Chain) { this.simple = false; }
657+
658+
visitAssignment(ast: Assignment) { this.simple = false; }
659+
660+
visitIf(ast: If) { this.simple = false; }
661+
}

modules/angular2/src/forms/directives/checkbox_value_accessor.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {Directive, Renderer, ElementRef} from 'angular2/angular2';
22
import {NgControl} from './ng_control';
33
import {ControlValueAccessor} from './control_value_accessor';
4+
import {isPresent} from 'angular2/src/facade/lang';
45
import {setProperty} from './shared';
56

67
/**
@@ -20,12 +21,12 @@ import {setProperty} from './shared';
2021
'(change)': 'onChange($event.target.checked)',
2122
'(blur)': 'onTouched()',
2223
'[checked]': 'checked',
23-
'[class.ng-untouched]': 'cd.control?.untouched == true',
24-
'[class.ng-touched]': 'cd.control?.touched == true',
25-
'[class.ng-pristine]': 'cd.control?.pristine == true',
26-
'[class.ng-dirty]': 'cd.control?.dirty == true',
27-
'[class.ng-valid]': 'cd.control?.valid == true',
28-
'[class.ng-invalid]': 'cd.control?.valid == false'
24+
'[class.ng-untouched]': 'ngClassUntouched',
25+
'[class.ng-touched]': 'ngClassTouched',
26+
'[class.ng-pristine]': 'ngClassPristine',
27+
'[class.ng-dirty]': 'ngClassDirty',
28+
'[class.ng-valid]': 'ngClassValid',
29+
'[class.ng-invalid]': 'ngClassInvalid'
2930
}
3031
})
3132
export class CheckboxControlValueAccessor implements ControlValueAccessor {
@@ -44,6 +45,21 @@ export class CheckboxControlValueAccessor implements ControlValueAccessor {
4445
setProperty(this.renderer, this.elementRef, "checked", value);
4546
}
4647

48+
get ngClassUntouched(): boolean {
49+
return isPresent(this.cd.control) ? this.cd.control.untouched : false;
50+
}
51+
get ngClassTouched(): boolean {
52+
return isPresent(this.cd.control) ? this.cd.control.touched : false;
53+
}
54+
get ngClassPristine(): boolean {
55+
return isPresent(this.cd.control) ? this.cd.control.pristine : false;
56+
}
57+
get ngClassDirty(): boolean { return isPresent(this.cd.control) ? this.cd.control.dirty : false; }
58+
get ngClassValid(): boolean { return isPresent(this.cd.control) ? this.cd.control.valid : false; }
59+
get ngClassInvalid(): boolean {
60+
return isPresent(this.cd.control) ? !this.cd.control.valid : false;
61+
}
62+
4763
registerOnChange(fn): void { this.onChange = fn; }
4864
registerOnTouched(fn): void { this.onTouched = fn; }
4965
}

modules/angular2/src/forms/directives/default_value_accessor.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {Directive, Renderer, ElementRef} from 'angular2/angular2';
22
import {NgControl} from './ng_control';
33
import {ControlValueAccessor} from './control_value_accessor';
4-
import {isBlank} from 'angular2/src/facade/lang';
4+
import {isBlank, isPresent} from 'angular2/src/facade/lang';
55
import {setProperty} from './shared';
66

77
/**
@@ -23,16 +23,17 @@ import {setProperty} from './shared';
2323
'(input)': 'onChange($event.target.value)',
2424
'(blur)': 'onTouched()',
2525
'[value]': 'value',
26-
'[class.ng-untouched]': 'cd.control?.untouched == true',
27-
'[class.ng-touched]': 'cd.control?.touched == true',
28-
'[class.ng-pristine]': 'cd.control?.pristine == true',
29-
'[class.ng-dirty]': 'cd.control?.dirty == true',
30-
'[class.ng-valid]': 'cd.control?.valid == true',
31-
'[class.ng-invalid]': 'cd.control?.valid == false'
26+
'[class.ng-untouched]': 'ngClassUntouched',
27+
'[class.ng-touched]': 'ngClassTouched',
28+
'[class.ng-pristine]': 'ngClassPristine',
29+
'[class.ng-dirty]': 'ngClassDirty',
30+
'[class.ng-valid]': 'ngClassValid',
31+
'[class.ng-invalid]': 'ngClassInvalid'
3232
}
3333
})
3434
export class DefaultValueAccessor implements ControlValueAccessor {
3535
value: string = null;
36+
3637
onChange = (_) => {};
3738
onTouched = () => {};
3839

@@ -47,6 +48,21 @@ export class DefaultValueAccessor implements ControlValueAccessor {
4748
setProperty(this.renderer, this.elementRef, 'value', this.value);
4849
}
4950

51+
get ngClassUntouched(): boolean {
52+
return isPresent(this.cd.control) ? this.cd.control.untouched : false;
53+
}
54+
get ngClassTouched(): boolean {
55+
return isPresent(this.cd.control) ? this.cd.control.touched : false;
56+
}
57+
get ngClassPristine(): boolean {
58+
return isPresent(this.cd.control) ? this.cd.control.pristine : false;
59+
}
60+
get ngClassDirty(): boolean { return isPresent(this.cd.control) ? this.cd.control.dirty : false; }
61+
get ngClassValid(): boolean { return isPresent(this.cd.control) ? this.cd.control.valid : false; }
62+
get ngClassInvalid(): boolean {
63+
return isPresent(this.cd.control) ? !this.cd.control.valid : false;
64+
}
65+
5066
registerOnChange(fn): void { this.onChange = fn; }
5167

5268
registerOnTouched(fn): void { this.onTouched = fn; }

modules/angular2/src/forms/directives/select_control_value_accessor.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {Directive, Query, QueryList, Renderer, ElementRef} from 'angular2/angular2';
22
import {NgControl} from './ng_control';
33
import {ControlValueAccessor} from './control_value_accessor';
4+
import {isPresent} from 'angular2/src/facade/lang';
45
import {setProperty} from './shared';
56

67
/**
@@ -30,12 +31,12 @@ export class NgSelectOption {
3031
'(input)': 'onChange($event.target.value)',
3132
'(blur)': 'onTouched()',
3233
'[value]': 'value',
33-
'[class.ng-untouched]': 'cd.control?.untouched == true',
34-
'[class.ng-touched]': 'cd.control?.touched == true',
35-
'[class.ng-pristine]': 'cd.control?.pristine == true',
36-
'[class.ng-dirty]': 'cd.control?.dirty == true',
37-
'[class.ng-valid]': 'cd.control?.valid == true',
38-
'[class.ng-invalid]': 'cd.control?.valid == false'
34+
'[class.ng-untouched]': 'ngClassUntouched',
35+
'[class.ng-touched]': 'ngClassTouched',
36+
'[class.ng-pristine]': 'ngClassPristine',
37+
'[class.ng-dirty]': 'ngClassDirty',
38+
'[class.ng-valid]': 'ngClassValid',
39+
'[class.ng-invalid]': 'ngClassInvalid'
3940
}
4041
})
4142
export class SelectControlValueAccessor implements ControlValueAccessor {
@@ -57,6 +58,21 @@ export class SelectControlValueAccessor implements ControlValueAccessor {
5758
setProperty(this.renderer, this.elementRef, "value", value);
5859
}
5960

61+
get ngClassUntouched(): boolean {
62+
return isPresent(this.cd.control) ? this.cd.control.untouched : false;
63+
}
64+
get ngClassTouched(): boolean {
65+
return isPresent(this.cd.control) ? this.cd.control.touched : false;
66+
}
67+
get ngClassPristine(): boolean {
68+
return isPresent(this.cd.control) ? this.cd.control.pristine : false;
69+
}
70+
get ngClassDirty(): boolean { return isPresent(this.cd.control) ? this.cd.control.dirty : false; }
71+
get ngClassValid(): boolean { return isPresent(this.cd.control) ? this.cd.control.valid : false; }
72+
get ngClassInvalid(): boolean {
73+
return isPresent(this.cd.control) ? !this.cd.control.valid : false;
74+
}
75+
6076
registerOnChange(fn): void { this.onChange = fn; }
6177
registerOnTouched(fn): void { this.onTouched = fn; }
6278

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,8 +168,8 @@ export class DirectiveParser implements CompileStep {
168168
}
169169

170170
_bindHostProperty(hostPropertyName, expression, compileElement, directiveBinderBuilder) {
171-
var ast = this._parser.parseBinding(expression,
172-
`hostProperties of ${compileElement.elementDescription}`);
171+
var ast = this._parser.parseSimpleBinding(
172+
expression, `hostProperties of ${compileElement.elementDescription}`);
173173
directiveBinderBuilder.bindHostProperty(hostPropertyName, ast);
174174
}
175175

modules/angular2/test/change_detection/parser/parser_spec.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {Parser} from 'angular2/src/change_detection/parser/parser';
66
import {Unparser} from './unparser';
77
import {Lexer} from 'angular2/src/change_detection/parser/lexer';
88
import {Locals} from 'angular2/src/change_detection/parser/locals';
9-
import {BindingPipe, LiteralPrimitive} from 'angular2/src/change_detection/parser/ast';
9+
import {BindingPipe, LiteralPrimitive, AST} from 'angular2/src/change_detection/parser/ast';
1010

1111
class TestData {
1212
constructor(public a?: any, public b?: any, public fnReturnValue?: any) {}
@@ -39,6 +39,12 @@ export function main() {
3939
return createParser().parseInterpolation(text, location);
4040
}
4141

42+
function parseSimpleBinding(text, location = null): any {
43+
return createParser().parseSimpleBinding(text, location);
44+
}
45+
46+
function unparse(ast: AST): string { return new Unparser().unparse(ast); }
47+
4248
function emptyLocals() { return new Locals(null, new Map()); }
4349

4450
function evalAction(text, passedInContext = null, passedInLocals = null) {
@@ -620,6 +626,24 @@ export function main() {
620626
});
621627
});
622628

629+
describe("parseSimpleBinding", () => {
630+
it("should parse a field access", () => {
631+
var p = parseSimpleBinding("name");
632+
expect(unparse(p)).toEqual("name");
633+
});
634+
635+
it("should parse a constant", () => {
636+
var p = parseSimpleBinding("[1, 2]");
637+
expect(unparse(p)).toEqual("[1, 2]");
638+
});
639+
640+
it("should throw when the given expression is not just a field name", () => {
641+
expect(() => parseSimpleBinding("name + 1"))
642+
.toThrowError(new RegExp(
643+
'Simple binding expression can only contain field access and constants'));
644+
});
645+
});
646+
623647
describe('wrapLiteralPrimitive', () => {
624648
it('should wrap a literal primitive', () => {
625649
expect(createParser().wrapLiteralPrimitive("foo", null).eval(null, emptyLocals()))

modules/angular2/test/forms/integration_spec.ts

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

671671
var input = view.querySelector("input");
672672
expect(DOM.classList(input))
673-
.toEqual(["ng-binding", "ng-untouched", "ng-pristine", "ng-invalid"]);
673+
.toEqual(['ng-binding', 'ng-untouched', 'ng-pristine', 'ng-invalid']);
674674

675675
dispatchEvent(input, "blur");
676676
view.detectChanges();

modules/angular2/test/render/dom/compiler/directive_parser_spec.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export function main() {
2424
decoratorWithMultipleAttrs,
2525
someDirectiveWithProps,
2626
someDirectiveWithHostProperties,
27+
someDirectiveWithInvalidHostProperties,
2728
someDirectiveWithHostAttributes,
2829
someDirectiveWithEvents,
2930
someDirectiveWithGlobalEvents,
@@ -103,6 +104,12 @@ export function main() {
103104
expect(ast.source).toEqual('dirProp');
104105
});
105106

107+
it('should throw when parsing invalid host properties', () => {
108+
expect(() => process(el('<input some-decor-with-invalid-host-props>')))
109+
.toThrowError(
110+
new RegExp('Simple binding expression can only contain field access and constants'));
111+
});
112+
106113
it('should set host element attributes', () => {
107114
var element = el('<input some-decor-with-host-attrs>');
108115
var results = process(element);
@@ -235,6 +242,11 @@ var someDirectiveWithHostProperties = DirectiveMetadata.create({
235242
host: MapWrapper.createFromStringMap({'[hostProp]': 'dirProp'})
236243
});
237244

245+
var someDirectiveWithInvalidHostProperties = DirectiveMetadata.create({
246+
selector: '[some-decor-with-invalid-host-props]',
247+
host: MapWrapper.createFromStringMap({'[hostProp]': 'dirProp + dirProp2'})
248+
});
249+
238250
var someDirectiveWithHostAttributes = DirectiveMetadata.create({
239251
selector: '[some-decor-with-host-attrs]',
240252
host: MapWrapper.createFromStringMap({'attr_name': 'attr_val', 'class': 'foo bar'})

0 commit comments

Comments
 (0)