From 92ffc465d61c94aa3262b748d7d2b38a26583b6e Mon Sep 17 00:00:00 2001 From: vsavkin Date: Mon, 22 Jun 2015 08:21:03 -0700 Subject: [PATCH] feat(host): limits host properties to renames --- .../src/change_detection/parser/parser.ts | 71 ++++++++++++++++++- .../directives/checkbox_value_accessor.ts | 28 ++++++-- .../directives/default_value_accessor.ts | 30 ++++++-- .../select_control_value_accessor.ts | 28 ++++++-- .../render/dom/compiler/directive_parser.ts | 4 +- .../change_detection/parser/parser_spec.ts | 26 ++++++- .../angular2/test/forms/integration_spec.ts | 2 +- .../dom/compiler/directive_parser_spec.ts | 12 ++++ 8 files changed, 177 insertions(+), 24 deletions(-) diff --git a/modules/angular2/src/change_detection/parser/parser.ts b/modules/angular2/src/change_detection/parser/parser.ts index 70480a5557..7fbe05adcc 100644 --- a/modules/angular2/src/change_detection/parser/parser.ts +++ b/modules/angular2/src/change_detection/parser/parser.ts @@ -45,7 +45,8 @@ import { SafeMethodCall, FunctionCall, TemplateBinding, - ASTWithSource + ASTWithSource, + AstVisitor } from './ast'; @@ -73,6 +74,12 @@ export class Parser { return new ASTWithSource(ast, input, location); } + parseSimpleBinding(input: string, location: string): ASTWithSource { + var tokens = this._lexer.tokenize(input); + var ast = new _ParseAST(input, location, tokens, this._reflector, false).parseSimpleBinding(); + return new ASTWithSource(ast, input, location); + } + parseTemplateBindings(input: string, location: any): List { var tokens = this._lexer.tokenize(input); return new _ParseAST(input, location, tokens, this._reflector, false).parseTemplateBindings(); @@ -202,6 +209,14 @@ class _ParseAST { return new Chain(exprs); } + parseSimpleBinding(): AST { + var ast = this.parseChain(); + if (!SimpleExpressionChecker.check(ast)) { + this.error(`Simple binding expression can only contain field access and constants'`); + } + return ast; + } + parsePipe() { var result = this.parseExpression(); if (this.optionalOperator("|")) { @@ -590,3 +605,57 @@ class _ParseAST { `Parser Error: ${message} ${location} [${this.input}] in ${this.location}`); } } + +class SimpleExpressionChecker implements AstVisitor { + static check(ast: AST) { + var s = new SimpleExpressionChecker(); + ast.visit(s); + return s.simple; + } + + simple = true; + + visitImplicitReceiver(ast: ImplicitReceiver) {} + + visitInterpolation(ast: Interpolation) { this.simple = false; } + + visitLiteralPrimitive(ast: LiteralPrimitive) {} + + visitAccessMember(ast: AccessMember) {} + + visitSafeAccessMember(ast: SafeAccessMember) { this.simple = false; } + + visitMethodCall(ast: MethodCall) { this.simple = false; } + + visitSafeMethodCall(ast: SafeMethodCall) { this.simple = false; } + + visitFunctionCall(ast: FunctionCall) { this.simple = false; } + + visitLiteralArray(ast: LiteralArray) { this.visitAll(ast.expressions); } + + visitLiteralMap(ast: LiteralMap) { this.visitAll(ast.values); } + + visitBinary(ast: Binary) { this.simple = false; } + + visitPrefixNot(ast: PrefixNot) { this.simple = false; } + + visitConditional(ast: Conditional) { this.simple = false; } + + visitPipe(ast: BindingPipe) { this.simple = false; } + + visitKeyedAccess(ast: KeyedAccess) { this.simple = false; } + + visitAll(asts: List) { + var res = ListWrapper.createFixedSize(asts.length); + for (var i = 0; i < asts.length; ++i) { + res[i] = asts[i].visit(this); + } + return res; + } + + visitChain(ast: Chain) { this.simple = false; } + + visitAssignment(ast: Assignment) { this.simple = false; } + + visitIf(ast: If) { this.simple = false; } +} diff --git a/modules/angular2/src/forms/directives/checkbox_value_accessor.ts b/modules/angular2/src/forms/directives/checkbox_value_accessor.ts index 624ae6f596..871730bb9e 100644 --- a/modules/angular2/src/forms/directives/checkbox_value_accessor.ts +++ b/modules/angular2/src/forms/directives/checkbox_value_accessor.ts @@ -1,6 +1,7 @@ import {Directive, Renderer, ElementRef} from 'angular2/angular2'; import {NgControl} from './ng_control'; import {ControlValueAccessor} from './control_value_accessor'; +import {isPresent} from 'angular2/src/facade/lang'; import {setProperty} from './shared'; /** @@ -20,12 +21,12 @@ import {setProperty} from './shared'; '(change)': 'onChange($event.target.checked)', '(blur)': 'onTouched()', '[checked]': 'checked', - '[class.ng-untouched]': 'cd.control?.untouched == true', - '[class.ng-touched]': 'cd.control?.touched == true', - '[class.ng-pristine]': 'cd.control?.pristine == true', - '[class.ng-dirty]': 'cd.control?.dirty == true', - '[class.ng-valid]': 'cd.control?.valid == true', - '[class.ng-invalid]': 'cd.control?.valid == false' + '[class.ng-untouched]': 'ngClassUntouched', + '[class.ng-touched]': 'ngClassTouched', + '[class.ng-pristine]': 'ngClassPristine', + '[class.ng-dirty]': 'ngClassDirty', + '[class.ng-valid]': 'ngClassValid', + '[class.ng-invalid]': 'ngClassInvalid' } }) export class CheckboxControlValueAccessor implements ControlValueAccessor { @@ -44,6 +45,21 @@ export class CheckboxControlValueAccessor implements ControlValueAccessor { setProperty(this.renderer, this.elementRef, "checked", value); } + get ngClassUntouched(): boolean { + return isPresent(this.cd.control) ? this.cd.control.untouched : false; + } + get ngClassTouched(): boolean { + return isPresent(this.cd.control) ? this.cd.control.touched : false; + } + get ngClassPristine(): boolean { + return isPresent(this.cd.control) ? this.cd.control.pristine : false; + } + get ngClassDirty(): boolean { return isPresent(this.cd.control) ? this.cd.control.dirty : false; } + get ngClassValid(): boolean { return isPresent(this.cd.control) ? this.cd.control.valid : false; } + get ngClassInvalid(): boolean { + return isPresent(this.cd.control) ? !this.cd.control.valid : false; + } + registerOnChange(fn): void { this.onChange = fn; } registerOnTouched(fn): void { this.onTouched = fn; } } diff --git a/modules/angular2/src/forms/directives/default_value_accessor.ts b/modules/angular2/src/forms/directives/default_value_accessor.ts index 04483805b3..f783d59ee5 100644 --- a/modules/angular2/src/forms/directives/default_value_accessor.ts +++ b/modules/angular2/src/forms/directives/default_value_accessor.ts @@ -1,7 +1,7 @@ import {Directive, Renderer, ElementRef} from 'angular2/angular2'; import {NgControl} from './ng_control'; import {ControlValueAccessor} from './control_value_accessor'; -import {isBlank} from 'angular2/src/facade/lang'; +import {isBlank, isPresent} from 'angular2/src/facade/lang'; import {setProperty} from './shared'; /** @@ -23,16 +23,17 @@ import {setProperty} from './shared'; '(input)': 'onChange($event.target.value)', '(blur)': 'onTouched()', '[value]': 'value', - '[class.ng-untouched]': 'cd.control?.untouched == true', - '[class.ng-touched]': 'cd.control?.touched == true', - '[class.ng-pristine]': 'cd.control?.pristine == true', - '[class.ng-dirty]': 'cd.control?.dirty == true', - '[class.ng-valid]': 'cd.control?.valid == true', - '[class.ng-invalid]': 'cd.control?.valid == false' + '[class.ng-untouched]': 'ngClassUntouched', + '[class.ng-touched]': 'ngClassTouched', + '[class.ng-pristine]': 'ngClassPristine', + '[class.ng-dirty]': 'ngClassDirty', + '[class.ng-valid]': 'ngClassValid', + '[class.ng-invalid]': 'ngClassInvalid' } }) export class DefaultValueAccessor implements ControlValueAccessor { value: string = null; + onChange = (_) => {}; onTouched = () => {}; @@ -47,6 +48,21 @@ export class DefaultValueAccessor implements ControlValueAccessor { setProperty(this.renderer, this.elementRef, 'value', this.value); } + get ngClassUntouched(): boolean { + return isPresent(this.cd.control) ? this.cd.control.untouched : false; + } + get ngClassTouched(): boolean { + return isPresent(this.cd.control) ? this.cd.control.touched : false; + } + get ngClassPristine(): boolean { + return isPresent(this.cd.control) ? this.cd.control.pristine : false; + } + get ngClassDirty(): boolean { return isPresent(this.cd.control) ? this.cd.control.dirty : false; } + get ngClassValid(): boolean { return isPresent(this.cd.control) ? this.cd.control.valid : false; } + get ngClassInvalid(): boolean { + return isPresent(this.cd.control) ? !this.cd.control.valid : false; + } + registerOnChange(fn): void { this.onChange = fn; } registerOnTouched(fn): void { this.onTouched = fn; } diff --git a/modules/angular2/src/forms/directives/select_control_value_accessor.ts b/modules/angular2/src/forms/directives/select_control_value_accessor.ts index a8e1072cc2..b7f5568cd4 100644 --- a/modules/angular2/src/forms/directives/select_control_value_accessor.ts +++ b/modules/angular2/src/forms/directives/select_control_value_accessor.ts @@ -1,6 +1,7 @@ import {Directive, Query, QueryList, Renderer, ElementRef} from 'angular2/angular2'; import {NgControl} from './ng_control'; import {ControlValueAccessor} from './control_value_accessor'; +import {isPresent} from 'angular2/src/facade/lang'; import {setProperty} from './shared'; /** @@ -30,12 +31,12 @@ export class NgSelectOption { '(input)': 'onChange($event.target.value)', '(blur)': 'onTouched()', '[value]': 'value', - '[class.ng-untouched]': 'cd.control?.untouched == true', - '[class.ng-touched]': 'cd.control?.touched == true', - '[class.ng-pristine]': 'cd.control?.pristine == true', - '[class.ng-dirty]': 'cd.control?.dirty == true', - '[class.ng-valid]': 'cd.control?.valid == true', - '[class.ng-invalid]': 'cd.control?.valid == false' + '[class.ng-untouched]': 'ngClassUntouched', + '[class.ng-touched]': 'ngClassTouched', + '[class.ng-pristine]': 'ngClassPristine', + '[class.ng-dirty]': 'ngClassDirty', + '[class.ng-valid]': 'ngClassValid', + '[class.ng-invalid]': 'ngClassInvalid' } }) export class SelectControlValueAccessor implements ControlValueAccessor { @@ -57,6 +58,21 @@ export class SelectControlValueAccessor implements ControlValueAccessor { setProperty(this.renderer, this.elementRef, "value", value); } + get ngClassUntouched(): boolean { + return isPresent(this.cd.control) ? this.cd.control.untouched : false; + } + get ngClassTouched(): boolean { + return isPresent(this.cd.control) ? this.cd.control.touched : false; + } + get ngClassPristine(): boolean { + return isPresent(this.cd.control) ? this.cd.control.pristine : false; + } + get ngClassDirty(): boolean { return isPresent(this.cd.control) ? this.cd.control.dirty : false; } + get ngClassValid(): boolean { return isPresent(this.cd.control) ? this.cd.control.valid : false; } + get ngClassInvalid(): boolean { + return isPresent(this.cd.control) ? !this.cd.control.valid : false; + } + registerOnChange(fn): void { this.onChange = fn; } registerOnTouched(fn): void { this.onTouched = fn; } diff --git a/modules/angular2/src/render/dom/compiler/directive_parser.ts b/modules/angular2/src/render/dom/compiler/directive_parser.ts index 08acb733b4..89faf4d4ca 100644 --- a/modules/angular2/src/render/dom/compiler/directive_parser.ts +++ b/modules/angular2/src/render/dom/compiler/directive_parser.ts @@ -168,8 +168,8 @@ export class DirectiveParser implements CompileStep { } _bindHostProperty(hostPropertyName, expression, compileElement, directiveBinderBuilder) { - var ast = this._parser.parseBinding(expression, - `hostProperties of ${compileElement.elementDescription}`); + var ast = this._parser.parseSimpleBinding( + expression, `hostProperties of ${compileElement.elementDescription}`); directiveBinderBuilder.bindHostProperty(hostPropertyName, ast); } diff --git a/modules/angular2/test/change_detection/parser/parser_spec.ts b/modules/angular2/test/change_detection/parser/parser_spec.ts index d00a651e55..13012d64fe 100644 --- a/modules/angular2/test/change_detection/parser/parser_spec.ts +++ b/modules/angular2/test/change_detection/parser/parser_spec.ts @@ -6,7 +6,7 @@ import {Parser} from 'angular2/src/change_detection/parser/parser'; import {Unparser} from './unparser'; import {Lexer} from 'angular2/src/change_detection/parser/lexer'; import {Locals} from 'angular2/src/change_detection/parser/locals'; -import {BindingPipe, LiteralPrimitive} from 'angular2/src/change_detection/parser/ast'; +import {BindingPipe, LiteralPrimitive, AST} from 'angular2/src/change_detection/parser/ast'; class TestData { constructor(public a?: any, public b?: any, public fnReturnValue?: any) {} @@ -39,6 +39,12 @@ export function main() { return createParser().parseInterpolation(text, location); } + function parseSimpleBinding(text, location = null): any { + return createParser().parseSimpleBinding(text, location); + } + + function unparse(ast: AST): string { return new Unparser().unparse(ast); } + function emptyLocals() { return new Locals(null, new Map()); } function evalAction(text, passedInContext = null, passedInLocals = null) { @@ -620,6 +626,24 @@ export function main() { }); }); + describe("parseSimpleBinding", () => { + it("should parse a field access", () => { + var p = parseSimpleBinding("name"); + expect(unparse(p)).toEqual("name"); + }); + + it("should parse a constant", () => { + var p = parseSimpleBinding("[1, 2]"); + expect(unparse(p)).toEqual("[1, 2]"); + }); + + it("should throw when the given expression is not just a field name", () => { + expect(() => parseSimpleBinding("name + 1")) + .toThrowError(new RegExp( + 'Simple binding expression can only contain field access and constants')); + }); + }); + describe('wrapLiteralPrimitive', () => { it('should wrap a literal primitive', () => { expect(createParser().wrapLiteralPrimitive("foo", null).eval(null, emptyLocals())) diff --git a/modules/angular2/test/forms/integration_spec.ts b/modules/angular2/test/forms/integration_spec.ts index a2a7f950eb..34ca332441 100644 --- a/modules/angular2/test/forms/integration_spec.ts +++ b/modules/angular2/test/forms/integration_spec.ts @@ -670,7 +670,7 @@ export function main() { var input = view.querySelector("input"); expect(DOM.classList(input)) - .toEqual(["ng-binding", "ng-untouched", "ng-pristine", "ng-invalid"]); + .toEqual(['ng-binding', 'ng-untouched', 'ng-pristine', 'ng-invalid']); dispatchEvent(input, "blur"); view.detectChanges(); diff --git a/modules/angular2/test/render/dom/compiler/directive_parser_spec.ts b/modules/angular2/test/render/dom/compiler/directive_parser_spec.ts index 8e87972dcd..8b877d1a0b 100644 --- a/modules/angular2/test/render/dom/compiler/directive_parser_spec.ts +++ b/modules/angular2/test/render/dom/compiler/directive_parser_spec.ts @@ -24,6 +24,7 @@ export function main() { decoratorWithMultipleAttrs, someDirectiveWithProps, someDirectiveWithHostProperties, + someDirectiveWithInvalidHostProperties, someDirectiveWithHostAttributes, someDirectiveWithEvents, someDirectiveWithGlobalEvents, @@ -103,6 +104,12 @@ export function main() { expect(ast.source).toEqual('dirProp'); }); + it('should throw when parsing invalid host properties', () => { + expect(() => process(el(''))) + .toThrowError( + new RegExp('Simple binding expression can only contain field access and constants')); + }); + it('should set host element attributes', () => { var element = el(''); var results = process(element); @@ -235,6 +242,11 @@ var someDirectiveWithHostProperties = DirectiveMetadata.create({ host: MapWrapper.createFromStringMap({'[hostProp]': 'dirProp'}) }); +var someDirectiveWithInvalidHostProperties = DirectiveMetadata.create({ + selector: '[some-decor-with-invalid-host-props]', + host: MapWrapper.createFromStringMap({'[hostProp]': 'dirProp + dirProp2'}) +}); + var someDirectiveWithHostAttributes = DirectiveMetadata.create({ selector: '[some-decor-with-host-attrs]', host: MapWrapper.createFromStringMap({'attr_name': 'attr_val', 'class': 'foo bar'})