feat(forms): add support for standalone ngModel dirs inside forms
Closes #9230
This commit is contained in:
parent
826f89f862
commit
6edf0474cc
|
@ -24,7 +24,7 @@ export interface Form {
|
||||||
/**
|
/**
|
||||||
* Add a control to this form.
|
* Add a control to this form.
|
||||||
*/
|
*/
|
||||||
addControl(dir: NgControl): FormControl;
|
addControl(dir: NgControl): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove a control from this form.
|
* Remove a control from this form.
|
||||||
|
|
|
@ -116,16 +116,13 @@ 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: NgModel): FormControl {
|
addControl(dir: NgModel): void {
|
||||||
const ctrl = new FormControl();
|
|
||||||
PromiseWrapper.scheduleMicrotask(() => {
|
PromiseWrapper.scheduleMicrotask(() => {
|
||||||
const container = this._findContainer(dir.path);
|
const container = this._findContainer(dir.path);
|
||||||
dir._control = <FormControl>container.registerControl(dir.name, ctrl);
|
dir._control = <FormControl>container.registerControl(dir.name, dir.control);
|
||||||
setUpControl(dir.control, dir);
|
setUpControl(dir.control, dir);
|
||||||
dir.control.updateValueAndValidity({emitEvent: false});
|
dir.control.updateValueAndValidity({emitEvent: false});
|
||||||
});
|
});
|
||||||
|
|
||||||
return ctrl;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getControl(dir: NgModel): FormControl { return <FormControl>this.form.find(dir.path); }
|
getControl(dir: NgModel): FormControl { return <FormControl>this.form.find(dir.path); }
|
||||||
|
|
|
@ -55,14 +55,14 @@ export const formControlBinding: any =
|
||||||
export class NgModel extends NgControl implements OnChanges,
|
export class NgModel extends NgControl implements OnChanges,
|
||||||
OnDestroy {
|
OnDestroy {
|
||||||
/** @internal */
|
/** @internal */
|
||||||
_control: FormControl;
|
_control = new FormControl();
|
||||||
/** @internal */
|
/** @internal */
|
||||||
_added = false;
|
_registered = false;
|
||||||
viewModel: any;
|
viewModel: any;
|
||||||
|
|
||||||
@Input('ngModel') model: any;
|
@Input('ngModel') model: any;
|
||||||
@Input() name: string;
|
@Input() name: string;
|
||||||
@Input('ngModelOptions') options: {name?: string};
|
@Input('ngModelOptions') options: {name?: string, standalone?: boolean};
|
||||||
@Output('ngModelChange') update = new EventEmitter();
|
@Output('ngModelChange') update = new EventEmitter();
|
||||||
|
|
||||||
constructor(@Optional() @Host() private _parent: ControlContainer,
|
constructor(@Optional() @Host() private _parent: ControlContainer,
|
||||||
|
@ -72,12 +72,11 @@ export class NgModel extends NgControl implements OnChanges,
|
||||||
valueAccessors: ControlValueAccessor[]) {
|
valueAccessors: ControlValueAccessor[]) {
|
||||||
super();
|
super();
|
||||||
this.valueAccessor = selectValueAccessor(this, valueAccessors);
|
this.valueAccessor = selectValueAccessor(this, valueAccessors);
|
||||||
if (!this._parent) this._control = new FormControl();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges) {
|
ngOnChanges(changes: SimpleChanges) {
|
||||||
this._checkName();
|
this._checkName();
|
||||||
if (!this._added) this._addControl();
|
if (!this._registered) this._setUpControl();
|
||||||
|
|
||||||
if (isPropertyUpdated(changes, this.viewModel)) {
|
if (isPropertyUpdated(changes, this.viewModel)) {
|
||||||
this._control.updateValue(this.model);
|
this._control.updateValue(this.model);
|
||||||
|
@ -106,25 +105,32 @@ export class NgModel extends NgControl implements OnChanges,
|
||||||
ObservableWrapper.callEmit(this.update, newValue);
|
ObservableWrapper.callEmit(this.update, newValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _addControl(): void {
|
private _setUpControl(): void {
|
||||||
this._control = this.formDirective ? this.formDirective.addControl(this) :
|
this._isStandalone() ? this._setUpStandalone() :
|
||||||
this._addStandaloneControl();
|
this.formDirective.addControl(this);
|
||||||
this._added = true;
|
this._registered = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _addStandaloneControl(): FormControl {
|
private _isStandalone(): boolean {
|
||||||
|
return !this._parent || (this.options && this.options.standalone);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setUpStandalone(): void {
|
||||||
setUpControl(this._control, this);
|
setUpControl(this._control, this);
|
||||||
this._control.updateValueAndValidity({emitEvent: false});
|
this._control.updateValueAndValidity({emitEvent: false});
|
||||||
return this._control;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _checkName() {
|
private _checkName() {
|
||||||
if (this.options && this.options.name) this.name = this.options.name;
|
if (this.options && this.options.name) this.name = this.options.name;
|
||||||
|
|
||||||
if (this._parent && !this.name) {
|
if (!this._isStandalone() && !this.name) {
|
||||||
throw new BaseException(
|
throw new BaseException(
|
||||||
`Name attribute must be set if ngModel is used within a form.
|
`If ngModel is used within a form tag, either the name attribute must be set
|
||||||
Example: <input [(ngModel)]="person.firstName" name="first">`);
|
or the form control must be defined as 'standalone' in ngModelOptions.
|
||||||
|
|
||||||
|
Example 1: <input [(ngModel)]="person.firstName" name="first">
|
||||||
|
Example 2: <input [(ngModel)]="person.firstName" [ngModelOptions]="{standalone: true}">
|
||||||
|
`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -144,12 +144,11 @@ export class FormGroupDirective extends ControlContainer implements Form,
|
||||||
|
|
||||||
get path(): string[] { return []; }
|
get path(): string[] { return []; }
|
||||||
|
|
||||||
addControl(dir: NgControl): FormControl {
|
addControl(dir: NgControl): void {
|
||||||
const 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): FormControl { return <FormControl>this.form.find(dir.path); }
|
getControl(dir: NgControl): FormControl { return <FormControl>this.form.find(dir.path); }
|
||||||
|
|
|
@ -281,7 +281,7 @@ export function main() {
|
||||||
personControlGroupDir = new NgModelGroup(form, [], []);
|
personControlGroupDir = new NgModelGroup(form, [], []);
|
||||||
personControlGroupDir.name = 'person';
|
personControlGroupDir.name = 'person';
|
||||||
|
|
||||||
loginControlDir = new FormControlName(personControlGroupDir, null, null, [defaultAccessor]);
|
loginControlDir = new NgModel(personControlGroupDir, null, null, [defaultAccessor]);
|
||||||
loginControlDir.name = 'login';
|
loginControlDir.name = 'login';
|
||||||
loginControlDir.valueAccessor = new DummyControlValueAccessor();
|
loginControlDir.valueAccessor = new DummyControlValueAccessor();
|
||||||
});
|
});
|
||||||
|
|
|
@ -1371,7 +1371,7 @@ export function main() {
|
||||||
})));
|
})));
|
||||||
|
|
||||||
|
|
||||||
it('should throw if ngModel has a parent form but no name attr',
|
it('should throw if ngModel has a parent form but no name attr or standalone label',
|
||||||
inject(
|
inject(
|
||||||
[TestComponentBuilder, AsyncTestCompleter],
|
[TestComponentBuilder, AsyncTestCompleter],
|
||||||
(tcb: TestComponentBuilder, async: AsyncTestCompleter) => {
|
(tcb: TestComponentBuilder, async: AsyncTestCompleter) => {
|
||||||
|
@ -1384,11 +1384,29 @@ export function main() {
|
||||||
.createAsync(MyComp8)
|
.createAsync(MyComp8)
|
||||||
.then((fixture) => {
|
.then((fixture) => {
|
||||||
expect(() => fixture.detectChanges())
|
expect(() => fixture.detectChanges())
|
||||||
.toThrowError(new RegExp(`Name attribute must be set`));
|
.toThrowError(new RegExp(`name attribute must be set`));
|
||||||
async.done();
|
async.done();
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
it('should not throw if ngModel has a parent form, no name attr, and a standalone label',
|
||||||
|
inject(
|
||||||
|
[TestComponentBuilder, AsyncTestCompleter],
|
||||||
|
(tcb: TestComponentBuilder, async: AsyncTestCompleter) => {
|
||||||
|
const t = `<form>
|
||||||
|
<input [(ngModel)]="name" [ngModelOptions]="{standalone: true}">
|
||||||
|
</form>`;
|
||||||
|
|
||||||
|
tcb.overrideTemplate(MyComp8, t)
|
||||||
|
.overrideProviders(MyComp8, providerArr)
|
||||||
|
.createAsync(MyComp8)
|
||||||
|
.then((fixture) => {
|
||||||
|
expect(() => fixture.detectChanges()).not.toThrow();
|
||||||
|
async.done();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
it('should override name attribute with ngModelOptions name if provided',
|
it('should override name attribute with ngModelOptions name if provided',
|
||||||
fakeAsync(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
|
fakeAsync(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
|
||||||
const t = `
|
const t = `
|
||||||
|
@ -1407,6 +1425,29 @@ export function main() {
|
||||||
expect(form.value).toEqual({two: 'some data'});
|
expect(form.value).toEqual({two: 'some data'});
|
||||||
})));
|
})));
|
||||||
|
|
||||||
|
it('should not register standalone ngModels with parent form',
|
||||||
|
fakeAsync(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
|
||||||
|
const t = `
|
||||||
|
<form>
|
||||||
|
<input name="one" [(ngModel)]="data">
|
||||||
|
<input [(ngModel)]="list" [ngModelOptions]="{standalone: true}">
|
||||||
|
</form>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const fixture = tcb.overrideTemplate(MyComp8, t).createFakeAsync(MyComp8);
|
||||||
|
tick();
|
||||||
|
fixture.debugElement.componentInstance.data = 'some data';
|
||||||
|
fixture.debugElement.componentInstance.list = 'should not show';
|
||||||
|
fixture.detectChanges();
|
||||||
|
const form = fixture.debugElement.children[0].injector.get(NgForm);
|
||||||
|
const inputs = fixture.debugElement.queryAll(By.css('input'));
|
||||||
|
|
||||||
|
tick();
|
||||||
|
expect(form.value).toEqual({one: 'some data'});
|
||||||
|
expect(inputs[1].nativeElement.value).toEqual('should not show');
|
||||||
|
})));
|
||||||
|
|
||||||
|
|
||||||
it('should support <type=radio>',
|
it('should support <type=radio>',
|
||||||
fakeAsync(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
|
fakeAsync(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
|
||||||
const t = `<form>
|
const t = `<form>
|
||||||
|
|
Loading…
Reference in New Issue