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,
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<TemplateBinding> {
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<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 {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; }
}

View File

@ -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; }

View File

@ -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; }

View File

@ -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);
}

View File

@ -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()))

View File

@ -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();

View File

@ -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('<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', () => {
var element = el('<input some-decor-with-host-attrs>');
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'})