fix(forms): Add event for forms submitted & reset (#55667)

This commit adds 2 new events to the unified control event observable.

PR Close #55667
This commit is contained in:
Matthieu Riegler 2024-05-04 04:30:21 +02:00 committed by Andrew Scott
parent c1915f19c6
commit fedeaac8ba
6 changed files with 89 additions and 2 deletions

View file

@ -565,6 +565,13 @@ export interface FormRecord<TControl> {
}): void;
}
// @public
export class FormResetEvent extends ControlEvent {
constructor(source: AbstractControl);
// (undocumented)
readonly source: AbstractControl;
}
// @public
export class FormsModule {
static withConfig(opts: {
@ -578,6 +585,13 @@ export class FormsModule {
static ɵmod: i0.ɵɵNgModuleDeclaration<FormsModule, [typeof i1_2.NgModel, typeof i2_2.NgModelGroup, typeof i3_2.NgForm], never, [typeof i4_2.ɵInternalFormsSharedModule, typeof i1_2.NgModel, typeof i2_2.NgModelGroup, typeof i3_2.NgForm]>;
}
// @public
export class FormSubmittedEvent extends ControlEvent {
constructor(source: AbstractControl);
// (undocumented)
readonly source: AbstractControl;
}
// @public
export const isFormArray: (control: unknown) => control is FormArray<any>;

View file

@ -248,6 +248,12 @@
{
"name": "FormRecord"
},
{
"name": "FormResetEvent"
},
{
"name": "FormSubmittedEvent"
},
{
"name": "FormsExampleModule"
},

View file

@ -45,6 +45,7 @@ import {AsyncValidator, AsyncValidatorFn, Validator, ValidatorFn} from '../valid
import {FormControlName} from './form_control_name';
import {FormArrayName, FormGroupName} from './form_group_name';
import {FormResetEvent, FormSubmittedEvent} from '../../model/abstract_model';
const formDirectiveProvider: Provider = {
provide: ControlContainer,
@ -302,6 +303,8 @@ export class FormGroupDirective extends ControlContainer implements Form, OnChan
(this as Writable<this>).submitted = true;
syncPendingControls(this.form, this.directives);
this.ngSubmit.emit($event);
this.form._events.next(new FormSubmittedEvent(this.control));
// Forms with `method="dialog"` have some special behavior that won't reload the page and that
// shouldn't be prevented. Note that we need to null check the `event` and the `target`, because
// some internal apps call this method directly with the wrong arguments.
@ -325,6 +328,7 @@ export class FormGroupDirective extends ControlContainer implements Form, OnChan
resetForm(value: any = undefined): void {
this.form.reset(value);
(this as Writable<this>).submitted = false;
this.form._events.next(new FormResetEvent(this.form));
}
/** @internal */

View file

@ -74,6 +74,8 @@ export {
AbstractControlOptions,
ControlEvent,
FormControlStatus,
FormResetEvent,
FormSubmittedEvent,
PristineChangeEvent as PristineEvent,
StatusChangeEvent as StatusEvent,
TouchedChangeEvent as TouchedEvent,

View file

@ -144,6 +144,27 @@ export class StatusChangeEvent extends ControlEvent {
}
}
/**
* Event fired when a form is submitted
*
* @publicApi
*/
export class FormSubmittedEvent extends ControlEvent {
constructor(public readonly source: AbstractControl) {
super();
}
}
/**
* Event fired when a form is reset.
*
* @publicApi
*/
export class FormResetEvent extends ControlEvent {
constructor(public readonly source: AbstractControl) {
super();
}
}
/**
* Gets validators from either an options object or given validators.
*/
@ -683,7 +704,7 @@ export abstract class AbstractControl<TValue = any, TRawValue extends TValue = T
*
* @internal
*/
private readonly _events = new Subject<ControlEvent<TValue>>();
readonly _events = new Subject<ControlEvent<TValue>>();
/**
* A multicasting observable that emits an event every time the state of the control changes.

View file

@ -29,7 +29,6 @@ import {
FormControl,
FormControlDirective,
FormControlName,
FormControlState,
FormGroup,
FormGroupDirective,
FormsModule,
@ -51,6 +50,8 @@ import {map, tap} from 'rxjs/operators';
import {
ControlEvent,
FormControlStatus,
FormResetEvent,
FormSubmittedEvent,
PristineChangeEvent,
StatusChangeEvent,
TouchedChangeEvent,
@ -1391,6 +1392,45 @@ describe('reactive forms integration tests', () => {
fc.markAsDirty({emitEvent: false});
expect(fcEvents.length).toBe(0);
});
it('formControl should emit an event when resetting a form', () => {
const fixture = initTest(FormGroupComp);
const form = new FormGroup({'login': new FormControl('', Validators.required)});
fixture.componentInstance.form = form;
fixture.detectChanges();
const formGroupDir = fixture.debugElement.children[0].injector.get(FormGroupDirective);
const events: ControlEvent[] = [];
fixture.componentInstance.form.events.subscribe((event) => events.push(event));
formGroupDir.resetForm();
expect(events.length).toBe(4);
expect(events[0]).toBeInstanceOf(TouchedChangeEvent);
expect(events[1]).toBeInstanceOf(ValueChangeEvent);
expect(events[2]).toBeInstanceOf(StatusChangeEvent);
// The event that matters
expect(events[3]).toBeInstanceOf(FormResetEvent);
expect(events[3].source).toBe(form);
});
it('formControl should emit an event when submitting a form', () => {
const fixture = initTest(FormGroupComp);
const form = new FormGroup({'login': new FormControl('', Validators.required)});
fixture.componentInstance.form = form;
fixture.detectChanges();
const formGroupDir = fixture.debugElement.children[0].injector.get(FormGroupDirective);
const events: ControlEvent[] = [];
fixture.componentInstance.form.events.subscribe((event) => events.push(event));
formGroupDir.onSubmit({} as any);
expect(events.length).toBe(1);
expect(events[0]).toBeInstanceOf(FormSubmittedEvent);
expect(events[0].source).toBe(form);
});
});
describe('setting status classes', () => {