feat(forms): added ng-model

This commit is contained in:
vsavkin 2015-05-31 12:24:34 -07:00
parent 17e1d7f117
commit 559f54e92b
15 changed files with 338 additions and 29 deletions

View File

@ -1,6 +1,7 @@
import {Type, CONST_EXPR} from 'angular2/src/facade/lang';
import {ControlNameDirective} from './directives/control_name_directive';
import {FormControlDirective} from './directives/form_control_directive';
import {NgModelDirective} from './directives/ng_model_directive';
import {ControlGroupDirective} from './directives/control_group_directive';
import {FormModelDirective} from './directives/form_model_directive';
import {TemplateDrivenFormDirective} from './directives/template_driven_form_directive';
@ -10,6 +11,7 @@ import {SelectControlValueAccessor} from './directives/select_control_value_acce
export {ControlNameDirective} from './directives/control_name_directive';
export {FormControlDirective} from './directives/form_control_directive';
export {NgModelDirective} from './directives/ng_model_directive';
export {ControlDirective} from './directives/control_directive';
export {ControlGroupDirective} from './directives/control_group_directive';
export {FormModelDirective} from './directives/form_model_directive';
@ -32,6 +34,7 @@ export const formDirectives: List<Type> = CONST_EXPR([
ControlGroupDirective,
FormControlDirective,
NgModelDirective,
FormModelDirective,
TemplateDrivenFormDirective,

View File

@ -15,7 +15,8 @@ import {ControlValueAccessor} from './control_value_accessor';
* @exportedAs angular2/forms
*/
@Directive({
selector: 'input[type=checkbox][control],input[type=checkbox][form-control]',
selector:
'input[type=checkbox][control],input[type=checkbox][form-control],input[type=checkbox][ng-model]',
hostListeners: {'change': 'onChange($event.target.checked)'},
hostProperties: {'checked': 'checked'}
})

View File

@ -1,6 +1,11 @@
import {FormDirective} from './form_directive';
import {List} from 'angular2/src/facade/collection';
/**
* A directive that contains a group of [ControlDirective].
*
* @exportedAs angular2/forms
*/
export class ControlContainerDirective {
name: string;
get formDirective(): FormDirective { return null; }

View File

@ -1,6 +1,11 @@
import {ControlValueAccessor} from './control_value_accessor';
import {Validators} from '../validators';
/**
* A directive that bind a [Control] object to a DOM element.
*
* @exportedAs angular2/forms
*/
export class ControlDirective {
name: string = null;
valueAccessor: ControlValueAccessor = null;
@ -8,4 +13,6 @@ export class ControlDirective {
get path(): List<string> { return null; }
constructor() { this.validator = Validators.nullValidator; }
viewToModelUpdate(newValue: any): void {}
}

View File

@ -14,9 +14,8 @@ const controlGroupBinding = CONST_EXPR(
*
* # Example
*
* In this example, we bind the control group to the form element, and we bind the login and
* password controls to the
* login and password elements.
* In this example, we create a control group, and we bind the login and
* password controls to the login and password elements.
*
* Here we use {@link formDirectives}, rather than importing each form directive individually, e.g.
* `ControlDirective`, `ControlGroupDirective`. This is just a shorthand for the same end result.
@ -25,10 +24,13 @@ const controlGroupBinding = CONST_EXPR(
* @Component({selector: "login-comp"})
* @View({
* directives: [formDirectives],
* template: "<form [control-group]='loginForm'>" +
* template:
* "<form [form-model]='loginForm'>" +
* "<div control-group="credentials">
* "Login <input type='text' control='login'>" +
* "Password <input type='password' control='password'>" +
* "<button (click)="onLogin()">Login</button>" +
* "</div>"
* "</form>"
* })
* class LoginComp {
@ -36,8 +38,10 @@ const controlGroupBinding = CONST_EXPR(
*
* constructor() {
* this.loginForm = new ControlGroup({
* login: new Control(""),
* password: new Control("")
* credentials: new ControlGroup({
* login: new Control(""),
* password: new Control("")
* })
* });
* }
*

View File

@ -1,6 +1,7 @@
import {CONST_EXPR} from 'angular2/src/facade/lang';
import {List} from 'angular2/src/facade/collection';
import {Directive, Ancestor, onDestroy, onInit} from 'angular2/angular2';
import {EventEmitter, ObservableWrapper} from 'angular2/src/facade/async';
import {List, StringMapWrapper, StringMap} from 'angular2/src/facade/collection';
import {Directive, Ancestor, onDestroy, onChange} from 'angular2/angular2';
import {FORWARD_REF, Binding, Inject} from 'angular2/di';
import {ControlContainerDirective} from './control_container_directive';
@ -11,11 +12,12 @@ const controlNameBinding =
CONST_EXPR(new Binding(ControlDirective, {toAlias: FORWARD_REF(() => ControlNameDirective)}));
/**
* Binds a control to a DOM element.
* Binds a control with the specified name to a DOM element.
*
* # Example
*
* In this example, we bind the control to an input element. When the value of the input element
* In this example, we bind the login control to an input element. When the value of the input
* element
* changes, the value of
* the control will reflect that change. Likewise, if the value of the control changes, the input
* element reflects that
@ -28,13 +30,23 @@ const controlNameBinding =
* @Component({selector: "login-comp"})
* @View({
* directives: [formDirectives],
* template: "<input type='text' [control]='loginControl'>"
* template:
* "<form [form-model]='loginForm'>" +
* "Login <input type='text' control='login'>" +
* "<button (click)="onLogin()">Login</button>" +
* "</form>"
* })
* class LoginComp {
* loginControl:Control;
* loginForm:ControlGroup;
*
* constructor() {
* this.loginControl = new Control('');
* this.loginForm = new ControlGroup({
* login: new Control(""),
* });
* }
*
* onLogin() {
* // this.loginForm.value
* }
* }
*
@ -45,20 +57,37 @@ const controlNameBinding =
@Directive({
selector: '[control]',
hostInjector: [controlNameBinding],
properties: ['name: control'],
lifecycle: [onDestroy, onInit]
properties: ['name: control', 'model: ng-model'],
events: ['ngModel'],
lifecycle: [onDestroy, onChange]
})
export class ControlNameDirective extends ControlDirective {
_parent: ControlContainerDirective;
ngModel: EventEmitter;
model: any;
_added: boolean;
constructor(@Ancestor() _parent: ControlContainerDirective) {
super();
this._parent = _parent;
this.ngModel = new EventEmitter();
this._added = false;
}
onInit() { this.formDirective.addControl(this); }
onChange(c: StringMap<string, any>) {
if (!this._added) {
this.formDirective.addControl(this);
this._added = true;
}
if (StringMapWrapper.contains(c, "model")) {
this.formDirective.updateModel(this, this.model);
}
}
onDestroy() { this.formDirective.removeControl(this); }
viewToModelUpdate(newValue: any): void { ObservableWrapper.callNext(this.ngModel, newValue); }
get path(): List<string> { return controlPath(this.name, this._parent); }
get formDirective(): any { return this._parent.formDirective; }

View File

@ -11,14 +11,15 @@ import {ControlValueAccessor} from './control_value_accessor';
*
* # Example
* ```
* <input type="text" [control]="loginControl">
* <input type="text" [form-control]="loginControl">
* ```
*
* @exportedAs angular2/forms
*/
@Directive({
selector:
'input:not([type=checkbox])[control],textarea[control],input:not([type=checkbox])[form-control],textarea[form-control]',
selector: 'input:not([type=checkbox])[control],textarea[control],' +
'input:not([type=checkbox])[form-control],textarea[form-control],' +
'input:not([type=checkbox])[ng-model],textarea[ng-model]',
hostListeners:
{'change': 'onChange($event.target.value)', 'input': 'onChange($event.target.value)'},
hostProperties: {'value': 'value'}

View File

@ -1,4 +1,6 @@
import {CONST_EXPR} from 'angular2/src/facade/lang';
import {EventEmitter, ObservableWrapper} from 'angular2/src/facade/async';
import {Directive, Ancestor, onChange} from 'angular2/angular2';
import {FORWARD_REF, Binding} from 'angular2/di';
@ -9,17 +11,63 @@ import {setUpControl} from './shared';
const formControlBinding =
CONST_EXPR(new Binding(ControlDirective, {toAlias: FORWARD_REF(() => FormControlDirective)}));
/**
* Binds a control to a DOM element.
*
* # Example
*
* In this example, we bind the control to an input element. When the value of the input element
* changes, the value of
* the control will reflect that change. Likewise, if the value of the control changes, the input
* element reflects that
* change.
*
* Here we use {@link formDirectives}, rather than importing each form directive individually, e.g.
* `ControlDirective`, `ControlGroupDirective`. This is just a shorthand for the same end result.
*
* ```
* @Component({selector: "login-comp"})
* @View({
* directives: [formDirectives],
* template: "<input type='text' [form-control]='loginControl'>"
* })
* class LoginComp {
* loginControl:Control;
*
* constructor() {
* this.loginControl = new Control('');
* }
* }
*
* ```
*
* @exportedAs angular2/forms
*/
@Directive({
selector: '[form-control]',
hostInjector: [formControlBinding],
properties: ['control: form-control'],
properties: ['control: form-control', 'model: ng-model'],
events: ['ngModel'],
lifecycle: [onChange]
})
export class FormControlDirective extends ControlDirective {
control: Control;
ngModel: EventEmitter;
constructor() {
super();
this.ngModel = new EventEmitter();
}
onChange(_) {
setUpControl(this.control, this);
this.control.updateValidity();
}
set model(value) {
this.control.updateValue(value);
this.valueAccessor.writeValue(value);
}
viewToModelUpdate(newValue: any): void { ObservableWrapper.callNext(this.ngModel, newValue); }
}

View File

@ -6,4 +6,5 @@ export interface FormDirective {
removeControl(dir: ControlDirective): void;
addControlGroup(dir: ControlGroupDirective): void;
removeControlGroup(dir: ControlGroupDirective): void;
updateModel(dir: ControlDirective, value: any): void;
}

View File

@ -6,12 +6,53 @@ import {ControlDirective} from './control_directive';
import {ControlGroupDirective} from './control_group_directive';
import {ControlContainerDirective} from './control_container_directive';
import {FormDirective} from './form_directive';
import {ControlGroup} from '../model';
import {Control, ControlGroup} from '../model';
import {setUpControl} from './shared';
const formDirectiveBinding = CONST_EXPR(
new Binding(ControlContainerDirective, {toAlias: FORWARD_REF(() => FormModelDirective)}));
/**
* Binds a control group to a DOM element.
*
* # Example
*
* In this example, we bind the control group to the form element, and we bind the login and
* password controls to the
* login and password elements.
*
* Here we use {@link formDirectives}, rather than importing each form directive individually, e.g.
* `ControlDirective`, `ControlGroupDirective`. This is just a shorthand for the same end result.
*
* ```
* @Component({selector: "login-comp"})
* @View({
* directives: [formDirectives],
* template: "<form [form-model]='loginForm'>" +
* "Login <input type='text' control='login'>" +
* "Password <input type='password' control='password'>" +
* "<button (click)="onLogin()">Login</button>" +
* "</form>"
* })
* class LoginComp {
* loginForm:ControlGroup;
*
* constructor() {
* this.loginForm = new ControlGroup({
* login: new Control(""),
* password: new Control("")
* });
* }
*
* onLogin() {
* // this.loginForm.value
* }
* }
*
* ```
*
* @exportedAs angular2/forms
*/
@Directive({
selector: '[form-model]',
hostInjector: [formDirectiveBinding],
@ -46,6 +87,12 @@ export class FormModelDirective extends ControlContainerDirective implements For
removeControlGroup(dir: ControlGroupDirective) {}
updateModel(dir: ControlDirective, value: any): void {
var c  = <Control>this.form.find(dir.path);
c.value = value;
dir.valueAccessor.writeValue(value);
}
_updateDomValue() {
ListWrapper.forEach(this.directives, dir => {
var c: any = this.form.find(dir.path);

View File

@ -0,0 +1,51 @@
import {CONST_EXPR} from 'angular2/src/facade/lang';
import {EventEmitter, ObservableWrapper} from 'angular2/src/facade/async';
import {StringMapWrapper} from 'angular2/src/facade/collection';
import {Directive, Ancestor, onChange} from 'angular2/angular2';
import {FORWARD_REF, Binding} from 'angular2/di';
import {ControlDirective} from './control_directive';
import {Control} from '../model';
import {setUpControl} from './shared';
const formControlBinding =
CONST_EXPR(new Binding(ControlDirective, {toAlias: FORWARD_REF(() => NgModelDirective)}));
@Directive({
selector: '[ng-model]:not([control]):not([form-control])',
hostInjector: [formControlBinding],
properties: ['model: ng-model'],
events: ['ngModel'],
lifecycle: [onChange]
})
export class NgModelDirective extends ControlDirective {
control: Control;
ngModel: EventEmitter;
model: any;
_added: boolean;
constructor() {
super();
this.control = new Control("");
this.ngModel = new EventEmitter();
this._added = false;
}
onChange(c) {
if (!this._added) {
setUpControl(this.control, this);
this.control.updateValidity();
this._added = true;
};
if (StringMapWrapper.contains(c, "model")) {
this.control.value = this.model;
this.valueAccessor.writeValue(this.model);
}
}
get path(): List<string> { return []; }
viewToModelUpdate(newValue: any): void { ObservableWrapper.callNext(this.ngModel, newValue); }
}

View File

@ -17,7 +17,7 @@ import {ControlValueAccessor} from './control_value_accessor';
* @exportedAs angular2/forms
*/
@Directive({
selector: 'select[control],select[form-control]',
selector: 'select[control],select[form-control],select[ng-model]',
hostListeners:
{'change': 'onChange($event.target.value)', 'input': 'onChange($event.target.value)'},
hostProperties: {'value': 'value'}

View File

@ -18,7 +18,10 @@ export function setUpControl(c: Control, dir: ControlDirective) {
c.validator = Validators.compose([c.validator, dir.validator]);
dir.valueAccessor.writeValue(c.value);
dir.valueAccessor.registerOnChange(newValue => c.updateValue(newValue));
dir.valueAccessor.registerOnChange(newValue => {
dir.viewToModelUpdate(newValue);
c.updateValue(newValue);
});
}
function _throwError(dir: ControlDirective, message: string): void {

View File

@ -62,6 +62,14 @@ export class TemplateDrivenFormDirective extends ControlContainerDirective imple
});
}
updateModel(dir: ControlDirective, value: any): void {
this._later(_ => {
var c = <Control>this.form.find(dir.path);
c.value = value;
dir.valueAccessor.writeValue(value);
});
}
_findContainer(path: List<string>): ControlGroup {
ListWrapper.removeLast(path);
return <ControlGroup>this.form.find(path);

View File

@ -8,11 +8,12 @@ import {
dispatchEvent,
fakeAsync,
flushMicrotasks,
tick,
el,
expect,
iit,
inject,
it,
inject,
iit,
xit
} from 'angular2/test_lib';
@ -347,6 +348,56 @@ export function main() {
}));
});
it("should support ng-model for complex forms",
inject(
[TestBed], fakeAsync(tb => {
var form = new ControlGroup({"name": new Control("")});
var ctx = MyComp.create({name: "oldValue", form: form});
var t =
`<div [form-model]="form"><input type="text" control="name" [(ng-model)]="name"></div>`;
tb.createView(MyComp, {context: ctx, html: t})
.then((view) => {
view.detectChanges();
var input = view.querySelector("input");
expect(input.value).toEqual("oldValue");
input.value = "updatedValue";
dispatchEvent(input, "change");
tick();
expect(ctx.name).toEqual("updatedValue");
});
flushMicrotasks();
})));
it("should support ng-model for single fields",
inject([TestBed], fakeAsync(tb => {
var form = new Control("");
var ctx = MyComp.create({name: "oldValue", form: form});
var t = `<div><input type="text" [form-control]="form" [(ng-model)]="name"></div>`;
tb.createView(MyComp, {context: ctx, html: t})
.then((view) => {
view.detectChanges();
var input = view.querySelector("input");
expect(input.value).toEqual("oldValue");
input.value = "updatedValue";
dispatchEvent(input, "change");
tick();
expect(ctx.name).toEqual("updatedValue");
});
flushMicrotasks();
})));
describe("template-driven forms", () => {
it("should add new controls and control groups",
inject([TestBed], fakeAsync(tb => {
@ -365,7 +416,7 @@ export function main() {
view.rawView.elementInjectors[0].get(TemplateDrivenFormDirective);
expect(form.controls['user']).not.toBeDefined();
flushMicrotasks();
tick();
expect(form.controls['user']).toBeDefined();
expect(form.controls['user'].controls['login']).toBeDefined();
@ -388,13 +439,13 @@ export function main() {
var form = view.rawView.elementInjectors[0].get(
TemplateDrivenFormDirective);
flushMicrotasks();
tick();
expect(form.controls['login']).toBeDefined();
ctx.name = 'hide';
view.detectChanges();
flushMicrotasks();
tick();
expect(form.controls['login']).not.toBeDefined();
});
@ -430,6 +481,56 @@ export function main() {
});
flushMicrotasks();
})));
it("should support ng-model for complex forms",
inject([TestBed], fakeAsync(tb => {
var ctx = MyComp.create({name: "oldValue"});
var t = `<div form>
<input type="text" control="name" [(ng-model)]="name">
</div>`;
tb.createView(MyComp, {context: ctx, html: t})
.then((view) => {
view.detectChanges();
tick();
var input = view.querySelector("input");
expect(input.value).toEqual("oldValue");
input.value = "updatedValue";
dispatchEvent(input, "change");
tick();
expect(ctx.name).toEqual("updatedValue");
});
flushMicrotasks();
})));
it("should support ng-model for single fields",
inject([TestBed], fakeAsync(tb => {
var ctx = MyComp.create({name: "oldValue"});
var t = `<div><input type="text" [(ng-model)]="name"></div>`;
tb.createView(MyComp, {context: ctx, html: t})
.then((view) => {
view.detectChanges();
var input = view.querySelector("input");
expect(input.value).toEqual("oldValue");
input.value = "updatedValue";
dispatchEvent(input, "change");
tick();
expect(ctx.name).toEqual("updatedValue");
});
flushMicrotasks();
})));
});
});
}