feat(forms): allow ngModel to register with parent form

This commit is contained in:
Kara Erickson 2016-06-10 10:09:50 -07:00
parent 5267115481
commit 4ed6cf7519
7 changed files with 117 additions and 47 deletions

View File

@ -15,7 +15,7 @@ export interface Form {
/** /**
* Add a control to this form. * Add a control to this form.
*/ */
addControl(dir: NgControl): void; addControl(dir: NgControl): Control;
/** /**
* Remove a control from this form. * Remove a control from this form.

View File

@ -109,14 +109,15 @@ export class NgForm extends ControlContainer implements Form {
get controls(): {[key: string]: AbstractControl} { return this.form.controls; } get controls(): {[key: string]: AbstractControl} { return this.form.controls; }
addControl(dir: NgControl): void { addControl(dir: NgControl): Control {
const ctrl = new Control();
PromiseWrapper.scheduleMicrotask(() => { PromiseWrapper.scheduleMicrotask(() => {
var container = this._findContainer(dir.path); const container = this._findContainer(dir.path);
var ctrl = new Control();
setUpControl(ctrl, dir); setUpControl(ctrl, dir);
container.registerControl(dir.name, ctrl); container.registerControl(dir.name, ctrl);
ctrl.updateValueAndValidity({emitEvent: false}); ctrl.updateValueAndValidity({emitEvent: false});
}); });
return ctrl;
} }
getControl(dir: NgControl): Control { return <Control>this.form.find(dir.path); } getControl(dir: NgControl): Control { return <Control>this.form.find(dir.path); }

View File

@ -138,11 +138,12 @@ export class NgFormModel extends ControlContainer implements Form,
get path(): string[] { return []; } get path(): string[] { return []; }
addControl(dir: NgControl): void { addControl(dir: NgControl): Control {
var ctrl: any = this.form.find(dir.path); const ctrl: any = this.form.find(dir.path);
setUpControl(ctrl, dir); setUpControl(ctrl, dir);
ctrl.updateValueAndValidity({emitEvent: false}); ctrl.updateValueAndValidity({emitEvent: false});
this.directives.push(dir); this.directives.push(dir);
return ctrl;
} }
getControl(dir: NgControl): Control { return <Control>this.form.find(dir.path); } getControl(dir: NgControl): Control { return <Control>this.form.find(dir.path); }

View File

@ -1,12 +1,14 @@
import {Directive, Inject, OnChanges, Optional, Self, SimpleChanges, forwardRef} from '@angular/core'; import {Directive, Host, Inject, Input, OnChanges, OnDestroy, Optional, Output, Self, SimpleChanges, forwardRef} from '@angular/core';
import {EventEmitter, ObservableWrapper} from '../../facade/async'; import {EventEmitter, ObservableWrapper} from '../../facade/async';
import {BaseException} from '../../facade/exceptions';
import {Control} from '../model'; import {Control} from '../model';
import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../validators'; import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../validators';
import {ControlContainer} from './control_container';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor'; import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor';
import {NgControl} from './ng_control'; import {NgControl} from './ng_control';
import {composeAsyncValidators, composeValidators, isPropertyUpdated, selectValueAccessor, setUpControl} from './shared'; import {composeAsyncValidators, composeValidators, controlPath, isPropertyUpdated, selectValueAccessor, setUpControl} from './shared';
import {AsyncValidatorFn, ValidatorFn} from './validators'; import {AsyncValidatorFn, ValidatorFn} from './validators';
export const formControlBinding: any = export const formControlBinding: any =
@ -41,33 +43,34 @@ export const formControlBinding: any =
@Directive({ @Directive({
selector: '[ngModel]:not([ngControl]):not([ngFormControl])', selector: '[ngModel]:not([ngControl]):not([ngFormControl])',
providers: [formControlBinding], providers: [formControlBinding],
inputs: ['model: ngModel'],
outputs: ['update: ngModelChange'],
exportAs: 'ngForm' exportAs: 'ngForm'
}) })
export class NgModel extends NgControl implements OnChanges { export class NgModel extends NgControl implements OnChanges,
OnDestroy {
/** @internal */ /** @internal */
_control = new Control(); _control: Control;
/** @internal */ /** @internal */
_added = false; _added = false;
update = new EventEmitter();
model: any;
viewModel: any; viewModel: any;
constructor(@Optional() @Self() @Inject(NG_VALIDATORS) private _validators: any[], @Input('ngModel') model: any;
@Input() name: string;
@Output('ngModelChange') update = new EventEmitter();
constructor(@Optional() @Host() private _parent: ControlContainer,
@Optional() @Self() @Inject(NG_VALIDATORS) private _validators: any[],
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) private _asyncValidators: any[], @Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) private _asyncValidators: any[],
@Optional() @Self() @Inject(NG_VALUE_ACCESSOR) @Optional() @Self() @Inject(NG_VALUE_ACCESSOR)
valueAccessors: ControlValueAccessor[]) { valueAccessors: ControlValueAccessor[]) {
super(); super();
this.valueAccessor = selectValueAccessor(this, valueAccessors); this.valueAccessor = selectValueAccessor(this, valueAccessors);
if (!this._parent) this._control = new Control();
} }
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
if (!this._added) { this._checkName();
setUpControl(this._control, this); if (!this._added) this._addControl();
this._control.updateValueAndValidity({emitEvent: false});
this._added = true;
}
if (isPropertyUpdated(changes, this.viewModel)) { if (isPropertyUpdated(changes, this.viewModel)) {
this._control.updateValue(this.model); this._control.updateValue(this.model);
@ -75,9 +78,15 @@ export class NgModel extends NgControl implements OnChanges {
} }
} }
ngOnDestroy(): void { this.formDirective && this.formDirective.removeControl(this); }
get control(): Control { return this._control; } get control(): Control { return this._control; }
get path(): string[] { return []; } get path(): string[] {
return this._parent ? controlPath(this.name, this._parent) : [];
}
get formDirective(): any { return this._parent ? this._parent.formDirective : null; }
get validator(): ValidatorFn { return composeValidators(this._validators); } get validator(): ValidatorFn { return composeValidators(this._validators); }
@ -89,4 +98,24 @@ export class NgModel extends NgControl implements OnChanges {
this.viewModel = newValue; this.viewModel = newValue;
ObservableWrapper.callEmit(this.update, newValue); ObservableWrapper.callEmit(this.update, newValue);
} }
private _addControl(): void {
this._control = this.formDirective ? this.formDirective.addControl(this) :
this._addStandaloneControl();
this._added = true;
}
private _addStandaloneControl(): Control {
setUpControl(this._control, this);
this._control.updateValueAndValidity({emitEvent: false});
return this._control;
}
private _checkName() {
if (this._parent && !this.name) {
throw new BaseException(
`Name attribute must be set if ngModel is used within a form.
Example: <input [(ngModel)]="person.firstName" name="first">`);
}
}
} }

View File

@ -4,7 +4,7 @@ import {fakeAsync, flushMicrotasks, Log, tick,} from '@angular/core/testing';
import {SpyNgControl, SpyValueAccessor} from '../spies'; import {SpyNgControl, SpyValueAccessor} from '../spies';
import {ControlGroup, Control, NgControlName, NgControlGroup, NgFormModel, ControlValueAccessor, Validators, NgForm, NgModel, NgFormControl, NgControl, DefaultValueAccessor, CheckboxControlValueAccessor, SelectControlValueAccessor, Validator} from '@angular/common'; import {ControlGroup, Control, NgControlName, NgControlGroup, NgFormModel, ControlValueAccessor, Validators, NgForm, NgModel, NgFormControl, NgControl, DefaultValueAccessor, CheckboxControlValueAccessor, SelectControlValueAccessor, Validator} from '@angular/common/src/forms-deprecated';
import {selectValueAccessor, composeValidators} from '@angular/common/src/forms-deprecated/directives/shared'; import {selectValueAccessor, composeValidators} from '@angular/common/src/forms-deprecated/directives/shared';

View File

@ -409,8 +409,8 @@ export function main() {
var ngModel: any /** TODO #9100 */; var ngModel: any /** TODO #9100 */;
beforeEach(() => { beforeEach(() => {
ngModel = ngModel = new NgModel(
new NgModel([Validators.required], [asyncValidator('expected')], [defaultAccessor]); null, [Validators.required], [asyncValidator('expected')], [defaultAccessor]);
ngModel.valueAccessor = new DummyControlValueAccessor(); ngModel.valueAccessor = new DummyControlValueAccessor();
}); });

View File

@ -1,4 +1,5 @@
import {Control, ControlGroup, ControlValueAccessor, FORM_DIRECTIVES, NG_ASYNC_VALIDATORS, NG_VALIDATORS, NgControl, NgFor, NgForm, NgIf, RadioButtonState, Validator, Validators} from '@angular/common'; import {NgFor, NgIf} from '@angular/common';
import {Control, ControlGroup, ControlValueAccessor, FORM_DIRECTIVES, FORM_PROVIDERS, NG_ASYNC_VALIDATORS, NG_VALIDATORS, NgControl, NgForm, NgModel, RadioButtonState, Validator, Validators} from '@angular/common/src/forms';
import {TestComponentBuilder} from '@angular/compiler/testing'; import {TestComponentBuilder} from '@angular/compiler/testing';
import {ComponentFixture} from '@angular/compiler/testing'; import {ComponentFixture} from '@angular/compiler/testing';
import {Component, Directive, EventEmitter, Output} from '@angular/core'; import {Component, Directive, EventEmitter, Output} from '@angular/core';
@ -812,31 +813,32 @@ export function main() {
}); });
})); }));
it('should support custom value accessors on non builtin input elements that fire a change event without a \'target\' property', // TODO(kara): Revisit when re-writing to ngModelOptions
inject( xit('should support custom value accessors on non builtin input elements that fire a change event without a \'target\' property',
[TestComponentBuilder, AsyncTestCompleter], inject(
(tcb: TestComponentBuilder, async: AsyncTestCompleter) => { [TestComponentBuilder, AsyncTestCompleter],
var t = `<div [ngFormModel]="form"> (tcb: TestComponentBuilder, async: AsyncTestCompleter) => {
var t = `<div [ngFormModel]="form">
<my-input ngControl="name"></my-input> <my-input ngControl="name"></my-input>
</div>`; </div>`;
tcb.overrideTemplate(MyComp8, t).createAsync(MyComp8).then((fixture) => { tcb.overrideTemplate(MyComp8, t).createAsync(MyComp8).then((fixture) => {
fixture.debugElement.componentInstance.form = fixture.debugElement.componentInstance.form =
new ControlGroup({'name': new Control('aa')}); new ControlGroup({'name': new Control('aa')});
fixture.detectChanges(); fixture.detectChanges();
var input = fixture.debugElement.query(By.css('my-input')); var input = fixture.debugElement.query(By.css('my-input'));
expect(input.componentInstance.value).toEqual('!aa!'); expect(input.componentInstance.value).toEqual('!aa!');
input.componentInstance.value = '!bb!'; input.componentInstance.value = '!bb!';
ObservableWrapper.subscribe(input.componentInstance.onInput, (value) => { ObservableWrapper.subscribe(input.componentInstance.onInput, (value) => {
expect(fixture.debugElement.componentInstance.form.value).toEqual({ expect(fixture.debugElement.componentInstance.form.value).toEqual({
'name': 'bb' 'name': 'bb'
}); });
async.done(); async.done();
}); });
input.componentInstance.dispatchChangeEvent(); input.componentInstance.dispatchChangeEvent();
}); });
})); }));
}); });
@ -1220,6 +1222,42 @@ export function main() {
expect(fixture.debugElement.componentInstance.name).toEqual('updatedValue'); expect(fixture.debugElement.componentInstance.name).toEqual('updatedValue');
}))); })));
it('should support ngModel registration with a parent form',
fakeAsync(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
const t = `
<form>
<input name="first" [(ngModel)]="name" maxlength="4">
</form>
`;
let fixture = tcb.overrideTemplate(MyComp8, t).createFakeAsync(MyComp8);
tick();
fixture.debugElement.componentInstance.name = 'Nancy';
fixture.detectChanges();
var form = fixture.debugElement.children[0].inject(NgForm);
tick();
expect(form.value).toEqual({first: 'Nancy'});
expect(form.valid).toBe(false);
})));
it('should throw if ngModel has a parent form but no name attr',
inject(
[TestComponentBuilder, AsyncTestCompleter],
(tcb: TestComponentBuilder, async: AsyncTestCompleter) => {
const t = `<form>
<input [(ngModel)]="name">
</form>`;
tcb.overrideTemplate(MyComp8, t).createAsync(MyComp8).then((fixture) => {
expect(() => fixture.detectChanges())
.toThrowError(new RegExp(`Name attribute must be set`));
async.done();
});
}));
it('should support <type=radio>', it('should support <type=radio>',
fakeAsync(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { fakeAsync(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
@ -1542,7 +1580,8 @@ class UniqLoginValidator implements Validator {
template: '', template: '',
directives: [ directives: [
FORM_DIRECTIVES, WrappedValue, MyInput, NgIf, NgFor, LoginIsEmptyValidator, UniqLoginValidator FORM_DIRECTIVES, WrappedValue, MyInput, NgIf, NgFor, LoginIsEmptyValidator, UniqLoginValidator
] ],
providers: [FORM_PROVIDERS]
}) })
class MyComp8 { class MyComp8 {
form: any; form: any;