feat(forms): add default updateOn values for groups and arrays (#18536)
This commit adds support for setting default `updateOn` values in `FormGroups` and `FormArrays`. If you set `updateOn` to ’blur’` at the group level, all child controls will default to `’blur’`, unless the child has explicitly specified a different `updateOn` value. ``` const c = new FormGroup({ one: new FormControl() }, {updateOn: blur}); ``` It's worth noting that parent groups will always update their value and validity immediately upon value/validity updates from children. In other words, if a group is set to update on blur and its children are individually set to update on change, the group will still update on change with its children; its default value will simply not be used.
This commit is contained in:
parent
dca50deae4
commit
ff5c58be6b
|
@ -150,7 +150,7 @@ export class FormGroupDirective extends ControlContainer implements Form,
|
|||
_syncPendingControls() {
|
||||
this.form._syncPendingControls();
|
||||
this.directives.forEach(dir => {
|
||||
if (dir.control._updateOn === 'submit') {
|
||||
if (dir.control.updateOn === 'submit') {
|
||||
dir.viewToModelUpdate(dir.control._pendingValue);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -84,7 +84,7 @@ function setUpViewChangePipeline(control: FormControl, dir: NgControl): void {
|
|||
control._pendingValue = newValue;
|
||||
control._pendingDirty = true;
|
||||
|
||||
if (control._updateOn === 'change') updateControl(control, dir);
|
||||
if (control.updateOn === 'change') updateControl(control, dir);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -92,8 +92,8 @@ function setUpBlurPipeline(control: FormControl, dir: NgControl): void {
|
|||
dir.valueAccessor !.registerOnTouched(() => {
|
||||
control._pendingTouched = true;
|
||||
|
||||
if (control._updateOn === 'blur') updateControl(control, dir);
|
||||
if (control._updateOn !== 'submit') control.markAsTouched();
|
||||
if (control.updateOn === 'blur') updateControl(control, dir);
|
||||
if (control.updateOn !== 'submit') control.markAsTouched();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -118,6 +118,9 @@ export abstract class AbstractControl {
|
|||
/** @internal */
|
||||
_onCollectionChange = () => {};
|
||||
|
||||
/** @internal */
|
||||
_updateOn: FormHooks;
|
||||
|
||||
private _valueChanges: EventEmitter<any>;
|
||||
private _statusChanges: EventEmitter<any>;
|
||||
private _status: string;
|
||||
|
@ -242,6 +245,15 @@ export abstract class AbstractControl {
|
|||
*/
|
||||
get statusChanges(): Observable<any> { return this._statusChanges; }
|
||||
|
||||
/**
|
||||
* Returns the update strategy of the `AbstractControl` (i.e.
|
||||
* the event on which the control will update itself).
|
||||
* Possible values: `'change'` (default) | `'blur'` | `'submit'`
|
||||
*/
|
||||
get updateOn(): FormHooks {
|
||||
return this._updateOn ? this._updateOn : (this.parent ? this.parent.updateOn : 'change');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the synchronous validators that are active on this control. Calling
|
||||
* this will overwrite any existing sync validators.
|
||||
|
@ -624,6 +636,13 @@ export abstract class AbstractControl {
|
|||
|
||||
/** @internal */
|
||||
_registerOnCollectionChange(fn: () => void): void { this._onCollectionChange = fn; }
|
||||
|
||||
/** @internal */
|
||||
_setUpdateStrategy(opts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null): void {
|
||||
if (isOptionsObj(opts) && (opts as AbstractControlOptions).updateOn != null) {
|
||||
this._updateOn = (opts as AbstractControlOptions).updateOn !;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -697,9 +716,6 @@ export class FormControl extends AbstractControl {
|
|||
/** @internal */
|
||||
_onChange: Function[] = [];
|
||||
|
||||
/** @internal */
|
||||
_updateOn: FormHooks = 'change';
|
||||
|
||||
/** @internal */
|
||||
_pendingValue: any;
|
||||
|
||||
|
@ -841,7 +857,7 @@ export class FormControl extends AbstractControl {
|
|||
|
||||
/** @internal */
|
||||
_syncPendingControls(): boolean {
|
||||
if (this._updateOn === 'submit') {
|
||||
if (this.updateOn === 'submit') {
|
||||
this.setValue(this._pendingValue, {onlySelf: true, emitModelToViewChange: false});
|
||||
if (this._pendingDirty) this.markAsDirty();
|
||||
if (this._pendingTouched) this.markAsTouched();
|
||||
|
@ -859,12 +875,6 @@ export class FormControl extends AbstractControl {
|
|||
this._value = this._pendingValue = formState;
|
||||
}
|
||||
}
|
||||
|
||||
private _setUpdateStrategy(opts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null): void {
|
||||
if (isOptionsObj(opts) && (opts as AbstractControlOptions).updateOn != null) {
|
||||
this._updateOn = (opts as AbstractControlOptions).updateOn !;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -925,6 +935,17 @@ export class FormControl extends AbstractControl {
|
|||
* }, {validators: passwordMatchValidator, asyncValidators: otherValidator});
|
||||
* ```
|
||||
*
|
||||
* The options object can also be used to set a default value for each child
|
||||
* control's `updateOn` property. If you set `updateOn` to `'blur'` at the
|
||||
* group level, all child controls will default to 'blur', unless the child
|
||||
* has explicitly specified a different `updateOn` value.
|
||||
*
|
||||
* ```ts
|
||||
* const c = new FormGroup({
|
||||
* one: new FormControl()
|
||||
* }, {updateOn: 'blur'});
|
||||
* ```
|
||||
*
|
||||
* * **npm package**: `@angular/forms`
|
||||
*
|
||||
* @stable
|
||||
|
@ -938,6 +959,7 @@ export class FormGroup extends AbstractControl {
|
|||
coerceToValidator(validatorOrOpts),
|
||||
coerceToAsyncValidator(asyncValidator, validatorOrOpts));
|
||||
this._initObservables();
|
||||
this._setUpdateStrategy(validatorOrOpts);
|
||||
this._setUpControls();
|
||||
this.updateValueAndValidity({onlySelf: true, emitEvent: false});
|
||||
}
|
||||
|
@ -1242,6 +1264,17 @@ export class FormGroup extends AbstractControl {
|
|||
* ], {validators: myValidator, asyncValidators: myAsyncValidator});
|
||||
* ```
|
||||
*
|
||||
* The options object can also be used to set a default value for each child
|
||||
* control's `updateOn` property. If you set `updateOn` to `'blur'` at the
|
||||
* array level, all child controls will default to 'blur', unless the child
|
||||
* has explicitly specified a different `updateOn` value.
|
||||
*
|
||||
* ```ts
|
||||
* const c = new FormArray([
|
||||
* new FormControl()
|
||||
* ], {updateOn: 'blur'});
|
||||
* ```
|
||||
*
|
||||
* ### Adding or removing controls
|
||||
*
|
||||
* To change the controls in the array, use the `push`, `insert`, or `removeAt` methods
|
||||
|
@ -1263,6 +1296,7 @@ export class FormArray extends AbstractControl {
|
|||
coerceToValidator(validatorOrOpts),
|
||||
coerceToAsyncValidator(asyncValidator, validatorOrOpts));
|
||||
this._initObservables();
|
||||
this._setUpdateStrategy(validatorOrOpts);
|
||||
this._setUpControls();
|
||||
this.updateValueAndValidity({onlySelf: true, emitEvent: false});
|
||||
}
|
||||
|
|
|
@ -80,17 +80,84 @@ export function main() {
|
|||
|
||||
it('should default to on change', () => {
|
||||
const c = new FormControl('');
|
||||
expect(c._updateOn).toEqual('change');
|
||||
expect(c.updateOn).toEqual('change');
|
||||
});
|
||||
|
||||
it('should default to on change with an options obj', () => {
|
||||
const c = new FormControl('', {validators: Validators.required});
|
||||
expect(c._updateOn).toEqual('change');
|
||||
expect(c.updateOn).toEqual('change');
|
||||
});
|
||||
|
||||
it('should set updateOn when updating on blur', () => {
|
||||
const c = new FormControl('', {updateOn: 'blur'});
|
||||
expect(c._updateOn).toEqual('blur');
|
||||
expect(c.updateOn).toEqual('blur');
|
||||
});
|
||||
|
||||
describe('in groups and arrays', () => {
|
||||
it('should default to group updateOn when not set in control', () => {
|
||||
const g =
|
||||
new FormGroup({one: new FormControl(), two: new FormControl()}, {updateOn: 'blur'});
|
||||
|
||||
expect(g.get('one') !.updateOn).toEqual('blur');
|
||||
expect(g.get('two') !.updateOn).toEqual('blur');
|
||||
});
|
||||
|
||||
it('should default to array updateOn when not set in control', () => {
|
||||
const a = new FormArray([new FormControl(), new FormControl()], {updateOn: 'blur'});
|
||||
|
||||
expect(a.get([0]) !.updateOn).toEqual('blur');
|
||||
expect(a.get([1]) !.updateOn).toEqual('blur');
|
||||
});
|
||||
|
||||
it('should set updateOn with nested groups', () => {
|
||||
const g = new FormGroup(
|
||||
{
|
||||
group: new FormGroup({one: new FormControl(), two: new FormControl()}),
|
||||
},
|
||||
{updateOn: 'blur'});
|
||||
|
||||
expect(g.get('group.one') !.updateOn).toEqual('blur');
|
||||
expect(g.get('group.two') !.updateOn).toEqual('blur');
|
||||
expect(g.get('group') !.updateOn).toEqual('blur');
|
||||
});
|
||||
|
||||
it('should set updateOn with nested arrays', () => {
|
||||
const g = new FormGroup(
|
||||
{
|
||||
arr: new FormArray([new FormControl(), new FormControl()]),
|
||||
},
|
||||
{updateOn: 'blur'});
|
||||
|
||||
expect(g.get(['arr', 0]) !.updateOn).toEqual('blur');
|
||||
expect(g.get(['arr', 1]) !.updateOn).toEqual('blur');
|
||||
expect(g.get('arr') !.updateOn).toEqual('blur');
|
||||
});
|
||||
|
||||
it('should allow control updateOn to override group updateOn', () => {
|
||||
const g = new FormGroup(
|
||||
{one: new FormControl('', {updateOn: 'change'}), two: new FormControl()},
|
||||
{updateOn: 'blur'});
|
||||
|
||||
expect(g.get('one') !.updateOn).toEqual('change');
|
||||
expect(g.get('two') !.updateOn).toEqual('blur');
|
||||
});
|
||||
|
||||
it('should set updateOn with complex setup', () => {
|
||||
const g = new FormGroup({
|
||||
group: new FormGroup(
|
||||
{one: new FormControl('', {updateOn: 'change'}), two: new FormControl()},
|
||||
{updateOn: 'blur'}),
|
||||
groupTwo: new FormGroup({one: new FormControl()}, {updateOn: 'submit'}),
|
||||
three: new FormControl()
|
||||
});
|
||||
|
||||
expect(g.get('group.one') !.updateOn).toEqual('change');
|
||||
expect(g.get('group.two') !.updateOn).toEqual('blur');
|
||||
expect(g.get('groupTwo.one') !.updateOn).toEqual('submit');
|
||||
expect(g.get('three') !.updateOn).toEqual('change');
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -931,7 +931,6 @@ export function main() {
|
|||
sub.unsubscribe();
|
||||
});
|
||||
|
||||
|
||||
it('should mark as pristine properly if pending dirty', () => {
|
||||
const fixture = initTest(FormControlComp);
|
||||
const control = new FormControl('', {updateOn: 'blur'});
|
||||
|
@ -955,6 +954,94 @@ export function main() {
|
|||
expect(control.dirty).toBe(false, 'Expected pending dirty value to reset.');
|
||||
});
|
||||
|
||||
it('should update on blur with group updateOn', () => {
|
||||
const fixture = initTest(FormGroupComp);
|
||||
const control = new FormControl('', Validators.required);
|
||||
const formGroup = new FormGroup({login: control}, {updateOn: 'blur'});
|
||||
fixture.componentInstance.form = formGroup;
|
||||
fixture.detectChanges();
|
||||
|
||||
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
input.value = 'Nancy';
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(control.value).toEqual('', 'Expected value to remain unchanged until blur.');
|
||||
expect(control.valid).toBe(false, 'Expected no validation to occur until blur.');
|
||||
|
||||
dispatchEvent(input, 'blur');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(control.value)
|
||||
.toEqual('Nancy', 'Expected value to change once control is blurred.');
|
||||
expect(control.valid).toBe(true, 'Expected validation to run once control is blurred.');
|
||||
|
||||
});
|
||||
|
||||
it('should update on blur with array updateOn', () => {
|
||||
const fixture = initTest(FormArrayComp);
|
||||
const control = new FormControl('', Validators.required);
|
||||
const cityArray = new FormArray([control], {updateOn: 'blur'});
|
||||
const formGroup = new FormGroup({cities: cityArray});
|
||||
fixture.componentInstance.form = formGroup;
|
||||
fixture.componentInstance.cityArray = cityArray;
|
||||
fixture.detectChanges();
|
||||
|
||||
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
input.value = 'Nancy';
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(control.value).toEqual('', 'Expected value to remain unchanged until blur.');
|
||||
expect(control.valid).toBe(false, 'Expected no validation to occur until blur.');
|
||||
|
||||
dispatchEvent(input, 'blur');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(control.value)
|
||||
.toEqual('Nancy', 'Expected value to change once control is blurred.');
|
||||
expect(control.valid).toBe(true, 'Expected validation to run once control is blurred.');
|
||||
|
||||
});
|
||||
|
||||
|
||||
it('should allow child control updateOn blur to override group updateOn', () => {
|
||||
const fixture = initTest(NestedFormGroupComp);
|
||||
const loginControl =
|
||||
new FormControl('', {validators: Validators.required, updateOn: 'change'});
|
||||
const passwordControl = new FormControl('', Validators.required);
|
||||
const formGroup = new FormGroup(
|
||||
{signin: new FormGroup({login: loginControl, password: passwordControl})},
|
||||
{updateOn: 'blur'});
|
||||
fixture.componentInstance.form = formGroup;
|
||||
fixture.detectChanges();
|
||||
|
||||
const [loginInput, passwordInput] = fixture.debugElement.queryAll(By.css('input'));
|
||||
loginInput.nativeElement.value = 'Nancy';
|
||||
dispatchEvent(loginInput.nativeElement, 'input');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(loginControl.value).toEqual('Nancy', 'Expected value change on input.');
|
||||
expect(loginControl.valid).toBe(true, 'Expected validation to run on input.');
|
||||
|
||||
passwordInput.nativeElement.value = 'Carson';
|
||||
dispatchEvent(passwordInput.nativeElement, 'input');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(passwordControl.value)
|
||||
.toEqual('', 'Expected value to remain unchanged until blur.');
|
||||
expect(passwordControl.valid).toBe(false, 'Expected no validation to occur until blur.');
|
||||
|
||||
dispatchEvent(passwordInput.nativeElement, 'blur');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(passwordControl.value)
|
||||
.toEqual('Carson', 'Expected value to change once control is blurred.');
|
||||
expect(passwordControl.valid)
|
||||
.toBe(true, 'Expected validation to run once control is blurred.');
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
describe('on submit', () => {
|
||||
|
@ -1193,7 +1280,6 @@ export function main() {
|
|||
|
||||
});
|
||||
|
||||
|
||||
it('should mark as untouched properly if pending touched', () => {
|
||||
const fixture = initTest(FormGroupComp);
|
||||
const formGroup = new FormGroup({login: new FormControl('', {updateOn: 'submit'})});
|
||||
|
@ -1216,6 +1302,99 @@ export function main() {
|
|||
expect(formGroup.touched).toBe(false, 'Expected touched to stay false on submit.');
|
||||
});
|
||||
|
||||
it('should update on submit with group updateOn', () => {
|
||||
const fixture = initTest(FormGroupComp);
|
||||
const control = new FormControl('', Validators.required);
|
||||
const formGroup = new FormGroup({login: control}, {updateOn: 'submit'});
|
||||
fixture.componentInstance.form = formGroup;
|
||||
fixture.detectChanges();
|
||||
|
||||
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
input.value = 'Nancy';
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(control.value).toEqual('', 'Expected value to remain unchanged until submit.');
|
||||
expect(control.valid).toBe(false, 'Expected no validation to occur until submit.');
|
||||
|
||||
dispatchEvent(input, 'blur');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(control.value).toEqual('', 'Expected value to remain unchanged until submit.');
|
||||
expect(control.valid).toBe(false, 'Expected no validation to occur until submit.');
|
||||
|
||||
const form = fixture.debugElement.query(By.css('form')).nativeElement;
|
||||
dispatchEvent(form, 'submit');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(control.value).toEqual('Nancy', 'Expected value to change on submit.');
|
||||
expect(control.valid).toBe(true, 'Expected validation to run on submit.');
|
||||
|
||||
});
|
||||
|
||||
it('should update on submit with array updateOn', () => {
|
||||
const fixture = initTest(FormArrayComp);
|
||||
const control = new FormControl('', Validators.required);
|
||||
const cityArray = new FormArray([control], {updateOn: 'submit'});
|
||||
const formGroup = new FormGroup({cities: cityArray});
|
||||
fixture.componentInstance.form = formGroup;
|
||||
fixture.componentInstance.cityArray = cityArray;
|
||||
fixture.detectChanges();
|
||||
|
||||
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
input.value = 'Nancy';
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(control.value).toEqual('', 'Expected value to remain unchanged until submit.');
|
||||
expect(control.valid).toBe(false, 'Expected no validation to occur until submit.');
|
||||
|
||||
|
||||
const form = fixture.debugElement.query(By.css('form')).nativeElement;
|
||||
dispatchEvent(form, 'submit');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(control.value).toEqual('Nancy', 'Expected value to change once control on submit');
|
||||
expect(control.valid).toBe(true, 'Expected validation to run on submit.');
|
||||
|
||||
});
|
||||
|
||||
it('should allow child control updateOn submit to override group updateOn', () => {
|
||||
const fixture = initTest(NestedFormGroupComp);
|
||||
const loginControl =
|
||||
new FormControl('', {validators: Validators.required, updateOn: 'change'});
|
||||
const passwordControl = new FormControl('', Validators.required);
|
||||
const formGroup = new FormGroup(
|
||||
{signin: new FormGroup({login: loginControl, password: passwordControl})},
|
||||
{updateOn: 'submit'});
|
||||
fixture.componentInstance.form = formGroup;
|
||||
fixture.detectChanges();
|
||||
|
||||
const [loginInput, passwordInput] = fixture.debugElement.queryAll(By.css('input'));
|
||||
loginInput.nativeElement.value = 'Nancy';
|
||||
dispatchEvent(loginInput.nativeElement, 'input');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(loginControl.value).toEqual('Nancy', 'Expected value change on input.');
|
||||
expect(loginControl.valid).toBe(true, 'Expected validation to run on input.');
|
||||
|
||||
passwordInput.nativeElement.value = 'Carson';
|
||||
dispatchEvent(passwordInput.nativeElement, 'input');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(passwordControl.value)
|
||||
.toEqual('', 'Expected value to remain unchanged until submit.');
|
||||
expect(passwordControl.valid)
|
||||
.toBe(false, 'Expected no validation to occur until submit.');
|
||||
|
||||
const form = fixture.debugElement.query(By.css('form')).nativeElement;
|
||||
dispatchEvent(form, 'submit');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(passwordControl.value).toEqual('Carson', 'Expected value to change on submit.');
|
||||
expect(passwordControl.valid).toBe(true, 'Expected validation to run on submit.');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -2008,13 +2187,13 @@ class NestedFormGroupComp {
|
|||
@Component({
|
||||
selector: 'form-array-comp',
|
||||
template: `
|
||||
<div [formGroup]="form">
|
||||
<form [formGroup]="form">
|
||||
<div formArrayName="cities">
|
||||
<div *ngFor="let city of cityArray.controls; let i=index">
|
||||
<input [formControlName]="i">
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
</form>`
|
||||
})
|
||||
class FormArrayComp {
|
||||
form: FormGroup;
|
||||
|
|
|
@ -14,6 +14,7 @@ export declare abstract class AbstractControl {
|
|||
readonly statusChanges: Observable<any>;
|
||||
readonly touched: boolean;
|
||||
readonly untouched: boolean;
|
||||
readonly updateOn: FormHooks;
|
||||
readonly valid: boolean;
|
||||
validator: ValidatorFn | null;
|
||||
readonly value: any;
|
||||
|
|
Loading…
Reference in New Issue