diff --git a/goldens/public-api/forms/index.md b/goldens/public-api/forms/index.md index 5049b7a9700..f627125c5f4 100644 --- a/goldens/public-api/forms/index.md +++ b/goldens/public-api/forms/index.md @@ -565,6 +565,13 @@ export interface FormRecord { }): 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; } +// @public +export class FormSubmittedEvent extends ControlEvent { + constructor(source: AbstractControl); + // (undocumented) + readonly source: AbstractControl; +} + // @public export const isFormArray: (control: unknown) => control is FormArray; diff --git a/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json b/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json index 7041e87ca9c..33b5a398311 100644 --- a/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json +++ b/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json @@ -248,6 +248,12 @@ { "name": "FormRecord" }, + { + "name": "FormResetEvent" + }, + { + "name": "FormSubmittedEvent" + }, { "name": "FormsExampleModule" }, diff --git a/packages/forms/src/directives/reactive_directives/form_group_directive.ts b/packages/forms/src/directives/reactive_directives/form_group_directive.ts index 835191c914e..fe573bb0768 100644 --- a/packages/forms/src/directives/reactive_directives/form_group_directive.ts +++ b/packages/forms/src/directives/reactive_directives/form_group_directive.ts @@ -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).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).submitted = false; + this.form._events.next(new FormResetEvent(this.form)); } /** @internal */ diff --git a/packages/forms/src/forms.ts b/packages/forms/src/forms.ts index b07099b661f..e5d6880d402 100644 --- a/packages/forms/src/forms.ts +++ b/packages/forms/src/forms.ts @@ -74,6 +74,8 @@ export { AbstractControlOptions, ControlEvent, FormControlStatus, + FormResetEvent, + FormSubmittedEvent, PristineChangeEvent as PristineEvent, StatusChangeEvent as StatusEvent, TouchedChangeEvent as TouchedEvent, diff --git a/packages/forms/src/model/abstract_model.ts b/packages/forms/src/model/abstract_model.ts index 1c185c7c2f0..1b9659342b9 100644 --- a/packages/forms/src/model/abstract_model.ts +++ b/packages/forms/src/model/abstract_model.ts @@ -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>(); + readonly _events = new Subject>(); /** * A multicasting observable that emits an event every time the state of the control changes. diff --git a/packages/forms/test/reactive_integration_spec.ts b/packages/forms/test/reactive_integration_spec.ts index 7a674aafd96..63c12dbbdd3 100644 --- a/packages/forms/test/reactive_integration_spec.ts +++ b/packages/forms/test/reactive_integration_spec.ts @@ -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', () => {