feat(host): limits host properties to renames

This commit is contained in:
vsavkin 2015-06-22 08:21:03 -07:00
parent c1a494bc37
commit 92ffc465d6
8 changed files with 177 additions and 24 deletions

View File

@ -45,7 +45,8 @@ import {
SafeMethodCall, SafeMethodCall,
FunctionCall, FunctionCall,
TemplateBinding, TemplateBinding,
ASTWithSource ASTWithSource,
AstVisitor
} from './ast'; } from './ast';
@ -73,6 +74,12 @@ export class Parser {
return new ASTWithSource(ast, input, location); 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<TemplateBinding> { parseTemplateBindings(input: string, location: any): List<TemplateBinding> {
var tokens = this._lexer.tokenize(input); var tokens = this._lexer.tokenize(input);
return new _ParseAST(input, location, tokens, this._reflector, false).parseTemplateBindings(); return new _ParseAST(input, location, tokens, this._reflector, false).parseTemplateBindings();
@ -202,6 +209,14 @@ class _ParseAST {
return new Chain(exprs); 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() { parsePipe() {
var result = this.parseExpression(); var result = this.parseExpression();
if (this.optionalOperator("|")) { if (this.optionalOperator("|")) {
@ -590,3 +605,57 @@ class _ParseAST {
`Parser Error: ${message} ${location} [${this.input}] in ${this.location}`); `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<any>) {
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; }
}

View File

@ -1,6 +1,7 @@
import {Directive, Renderer, ElementRef} from 'angular2/angular2'; import {Directive, Renderer, ElementRef} from 'angular2/angular2';
import {NgControl} from './ng_control'; import {NgControl} from './ng_control';
import {ControlValueAccessor} from './control_value_accessor'; import {ControlValueAccessor} from './control_value_accessor';
import {isPresent} from 'angular2/src/facade/lang';
import {setProperty} from './shared'; import {setProperty} from './shared';
/** /**
@ -20,12 +21,12 @@ import {setProperty} from './shared';
'(change)': 'onChange($event.target.checked)', '(change)': 'onChange($event.target.checked)',
'(blur)': 'onTouched()', '(blur)': 'onTouched()',
'[checked]': 'checked', '[checked]': 'checked',
'[class.ng-untouched]': 'cd.control?.untouched == true', '[class.ng-untouched]': 'ngClassUntouched',
'[class.ng-touched]': 'cd.control?.touched == true', '[class.ng-touched]': 'ngClassTouched',
'[class.ng-pristine]': 'cd.control?.pristine == true', '[class.ng-pristine]': 'ngClassPristine',
'[class.ng-dirty]': 'cd.control?.dirty == true', '[class.ng-dirty]': 'ngClassDirty',
'[class.ng-valid]': 'cd.control?.valid == true', '[class.ng-valid]': 'ngClassValid',
'[class.ng-invalid]': 'cd.control?.valid == false' '[class.ng-invalid]': 'ngClassInvalid'
} }
}) })
export class CheckboxControlValueAccessor implements ControlValueAccessor { export class CheckboxControlValueAccessor implements ControlValueAccessor {
@ -44,6 +45,21 @@ export class CheckboxControlValueAccessor implements ControlValueAccessor {
setProperty(this.renderer, this.elementRef, "checked", value); 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; } registerOnChange(fn): void { this.onChange = fn; }
registerOnTouched(fn): void { this.onTouched = fn; } registerOnTouched(fn): void { this.onTouched = fn; }
} }

View File

@ -1,7 +1,7 @@
import {Directive, Renderer, ElementRef} from 'angular2/angular2'; import {Directive, Renderer, ElementRef} from 'angular2/angular2';
import {NgControl} from './ng_control'; import {NgControl} from './ng_control';
import {ControlValueAccessor} from './control_value_accessor'; 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'; import {setProperty} from './shared';
/** /**
@ -23,16 +23,17 @@ import {setProperty} from './shared';
'(input)': 'onChange($event.target.value)', '(input)': 'onChange($event.target.value)',
'(blur)': 'onTouched()', '(blur)': 'onTouched()',
'[value]': 'value', '[value]': 'value',
'[class.ng-untouched]': 'cd.control?.untouched == true', '[class.ng-untouched]': 'ngClassUntouched',
'[class.ng-touched]': 'cd.control?.touched == true', '[class.ng-touched]': 'ngClassTouched',
'[class.ng-pristine]': 'cd.control?.pristine == true', '[class.ng-pristine]': 'ngClassPristine',
'[class.ng-dirty]': 'cd.control?.dirty == true', '[class.ng-dirty]': 'ngClassDirty',
'[class.ng-valid]': 'cd.control?.valid == true', '[class.ng-valid]': 'ngClassValid',
'[class.ng-invalid]': 'cd.control?.valid == false' '[class.ng-invalid]': 'ngClassInvalid'
} }
}) })
export class DefaultValueAccessor implements ControlValueAccessor { export class DefaultValueAccessor implements ControlValueAccessor {
value: string = null; value: string = null;
onChange = (_) => {}; onChange = (_) => {};
onTouched = () => {}; onTouched = () => {};
@ -47,6 +48,21 @@ export class DefaultValueAccessor implements ControlValueAccessor {
setProperty(this.renderer, this.elementRef, 'value', this.value); 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; } registerOnChange(fn): void { this.onChange = fn; }
registerOnTouched(fn): void { this.onTouched = fn; } registerOnTouched(fn): void { this.onTouched = fn; }

View File

@ -1,6 +1,7 @@
import {Directive, Query, QueryList, Renderer, ElementRef} from 'angular2/angular2'; import {Directive, Query, QueryList, Renderer, ElementRef} from 'angular2/angular2';
import {NgControl} from './ng_control'; import {NgControl} from './ng_control';
import {ControlValueAccessor} from './control_value_accessor'; import {ControlValueAccessor} from './control_value_accessor';
import {isPresent} from 'angular2/src/facade/lang';
import {setProperty} from './shared'; import {setProperty} from './shared';
/** /**
@ -30,12 +31,12 @@ export class NgSelectOption {
'(input)': 'onChange($event.target.value)', '(input)': 'onChange($event.target.value)',
'(blur)': 'onTouched()', '(blur)': 'onTouched()',
'[value]': 'value', '[value]': 'value',
'[class.ng-untouched]': 'cd.control?.untouched == true', '[class.ng-untouched]': 'ngClassUntouched',
'[class.ng-touched]': 'cd.control?.touched == true', '[class.ng-touched]': 'ngClassTouched',
'[class.ng-pristine]': 'cd.control?.pristine == true', '[class.ng-pristine]': 'ngClassPristine',
'[class.ng-dirty]': 'cd.control?.dirty == true', '[class.ng-dirty]': 'ngClassDirty',
'[class.ng-valid]': 'cd.control?.valid == true', '[class.ng-valid]': 'ngClassValid',
'[class.ng-invalid]': 'cd.control?.valid == false' '[class.ng-invalid]': 'ngClassInvalid'
} }
}) })
export class SelectControlValueAccessor implements ControlValueAccessor { export class SelectControlValueAccessor implements ControlValueAccessor {
@ -57,6 +58,21 @@ export class SelectControlValueAccessor implements ControlValueAccessor {
setProperty(this.renderer, this.elementRef, "value", value); 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; } registerOnChange(fn): void { this.onChange = fn; }
registerOnTouched(fn): void { this.onTouched = fn; } registerOnTouched(fn): void { this.onTouched = fn; }

View File

@ -168,8 +168,8 @@ export class DirectiveParser implements CompileStep {
} }
_bindHostProperty(hostPropertyName, expression, compileElement, directiveBinderBuilder) { _bindHostProperty(hostPropertyName, expression, compileElement, directiveBinderBuilder) {
var ast = this._parser.parseBinding(expression, var ast = this._parser.parseSimpleBinding(
`hostProperties of ${compileElement.elementDescription}`); expression, `hostProperties of ${compileElement.elementDescription}`);
directiveBinderBuilder.bindHostProperty(hostPropertyName, ast); directiveBinderBuilder.bindHostProperty(hostPropertyName, ast);
} }

View File

@ -6,7 +6,7 @@ import {Parser} from 'angular2/src/change_detection/parser/parser';
import {Unparser} from './unparser'; import {Unparser} from './unparser';
import {Lexer} from 'angular2/src/change_detection/parser/lexer'; import {Lexer} from 'angular2/src/change_detection/parser/lexer';
import {Locals} from 'angular2/src/change_detection/parser/locals'; 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 { class TestData {
constructor(public a?: any, public b?: any, public fnReturnValue?: any) {} constructor(public a?: any, public b?: any, public fnReturnValue?: any) {}
@ -39,6 +39,12 @@ export function main() {
return createParser().parseInterpolation(text, location); 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 emptyLocals() { return new Locals(null, new Map()); }
function evalAction(text, passedInContext = null, passedInLocals = null) { 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', () => { describe('wrapLiteralPrimitive', () => {
it('should wrap a literal primitive', () => { it('should wrap a literal primitive', () => {
expect(createParser().wrapLiteralPrimitive("foo", null).eval(null, emptyLocals())) expect(createParser().wrapLiteralPrimitive("foo", null).eval(null, emptyLocals()))

View File

@ -670,7 +670,7 @@ export function main() {
var input = view.querySelector("input"); var input = view.querySelector("input");
expect(DOM.classList(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"); dispatchEvent(input, "blur");
view.detectChanges(); view.detectChanges();

View File

@ -24,6 +24,7 @@ export function main() {
decoratorWithMultipleAttrs, decoratorWithMultipleAttrs,
someDirectiveWithProps, someDirectiveWithProps,
someDirectiveWithHostProperties, someDirectiveWithHostProperties,
someDirectiveWithInvalidHostProperties,
someDirectiveWithHostAttributes, someDirectiveWithHostAttributes,
someDirectiveWithEvents, someDirectiveWithEvents,
someDirectiveWithGlobalEvents, someDirectiveWithGlobalEvents,
@ -103,6 +104,12 @@ export function main() {
expect(ast.source).toEqual('dirProp'); expect(ast.source).toEqual('dirProp');
}); });
it('should throw when parsing invalid host properties', () => {
expect(() => process(el('<input some-decor-with-invalid-host-props>')))
.toThrowError(
new RegExp('Simple binding expression can only contain field access and constants'));
});
it('should set host element attributes', () => { it('should set host element attributes', () => {
var element = el('<input some-decor-with-host-attrs>'); var element = el('<input some-decor-with-host-attrs>');
var results = process(element); var results = process(element);
@ -235,6 +242,11 @@ var someDirectiveWithHostProperties = DirectiveMetadata.create({
host: MapWrapper.createFromStringMap({'[hostProp]': 'dirProp'}) 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({ var someDirectiveWithHostAttributes = DirectiveMetadata.create({
selector: '[some-decor-with-host-attrs]', selector: '[some-decor-with-host-attrs]',
host: MapWrapper.createFromStringMap({'attr_name': 'attr_val', 'class': 'foo bar'}) host: MapWrapper.createFromStringMap({'attr_name': 'attr_val', 'class': 'foo bar'})