Skip to content

Commit f69561b

Browse files
karavicb
authored andcommitted
feat(forms): add updateOn submit option to FormControls (angular#18514)
1 parent 685cc26 commit f69561b

File tree

4 files changed

+411
-20
lines changed

4 files changed

+411
-20
lines changed

packages/forms/src/directives/reactive_directives/form_group_directive.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ export class FormGroupDirective extends ControlContainer implements Form,
134134

135135
onSubmit($event: Event): boolean {
136136
this._submitted = true;
137+
this._syncPendingControls();
137138
this.ngSubmit.emit($event);
138139
return false;
139140
}
@@ -145,6 +146,16 @@ export class FormGroupDirective extends ControlContainer implements Form,
145146
this._submitted = false;
146147
}
147148

149+
/** @internal */
150+
_syncPendingControls() {
151+
this.form._syncPendingControls();
152+
this.directives.forEach(dir => {
153+
if (dir.control._updateOn === 'submit') {
154+
dir.viewToModelUpdate(dir.control._pendingValue);
155+
}
156+
});
157+
}
158+
148159
/** @internal */
149160
_updateDomValue() {
150161
this.directives.forEach(dir => {

packages/forms/src/directives/shared.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -84,25 +84,25 @@ function setUpViewChangePipeline(control: FormControl, dir: NgControl): void {
8484
control._pendingValue = newValue;
8585
control._pendingDirty = true;
8686

87-
if (control._updateOn === 'change') {
88-
dir.viewToModelUpdate(newValue);
89-
control.markAsDirty();
90-
control.setValue(newValue, {emitModelToViewChange: false});
91-
}
87+
if (control._updateOn === 'change') updateControl(control, dir);
9288
});
9389
}
9490

9591
function setUpBlurPipeline(control: FormControl, dir: NgControl): void {
9692
dir.valueAccessor !.registerOnTouched(() => {
97-
if (control._updateOn === 'blur') {
98-
dir.viewToModelUpdate(control._pendingValue);
99-
if (control._pendingDirty) control.markAsDirty();
100-
control.setValue(control._pendingValue, {emitModelToViewChange: false});
101-
}
102-
control.markAsTouched();
93+
control._pendingTouched = true;
94+
95+
if (control._updateOn === 'blur') updateControl(control, dir);
96+
if (control._updateOn !== 'submit') control.markAsTouched();
10397
});
10498
}
10599

100+
function updateControl(control: FormControl, dir: NgControl): void {
101+
dir.viewToModelUpdate(control._pendingValue);
102+
if (control._pendingDirty) control.markAsDirty();
103+
control.setValue(control._pendingValue, {emitModelToViewChange: false});
104+
}
105+
106106
function setUpModelChangePipeline(control: FormControl, dir: NgControl): void {
107107
control.registerOnChange((newValue: any, emitModelEvent: boolean) => {
108108
// control -> view

packages/forms/src/model.ts

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ function coerceToAsyncValidator(
7878
origAsyncValidator || null;
7979
}
8080

81-
export type FormHooks = 'change' | 'blur';
81+
export type FormHooks = 'change' | 'blur' | 'submit';
8282

8383
export interface AbstractControlOptions {
8484
validators?: ValidatorFn|ValidatorFn[]|null;
@@ -108,6 +108,13 @@ function isOptionsObj(
108108
export abstract class AbstractControl {
109109
/** @internal */
110110
_value: any;
111+
112+
/** @internal */
113+
_pendingDirty: boolean;
114+
115+
/** @internal */
116+
_pendingTouched: boolean;
117+
111118
/** @internal */
112119
_onCollectionChange = () => {};
113120

@@ -284,6 +291,7 @@ export abstract class AbstractControl {
284291
*/
285292
markAsUntouched(opts: {onlySelf?: boolean} = {}): void {
286293
this._touched = false;
294+
this._pendingTouched = false;
287295

288296
this._forEachChild(
289297
(control: AbstractControl) => { control.markAsUntouched({onlySelf: true}); });
@@ -316,6 +324,7 @@ export abstract class AbstractControl {
316324
*/
317325
markAsPristine(opts: {onlySelf?: boolean} = {}): void {
318326
this._pristine = true;
327+
this._pendingDirty = false;
319328

320329
this._forEachChild((control: AbstractControl) => { control.markAsPristine({onlySelf: true}); });
321330

@@ -568,6 +577,9 @@ export abstract class AbstractControl {
568577
/** @internal */
569578
abstract _allControlsDisabled(): boolean;
570579

580+
/** @internal */
581+
abstract _syncPendingControls(): boolean;
582+
571583
/** @internal */
572584
_anyControlsHaveStatus(status: string): boolean {
573585
return this._anyControls((control: AbstractControl) => control.status === status);
@@ -672,6 +684,9 @@ export abstract class AbstractControl {
672684
* const c = new FormControl('', { updateOn: 'blur' });
673685
* ```
674686
*
687+
* You can also set `updateOn` to `'submit'`, which will delay value and validity
688+
* updates until the parent form of the control fires a submit event.
689+
*
675690
* See its superclass, {@link AbstractControl}, for more properties and methods.
676691
*
677692
* * **npm package**: `@angular/forms`
@@ -688,9 +703,6 @@ export class FormControl extends AbstractControl {
688703
/** @internal */
689704
_pendingValue: any;
690705

691-
/** @internal */
692-
_pendingDirty: boolean;
693-
694706
constructor(
695707
formState: any = null,
696708
validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null,
@@ -782,7 +794,6 @@ export class FormControl extends AbstractControl {
782794
reset(formState: any = null, options: {onlySelf?: boolean, emitEvent?: boolean} = {}): void {
783795
this._applyFormState(formState);
784796
this.markAsPristine(options);
785-
this._pendingDirty = false;
786797
this.markAsUntouched(options);
787798
this.setValue(this._value, options);
788799
}
@@ -828,6 +839,17 @@ export class FormControl extends AbstractControl {
828839
*/
829840
_forEachChild(cb: Function): void {}
830841

842+
/** @internal */
843+
_syncPendingControls(): boolean {
844+
if (this._updateOn === 'submit') {
845+
this.setValue(this._pendingValue, {onlySelf: true, emitModelToViewChange: false});
846+
if (this._pendingDirty) this.markAsDirty();
847+
if (this._pendingTouched) this.markAsTouched();
848+
return true;
849+
}
850+
return false;
851+
}
852+
831853
private _applyFormState(formState: any) {
832854
if (this._isBoxedValue(formState)) {
833855
this._value = this._pendingValue = formState.value;
@@ -1092,6 +1114,15 @@ export class FormGroup extends AbstractControl {
10921114
});
10931115
}
10941116

1117+
/** @internal */
1118+
_syncPendingControls(): boolean {
1119+
let subtreeUpdated = this._reduceChildren(false, (updated: boolean, child: AbstractControl) => {
1120+
return child._syncPendingControls() ? true : updated;
1121+
});
1122+
if (subtreeUpdated) this.updateValueAndValidity({onlySelf: true});
1123+
return subtreeUpdated;
1124+
}
1125+
10951126
/** @internal */
10961127
_throwIfControlMissing(name: string): void {
10971128
if (!Object.keys(this.controls).length) {
@@ -1404,6 +1435,15 @@ export class FormArray extends AbstractControl {
14041435
});
14051436
}
14061437

1438+
/** @internal */
1439+
_syncPendingControls(): boolean {
1440+
let subtreeUpdated = this.controls.reduce((updated: boolean, child: AbstractControl) => {
1441+
return child._syncPendingControls() ? true : updated;
1442+
}, false);
1443+
if (subtreeUpdated) this.updateValueAndValidity({onlySelf: true});
1444+
return subtreeUpdated;
1445+
}
1446+
14071447
/** @internal */
14081448
_throwIfControlMissing(index: number): void {
14091449
if (!this.controls.length) {

0 commit comments

Comments
 (0)