feat(host): limits host properties to renames
This commit is contained in:
parent
c1a494bc37
commit
92ffc465d6
@ -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; }
|
||||||
|
}
|
||||||
|
@ -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; }
|
||||||
}
|
}
|
||||||
|
@ -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; }
|
||||||
|
@ -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; }
|
||||||
|
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()))
|
||||||
|
@ -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();
|
||||||
|
@ -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'})
|
||||||
|
Loading…
x
Reference in New Issue
Block a user