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.
*/
addControl(dir: NgControl): void;
addControl(dir: NgControl): Control;
/**
* 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; }
addControl(dir: NgControl): void {
addControl(dir: NgControl): Control {
const ctrl = new Control();
PromiseWrapper.scheduleMicrotask(() => {
var container = this._findContainer(dir.path);
var ctrl = new Control();
const container = this._findContainer(dir.path);
setUpControl(ctrl, dir);
container.registerControl(dir.name, ctrl);
ctrl.updateValueAndValidity({emitEvent: false});
});
return ctrl;
}
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 []; }
addControl(dir: NgControl): void {
var ctrl: any = this.form.find(dir.path);
addControl(dir: NgControl): Control {
const ctrl: any = this.form.find(dir.path);
setUpControl(ctrl, dir);
ctrl.updateValueAndValidity({emitEvent: false});
this.directives.push(dir);
return ctrl;
}
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 {BaseException} from '../../facade/exceptions';
import {Control} from '../model';
import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../validators';
import {ControlContainer} from './control_container';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor';
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';
export const formControlBinding: any =
@ -41,33 +43,34 @@ export const formControlBinding: any =
@Directive({
selector: '[ngModel]:not([ngControl]):not([ngFormControl])',
providers: [formControlBinding],
inputs: ['model: ngModel'],
outputs: ['update: ngModelChange'],
exportAs: 'ngForm'
})
export class NgModel extends NgControl implements OnChanges {
export class NgModel extends NgControl implements OnChanges,
OnDestroy {
/** @internal */
_control = new Control();
_control: Control;
/** @internal */
_added = false;
update = new EventEmitter();
model: 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_VALUE_ACCESSOR)
valueAccessors: ControlValueAccessor[]) {
super();
this.valueAccessor = selectValueAccessor(this, valueAccessors);
if (!this._parent) this._control = new Control();
}
ngOnChanges(changes: SimpleChanges) {
if (!this._added) {
setUpControl(this._control, this);
this._control.updateValueAndValidity({emitEvent: false});
this._added = true;
}
this._checkName();
if (!this._added) this._addControl();
if (isPropertyUpdated(changes, this.viewModel)) {
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 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); }
@ -89,4 +98,24 @@ export class NgModel extends NgControl implements OnChanges {
this.viewModel = 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 {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';

View File

@ -409,8 +409,8 @@ export function main() {
var ngModel: any /** TODO #9100 */;
beforeEach(() => {
ngModel =
new NgModel([Validators.required], [asyncValidator('expected')], [defaultAccessor]);
ngModel = new NgModel(
null, [Validators.required], [asyncValidator('expected')], [defaultAccessor]);
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 {ComponentFixture} from '@angular/compiler/testing';
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',
inject(
[TestComponentBuilder, AsyncTestCompleter],
(tcb: TestComponentBuilder, async: AsyncTestCompleter) => {
var t = `<div [ngFormModel]="form">
// TODO(kara): Revisit when re-writing to ngModelOptions
xit('should support custom value accessors on non builtin input elements that fire a change event without a \'target\' property',
inject(
[TestComponentBuilder, AsyncTestCompleter],
(tcb: TestComponentBuilder, async: AsyncTestCompleter) => {
var t = `<div [ngFormModel]="form">
<my-input ngControl="name"></my-input>
</div>`;
tcb.overrideTemplate(MyComp8, t).createAsync(MyComp8).then((fixture) => {
fixture.debugElement.componentInstance.form =
new ControlGroup({'name': new Control('aa')});
fixture.detectChanges();
var input = fixture.debugElement.query(By.css('my-input'));
expect(input.componentInstance.value).toEqual('!aa!');
tcb.overrideTemplate(MyComp8, t).createAsync(MyComp8).then((fixture) => {
fixture.debugElement.componentInstance.form =
new ControlGroup({'name': new Control('aa')});
fixture.detectChanges();
var input = fixture.debugElement.query(By.css('my-input'));
expect(input.componentInstance.value).toEqual('!aa!');
input.componentInstance.value = '!bb!';
ObservableWrapper.subscribe(input.componentInstance.onInput, (value) => {
expect(fixture.debugElement.componentInstance.form.value).toEqual({
'name': 'bb'
});
async.done();
});
input.componentInstance.dispatchChangeEvent();
});
}));
input.componentInstance.value = '!bb!';
ObservableWrapper.subscribe(input.componentInstance.onInput, (value) => {
expect(fixture.debugElement.componentInstance.form.value).toEqual({
'name': 'bb'
});
async.done();
});
input.componentInstance.dispatchChangeEvent();
});
}));
});
@ -1220,6 +1222,42 @@ export function main() {
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>',
fakeAsync(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
@ -1542,7 +1580,8 @@ class UniqLoginValidator implements Validator {
template: '',
directives: [
FORM_DIRECTIVES, WrappedValue, MyInput, NgIf, NgFor, LoginIsEmptyValidator, UniqLoginValidator
]
],
providers: [FORM_PROVIDERS]
})
class MyComp8 {
form: any;