feat(forms): initial implementation of forms declared in html

This commit is contained in:
vsavkin 2015-02-04 11:45:33 -08:00
parent fa7cbf9bb1
commit 640134dee1
2 changed files with 183 additions and 41 deletions

View File

@ -1,48 +1,88 @@
import {Decorator, NgElement, Ancestor} from 'angular2/core'; import {TemplateConfig, Component, Decorator, NgElement, Ancestor} from 'angular2/core';
import {DOM} from 'angular2/src/facade/dom'; import {DOM} from 'angular2/src/facade/dom';
import {isPresent} from 'angular2/src/facade/lang'; import {isBlank, isPresent} from 'angular2/src/facade/lang';
import {ListWrapper} from 'angular2/src/facade/collection'; import {ListWrapper} from 'angular2/src/facade/collection';
import {ControlGroup, Control} from './model'; import {ControlGroup, Control} from './model';
export class ControlDirectiveBase {
_groupDecorator:ControlGroupDirectiveBase;
_el:NgElement;
_controlName:string;
constructor(groupDecorator, el:NgElement) {
this._groupDecorator = groupDecorator;
this._el = el;
DOM.on(el.domElement, "change", (_) => this._updateControl());
}
set controlName(name:string) {
this._controlName = name;
this._groupDecorator.addDirective(this);
this._updateDOM();
}
get controlName() {
return this._controlName;
}
//TODO:vsavkin: Remove it once change detection lifecycle callbacks are available
isInitialized():boolean {
return isPresent(this._controlName);
}
_updateDOM() {
// remove it once all DOM write go through a queue
if (this.isInitialized()) {
var inputElement:any = this._el.domElement;
inputElement.value = this._control().value;
}
}
_updateControl() {
var inputElement:any = this._el.domElement;
this._control().value = inputElement.value;
}
_control() {
return this._groupDecorator.findControl(this._controlName);
}
}
class ControlGroupDirectiveBase {
addDirective(c:ControlNameDirective):void {}
findControl(name:string):Control {}
}
@Decorator({ @Decorator({
selector: '[control-name]', selector: '[control-name]',
bind: { bind: {
'control-name' : 'controlName' 'control-name' : 'controlName'
} }
}) })
export class ControlDecorator { export class ControlNameDirective extends ControlDirectiveBase {
_groupDecorator:ControlGroupDecorator; _groupDecorator:ControlGroupDirective;
_el:NgElement; _el:NgElement;
_controlName:String; _controlName:String;
constructor(@Ancestor() groupDecorator:ControlGroupDecorator, el:NgElement) { constructor(@Ancestor() groupDecorator:ControlGroupDirective, el:NgElement) {
this._groupDecorator = groupDecorator; super(groupDecorator, el);
groupDecorator.addControlDecorator(this);
this._el = el;
DOM.on(el.domElement, "change", (_) => this._updateControl());
}
set controlName(name:string) {
this._controlName = name;
this._updateDOM();
}
_updateDOM() {
// remove it once all DOM write go throuh a queue
if (isPresent(this._controlName)) {
var inputElem: any = this._el.domElement;
inputElem.value = this._control().value;
} }
} }
_updateControl() { @Decorator({
var inputElem: any = this._el.domElement; selector: '[control]',
this._control().value = inputElem.value; bind: {
'control' : 'controlName'
} }
})
export class ControlDirective extends ControlDirectiveBase {
_groupDecorator:ControlGroupDirective;
_el:NgElement;
_controlName:String;
_control() { constructor(@Ancestor() groupDecorator:NewControlGroupDirective, el:NgElement) {
return this._groupDecorator.findControl(this._controlName); super(groupDecorator, el);
} }
} }
@ -52,24 +92,74 @@ export class ControlDecorator {
'control-group' : 'controlGroup' 'control-group' : 'controlGroup'
} }
}) })
export class ControlGroupDecorator { export class ControlGroupDirective extends ControlGroupDirectiveBase {
_controlGroup:ControlGroup; _controlGroup:ControlGroup;
_controlDecorators:List<ControlDecorator>; _directives:List<ControlNameDirective>;
constructor() { constructor() {
this._controlDecorators = ListWrapper.create(); this._directives = ListWrapper.create();
} }
set controlGroup(controlGroup:ControlGroup) { set controlGroup(controlGroup:ControlGroup) {
this._controlGroup = controlGroup; this._controlGroup = controlGroup;
ListWrapper.forEach(this._controlDecorators, (cd) => cd._updateDOM()); ListWrapper.forEach(this._directives, (cd) => cd._updateDOM());
} }
addControlDecorator(c:ControlDecorator) { addDirective(c:ControlNameDirective) {
ListWrapper.push(this._controlDecorators, c); ListWrapper.push(this._directives, c);
} }
findControl(name:string):Control { findControl(name:string):Control {
return this._controlGroup.controls[name]; return this._controlGroup.controls[name];
} }
} }
@Component({
selector: '[new-control-group]',
bind: {
'new-control-group' : 'initData'
},
template: new TemplateConfig({
inline: '<content>'
})
})
export class NewControlGroupDirective extends ControlGroupDirectiveBase {
_initData:any;
_controlGroup:ControlGroup;
_directives:List<ControlNameDirective>;
constructor() {
this._directives = ListWrapper.create();
}
set initData(value) {
this._initData = value;
}
addDirective(c:ControlDirective) {
ListWrapper.push(this._directives, c);
this._controlGroup = null;
}
findControl(name:string):Control {
if (isBlank(this._controlGroup)) {
this._controlGroup = this._createControlGroup();
}
return this._controlGroup.controls[name];
}
_createControlGroup():ControlGroup {
var controls = ListWrapper.reduce(this._directives, (memo, cd) => {
if (cd.isInitialized()) {
var initControlValue = this._initData[cd.controlName];
memo[cd.controlName] = new Control(initControlValue);
}
return memo;
}, {});
return new ControlGroup(controls);
}
get value() {
return this._controlGroup.value;
}
}

View File

@ -9,7 +9,11 @@ import {Injector} from 'angular2/di';
import {DOM} from 'angular2/src/facade/dom'; import {DOM} from 'angular2/src/facade/dom';
import {Component, TemplateConfig} from 'angular2/core'; import {Component, TemplateConfig} from 'angular2/core';
import {ControlDecorator, ControlGroupDecorator, Control, ControlGroup} from 'angular2/forms'; import {ControlDirective, ControlNameDirective, ControlGroupDirective, NewControlGroupDirective,
Control, ControlGroup} from 'angular2/forms';
import {TemplateLoader} from 'angular2/src/core/compiler/template_loader';
import {XHRMock} from 'angular2/src/mock/xhr_mock';
export function main() { export function main() {
function detectChanges(view) { function detectChanges(view) {
@ -17,8 +21,12 @@ export function main() {
} }
function compile(componentType, template, context, callback) { function compile(componentType, template, context, callback) {
var compiler = new Compiler(dynamicChangeDetection, null, new DirectiveMetadataReader(), var compiler = new Compiler(dynamicChangeDetection,
new Parser(new Lexer()), new CompilerCache(), new NativeShadowDomStrategy()); new TemplateLoader(new XHRMock()),
new DirectiveMetadataReader(),
new Parser(new Lexer()),
new CompilerCache(),
new NativeShadowDomStrategy());
compiler.compile(componentType, el(template)).then((pv) => { compiler.compile(componentType, el(template)).then((pv) => {
var view = pv.instantiate(null); var view = pv.instantiate(null);
@ -28,6 +36,11 @@ export function main() {
}); });
} }
function formComponent(view) {
// TODO: vsavkin remove when view variables work
return view.elementInjectors[0].getComponent();
}
describe("integration tests", () => { describe("integration tests", () => {
it("should initialize DOM elements with the given form object", (done) => { it("should initialize DOM elements with the given form object", (done) => {
var ctx = new MyComp(new ControlGroup({ var ctx = new MyComp(new ControlGroup({
@ -109,19 +122,58 @@ export function main() {
done(); done();
}); });
}); });
describe("declarative forms", () => {
it("should initialize dom elements", (done) => {
var t = `<div [new-control-group]="{'login': 'loginValue', 'password':'passValue'}">
<input id="login" [control]="'login'">
<input id="password" [control]="'password'">
</div>`;
compile(MyComp, t, new MyComp(), (view) => {
var loginInput = queryView(view, "#login")
expect(loginInput.value).toEqual("loginValue");
var passInput = queryView(view, "#password")
expect(passInput.value).toEqual("passValue");
done();
});
});
it("should update the control group values on DOM change", (done) => {
var t = `<div [new-control-group]="{'login': 'loginValue'}">
<input [control]="'login'">
</div>`;
compile(MyComp, t, new MyComp(), (view) => {
var input = queryView(view, "input")
input.value = "updatedValue";
dispatchEvent(input, "change");
expect(formComponent(view).value).toEqual({'login': 'updatedValue'});
done();
});
});
});
}); });
} }
@Component({ @Component({
selector: "my-comp",
template: new TemplateConfig({ template: new TemplateConfig({
directives: [ControlGroupDecorator, ControlDecorator] inline: "",
directives: [ControlGroupDirective, ControlNameDirective,
ControlDirective, NewControlGroupDirective]
}) })
}) })
class MyComp { class MyComp {
form:ControlGroup; form:ControlGroup;
name:string; name:string;
constructor(form, name = null) { constructor(form = null, name = null) {
this.form = form; this.form = form;
this.name = name; this.name = name;
} }