diff --git a/goldens/public-api/forms/index.md b/goldens/public-api/forms/index.md index c981503a8045a..4947c856dca1c 100644 --- a/goldens/public-api/forms/index.md +++ b/goldens/public-api/forms/index.md @@ -41,6 +41,7 @@ export abstract class AbstractControl>; get

(path: P): AbstractControl<ɵGetProperty> | null; get

>(path: P): AbstractControl<ɵGetProperty> | null; getError(errorCode: string, path?: Array | string): any; @@ -49,9 +50,12 @@ export abstract class AbstractControl | string): boolean; hasValidator(validator: ValidatorFn): boolean; get invalid(): boolean; - markAllAsTouched(): void; + markAllAsTouched(opts?: { + emitEvent?: boolean; + }): void; markAsDirty(opts?: { onlySelf?: boolean; + emitEvent?: boolean; }): void; markAsPending(opts?: { onlySelf?: boolean; @@ -59,12 +63,15 @@ export abstract class AbstractControl { + abstract readonly source: AbstractControl; +} + // @public export interface ControlValueAccessor { registerOnChange(fn: any): void; @@ -769,6 +781,15 @@ export class PatternValidator extends AbstractValidatorDirective { static ɵfac: i0.ɵɵFactoryDeclaration; } +// @public +export class PristineEvent extends ControlEvent { + constructor(pristine: boolean, source: AbstractControl); + // (undocumented) + readonly pristine: boolean; + // (undocumented) + readonly source: AbstractControl; +} + // @public export class RadioControlValueAccessor extends BuiltInControlValueAccessor implements ControlValueAccessor, OnDestroy, OnInit { constructor(renderer: Renderer2, elementRef: ElementRef, _registry: RadioControlRegistry, _injector: Injector); @@ -854,6 +875,24 @@ export class SelectMultipleControlValueAccessor extends BuiltInControlValueAcces // @public export type SetDisabledStateOption = 'whenDisabledForLegacyCode' | 'always'; +// @public +export class StatusEvent extends ControlEvent { + constructor(status: FormControlStatus, source: AbstractControl); + // (undocumented) + readonly source: AbstractControl; + // (undocumented) + readonly status: FormControlStatus; +} + +// @public +export class TouchedEvent extends ControlEvent { + constructor(touched: boolean, source: AbstractControl); + // (undocumented) + readonly source: AbstractControl; + // (undocumented) + readonly touched: boolean; +} + // @public export type UntypedFormArray = FormArray; @@ -925,6 +964,15 @@ export class Validators { static requiredTrue(control: AbstractControl): ValidationErrors | null; } +// @public +export class ValueChangeEvent extends ControlEvent { + constructor(value: T, source: AbstractControl); + // (undocumented) + readonly source: AbstractControl; + // (undocumented) + readonly value: T; +} + // @public (undocumented) export const VERSION: Version; 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 0b47ce6cb8f00..5aef326b22028 100644 --- a/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json +++ b/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json @@ -134,6 +134,9 @@ { "name": "ControlContainer" }, + { + "name": "ControlEvent" + }, { "name": "DEFAULT_APP_ID" }, @@ -467,6 +470,9 @@ { "name": "PlatformRef" }, + { + "name": "PristineChangeEvent" + }, { "name": "R3Injector" }, @@ -536,6 +542,9 @@ { "name": "SkipSelf" }, + { + "name": "StatusChangeEvent" + }, { "name": "Subject" }, @@ -572,6 +581,9 @@ { "name": "TestabilityRegistry" }, + { + "name": "TouchedChangeEvent" + }, { "name": "USE_VALUE" }, @@ -584,6 +596,9 @@ { "name": "Validators" }, + { + "name": "ValueChangeEvent" + }, { "name": "ViewContainerRef" }, diff --git a/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json b/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json index 3fc05b49ab3d8..de9b70823f2e1 100644 --- a/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json +++ b/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json @@ -140,6 +140,9 @@ { "name": "ControlContainer" }, + { + "name": "ControlEvent" + }, { "name": "DEFAULT_APP_ID" }, @@ -458,6 +461,9 @@ { "name": "PlatformRef" }, + { + "name": "PristineChangeEvent" + }, { "name": "R3Injector" }, @@ -524,6 +530,9 @@ { "name": "SkipSelf" }, + { + "name": "StatusChangeEvent" + }, { "name": "Subject" }, @@ -566,6 +575,9 @@ { "name": "TestabilityRegistry" }, + { + "name": "TouchedChangeEvent" + }, { "name": "USE_VALUE" }, @@ -575,6 +587,9 @@ { "name": "VE_ViewContainerRef" }, + { + "name": "ValueChangeEvent" + }, { "name": "ViewContainerRef" }, diff --git a/packages/forms/src/forms.ts b/packages/forms/src/forms.ts index 84856652de057..40fa280e5f739 100644 --- a/packages/forms/src/forms.ts +++ b/packages/forms/src/forms.ts @@ -43,7 +43,7 @@ export {SelectMultipleControlValueAccessor, ɵNgSelectMultipleOption} from './di export {SetDisabledStateOption} from './directives/shared'; export {AsyncValidator, AsyncValidatorFn, CheckboxRequiredValidator, EmailValidator, MaxLengthValidator, MaxValidator, MinLengthValidator, MinValidator, PatternValidator, RequiredValidator, ValidationErrors, Validator, ValidatorFn} from './directives/validators'; export {ControlConfig, FormBuilder, NonNullableFormBuilder, UntypedFormBuilder, ɵElement} from './form_builder'; -export {AbstractControl, AbstractControlOptions, FormControlStatus, ɵCoerceStrArrToNumArr, ɵGetProperty, ɵNavigate, ɵRawValue, ɵTokenize, ɵTypedOrUntyped, ɵValue, ɵWriteable} from './model/abstract_model'; +export {AbstractControl, AbstractControlOptions, ControlEvent, FormControlStatus, PristineChangeEvent as PristineEvent, StatusChangeEvent as StatusEvent, TouchedChangeEvent as TouchedEvent, ValueChangeEvent, ɵCoerceStrArrToNumArr, ɵGetProperty, ɵNavigate, ɵRawValue, ɵTokenize, ɵTypedOrUntyped, ɵValue, ɵWriteable} from './model/abstract_model'; export {FormArray, isFormArray, UntypedFormArray, ɵFormArrayRawValue, ɵFormArrayValue} from './model/form_array'; export {FormControl, FormControlOptions, FormControlState, isFormControl, UntypedFormControl, ɵFormControlCtor} from './model/form_control'; export {FormGroup, FormRecord, isFormGroup, isFormRecord, UntypedFormGroup, ɵFormGroupRawValue, ɵFormGroupValue, ɵOptionalKeys} from './model/form_group'; diff --git a/packages/forms/src/model/abstract_model.ts b/packages/forms/src/model/abstract_model.ts index 2f0880eaecee3..a9904338499e4 100644 --- a/packages/forms/src/model/abstract_model.ts +++ b/packages/forms/src/model/abstract_model.ts @@ -7,7 +7,7 @@ */ import {EventEmitter, ɵRuntimeError as RuntimeError, ɵWritable as Writable} from '@angular/core'; -import {Observable} from 'rxjs'; +import {Observable, Subject} from 'rxjs'; import {asyncValidatorsDroppedWithOptsWarning, missingControlError, missingControlValueError, noControlsError} from '../directives/reactive_errors'; import {AsyncValidatorFn, ValidationErrors, ValidatorFn} from '../directives/validators'; @@ -65,6 +65,63 @@ export const DISABLED = 'DISABLED'; */ export type FormControlStatus = 'VALID'|'INVALID'|'PENDING'|'DISABLED'; + +/** + * Base class for every event sent by `AbstractControl.events()` + * + * @publicApi + */ +export abstract class ControlEvent { + /** + * Form control from which this event is originated. + */ + public abstract readonly source: AbstractControl; +} + +/** + * Event fired when the value of a control changes. + * + * @publicApi + */ +export class ValueChangeEvent extends ControlEvent { + constructor(public readonly value: T, public readonly source: AbstractControl) { + super(); + } +} + +/** + * Event fired when the control's pristine state changes (pristine <=> dirty). + * + * @publicApi + */ +export class PristineChangeEvent extends ControlEvent { + constructor(public readonly pristine: boolean, public readonly source: AbstractControl) { + super(); + } +} + +/** + * Event fired when the control's touched status changes (touched <=> untouched). + * + * @publicApi + */ +export class TouchedChangeEvent extends ControlEvent { + constructor(public readonly touched: boolean, public readonly source: AbstractControl) { + super(); + } +} + +/** + * Event fired when the control's status changes. + * + * @publicApi + */ +export class StatusChangeEvent extends ControlEvent { + constructor(public readonly status: FormControlStatus, public readonly source: AbstractControl) { + super(); + } +} + /** * Gets validators from either an options object or given validators. */ @@ -582,6 +639,27 @@ export abstract class AbstractControl>(); + + /** + * A multicasting observable that emits an event every time the state of the control changes. + * It emits for value, status, pristine or touched changes. + * + * **Note**: On value change, the emit happens right after a value of this control is updated. The + * value of a parent control (for example if this FormControl is a part of a FormGroup) is updated + * later, so accessing a value of a parent control (using the `value` property) from the callback + * of this event might result in getting a value that has not been updated yet. Subscribe to the + * `events` of the parent control instead. + * For other event types, the events are emitted after the parent control has been updated. + * + */ + public readonly events = this._events.asObservable(); + /** * A multicasting observable that emits an event every time the value of the control changes, in * the UI or programmatically. It also emits an event each time you call enable() or disable() @@ -592,6 +670,8 @@ export abstract class AbstractControl; @@ -602,6 +682,7 @@ export abstract class AbstractControl; @@ -796,11 +877,25 @@ export abstract class AbstractControl).touched = true; + const sourceControl = opts.sourceControl ?? this; if (this._parent && !opts.onlySelf) { - this._parent.markAsTouched(opts); + this._parent.markAsTouched({...opts, sourceControl}); + } + + if (changed && opts.emitEvent !== false) { + this._events.next(new TouchedChangeEvent(true, sourceControl)); } } @@ -808,10 +903,10 @@ export abstract class AbstractControl control.markAllAsTouched()); + this._forEachChild((control: AbstractControl) => control.markAllAsTouched(opts)); } /** @@ -829,16 +924,31 @@ export abstract class AbstractControl).touched = false; this._pendingTouched = false; + const sourceControl = opts.sourceControl ?? this; this._forEachChild((control: AbstractControl) => { - control.markAsUntouched({onlySelf: true}); + control.markAsUntouched({onlySelf: true, emitEvent: opts.emitEvent, sourceControl}); }); if (this._parent && !opts.onlySelf) { - this._parent._updateTouched(opts); + this._parent._updateTouched(opts, sourceControl); + } + + if (changed && opts.emitEvent !== false) { + this._events.next(new TouchedChangeEvent(false, sourceControl)); } } @@ -854,12 +964,29 @@ export abstract class AbstractControl).pristine = false; + const sourceControl = opts.sourceControl ?? this; if (this._parent && !opts.onlySelf) { - this._parent.markAsDirty(opts); + this._parent.markAsDirty({...opts, sourceControl}); + } + + if (changed && opts.emitEvent !== false) { + this._events.next(new PristineChangeEvent(false, sourceControl)); } } @@ -878,17 +1005,35 @@ export abstract class AbstractControl).pristine = true; this._pendingDirty = false; + const sourceControl = opts.sourceControl ?? this; this._forEachChild((control: AbstractControl) => { - control.markAsPristine({onlySelf: true}); + /** We don't propagate the source control downwards */ + control.markAsPristine({onlySelf: true, emitEvent: opts.emitEvent}); }); if (this._parent && !opts.onlySelf) { - this._parent._updatePristine(opts); + this._parent._updatePristine(opts, sourceControl); + } + + if (changed && opts.emitEvent !== false) { + this._events.next(new PristineChangeEvent(true, sourceControl)); } } @@ -908,15 +1053,25 @@ export abstract class AbstractControl).status = PENDING; + const sourceControl = opts.sourceControl ?? this; if (opts.emitEvent !== false) { + this._events.next(new StatusChangeEvent(this.status, sourceControl)); (this.statusChanges as EventEmitter).emit(this.status); } if (this._parent && !opts.onlySelf) { - this._parent.markAsPending(opts); + this._parent.markAsPending({...opts, sourceControl}); } } @@ -937,7 +1092,13 @@ export abstract class AbstractControl).status = DISABLED; (this as Writable).errors = null; this._forEachChild((control: AbstractControl) => { + /** We don't propagate the source control downwards */ control.disable({...opts, onlySelf: true}); }); this._updateValue(); + const sourceControl = opts.sourceControl ?? this; if (opts.emitEvent !== false) { + this._events.next(new StatusChangeEvent(this.status, sourceControl)); + this._events.next(new ValueChangeEvent(this.value, sourceControl)); (this.valueChanges as EventEmitter).emit(this.value); (this.statusChanges as EventEmitter).emit(this.status); } - this._updateAncestors({...opts, skipPristineCheck}); + this._updateAncestors({...opts, skipPristineCheck}, this); this._onDisabledChange.forEach((changeFn) => changeFn(true)); } @@ -987,18 +1152,19 @@ export abstract class AbstractControl changeFn(false)); } private _updateAncestors( - opts: {onlySelf?: boolean, emitEvent?: boolean, skipPristineCheck?: boolean}): void { + opts: {onlySelf?: boolean, emitEvent?: boolean, skipPristineCheck?: boolean}, + sourceControl: AbstractControl): void { if (this._parent && !opts.onlySelf) { this._parent.updateValueAndValidity(opts); if (!opts.skipPristineCheck) { - this._parent._updatePristine(); + this._parent._updatePristine({}, sourceControl); } - this._parent._updateTouched(); + this._parent._updateTouched({}, sourceControl); } } @@ -1048,7 +1214,14 @@ export abstract class AbstractControl(this.value, sourceControl)); + this._events.next(new StatusChangeEvent(this.status, sourceControl)); (this.valueChanges as EventEmitter).emit(this.value); (this.statusChanges as EventEmitter).emit(this.status); } if (this._parent && !opts.onlySelf) { - this._parent.updateValueAndValidity(opts); + this._parent.updateValueAndValidity({...opts, sourceControl}); } } @@ -1138,7 +1314,7 @@ export abstract class AbstractControl).errors = errors; - this._updateControlsErrors(opts.emitEvent !== false); + this._updateControlsErrors(opts.emitEvent !== false, this); } /** @@ -1278,15 +1454,16 @@ export abstract class AbstractControl).status = this._calculateStatus(); if (emitEvent) { (this.statusChanges as EventEmitter).emit(this.status); + this._events.next(new StatusChangeEvent(this.status, changedControl)); } if (this._parent) { - this._parent._updateControlsErrors(emitEvent); + this._parent._updateControlsErrors(emitEvent, changedControl); } } @@ -1336,20 +1513,27 @@ export abstract class AbstractControl).pristine = !this._anyControlsDirty(); + _updatePristine(opts: {onlySelf?: boolean}, changedControl: AbstractControl): void { + const newPristine = !this._anyControlsDirty(); + const changed = this.pristine !== newPristine; + (this as Writable).pristine = newPristine; if (this._parent && !opts.onlySelf) { - this._parent._updatePristine(opts); + this._parent._updatePristine(opts, changedControl); + } + + if (changed) { + this._events.next(new PristineChangeEvent(this.pristine, changedControl)); } } /** @internal */ - _updateTouched(opts: {onlySelf?: boolean} = {}): void { + _updateTouched(opts: {onlySelf?: boolean} = {}, changedControl: AbstractControl): void { (this as Writable).touched = this._anyControlsTouched(); + this._events.next(new TouchedChangeEvent(this.touched, changedControl)); if (this._parent && !opts.onlySelf) { - this._parent._updateTouched(opts); + this._parent._updateTouched(opts, changedControl); } } diff --git a/packages/forms/src/model/form_array.ts b/packages/forms/src/model/form_array.ts index 0ae09e822795b..68a8d72af8070 100644 --- a/packages/forms/src/model/form_array.ts +++ b/packages/forms/src/model/form_array.ts @@ -394,8 +394,8 @@ export class FormArray = any> extends Abst this._forEachChild((control: AbstractControl, index: number) => { control.reset(value[index], {onlySelf: true, emitEvent: options.emitEvent}); }); - this._updatePristine(options); - this._updateTouched(options); + this._updatePristine(options, this); + this._updateTouched(options, this); this.updateValueAndValidity(options); } diff --git a/packages/forms/src/model/form_group.ts b/packages/forms/src/model/form_group.ts index 30c55930cf7c6..6f6108f89c6f9 100644 --- a/packages/forms/src/model/form_group.ts +++ b/packages/forms/src/model/form_group.ts @@ -490,8 +490,8 @@ export class FormGroup { }); }); + describe('unified form state change', () => { + type UnwrapObservable = T extends Observable? U : never; + type ControlEvents = UnwrapObservable[]; + + function expectValueChangeEvent( + event: ControlEvent|undefined, value: T, sourceControl: AbstractControl) { + const valueEvent = event as ValueChangeEvent; + expect(valueEvent).toBeInstanceOf(ValueChangeEvent); + expect(valueEvent.source).toBe(sourceControl); + expect(valueEvent.value).toEqual(value); + } + + function expectPristineChangeEvent( + event: ControlEvent|undefined, pristine: boolean, sourceControl: AbstractControl) { + const pristineEvent = event as PristineChangeEvent; + expect(pristineEvent).toBeInstanceOf(PristineChangeEvent); + expect(pristineEvent.source).toBe(sourceControl); + expect(pristineEvent.pristine).toBe(pristine); + } + + function expectTouchedChangeEvent( + event: ControlEvent|undefined, touched: boolean, sourceControl: AbstractControl) { + const touchedEvent = event as TouchedChangeEvent; + expect(touchedEvent).toBeInstanceOf(TouchedChangeEvent); + expect(touchedEvent.source).toBe(sourceControl); + expect(touchedEvent.touched).toBe(touched); + } + + function expectStatusChangeEvent( + event: ControlEvent|undefined, status: FormControlStatus, + sourceControl: AbstractControl) { + const touchedEvent = event as StatusChangeEvent; + expect(touchedEvent).toBeInstanceOf(StatusChangeEvent); + expect(touchedEvent.source).toBe(sourceControl); + expect(touchedEvent.status).toBe(status); + } + + it('Single level Control should emit changes for itself', () => { + const fc = new FormControl('foo', Validators.required); + + const values: ControlEvent[] = []; + fc.events.subscribe(event => values.push(event)); + expect(values.length).toBe(0); + + fc.markAsTouched(); + expectTouchedChangeEvent(values.at(-1), true, fc); + + fc.markAsUntouched(); + expectTouchedChangeEvent(values.at(-1), false, fc); + + fc.markAsDirty(); + expectPristineChangeEvent(values.at(-1), false, fc); + + fc.markAsPristine(); + expectPristineChangeEvent(values.at(-1), true, fc); + + fc.disable(); + expectStatusChangeEvent(values.at(-2), 'DISABLED', fc); + expectValueChangeEvent(values.at(-1), 'foo', fc); + expect(values.length).toBe(6); + + fc.enable(); + expectStatusChangeEvent(values.at(-1), 'VALID', fc); + expectValueChangeEvent(values.at(-2), 'foo', fc); + expect(values.length).toBe(8); + + fc.setValue(null); + expectStatusChangeEvent(values.at(-1), 'INVALID', fc); + expectValueChangeEvent(values.at(-2), null, fc); + expect(values.length).toBe(10); // setValue doesnt emit dirty or touched + + fc.setValue('bar'); + expectValueChangeEvent(values.at(-2), 'bar', fc); + expectStatusChangeEvent(values.at(-1), 'VALID', fc); + expect(values.length).toBe(12); + + const subject = new Subject(); + const asyncValidator = () => subject.pipe(map(() => null)); + fc.addAsyncValidators(asyncValidator); + fc.updateValueAndValidity(); + + // Value is emitted because of updateValueAndValidity + expectValueChangeEvent(values.at(-2), 'bar', fc); + expectStatusChangeEvent(values.at(-1), 'PENDING', fc); + + subject.next(''); + subject.complete(); + expectStatusChangeEvent(values.at(-1), 'VALID', fc); + expect(values.length).toBe(15); + }); + + it('should not emit twice the same value', () => { + const fc = new FormControl('foo', Validators.required); + const fcEvents: ControlEvent[] = []; + fc.events.subscribe(event => fcEvents.push(event)); + expect(fcEvents.length).toBe(0); + + fc.markAsDirty(); + fc.markAsDirty(); + expect(fcEvents.length).toBe(1); + + fc.markAsPristine(); + fc.markAsPristine(); + expect(fcEvents.length).toBe(2); + + fc.markAsTouched(); + fc.markAsTouched(); + expect(fcEvents.length).toBe(3); + + fc.markAsUntouched(); + fc.markAsUntouched(); + expect(fcEvents.length).toBe(4); + }); + + it('Nested formControl should emit changes for itself and its parent', () => { + const fc1 = new FormControl('foo', Validators.required); + const fc2 = new FormControl('bar', Validators.required); + const fg = new FormGroup({fc1, fc2}); + + const fc1Events: ControlEvent[] = []; + const fc2Events: ControlEvent[] = []; + const fgEvents: ControlEvent>[] = []; + fc1.events.subscribe(event => fc1Events.push(event)); + fc2.events.subscribe(event => fc2Events.push(event)); + fg.events.subscribe(event => fgEvents.push(event)); + + fc1.setValue('bar'); + expectValueChangeEvent(fc1Events.at(-2), 'bar', fc1); + expectStatusChangeEvent(fc1Events.at(-1), 'VALID', fc1); + expect(fc1Events.length).toBe(2); + + expectValueChangeEvent(fgEvents.at(-2), {fc1: 'bar', fc2: 'bar'}, fc1); + expectStatusChangeEvent(fgEvents.at(-1), 'VALID', fc1); + }); + + it('Nested formControl children should emit pristine', () => { + const fc1 = new FormControl('foo', Validators.required); + const fc2 = new FormControl('bar', Validators.required); + const fg = new FormGroup({fc1, fc2}); + + const fc1Event: ControlEvents = []; + const fc2Event: ControlEvents = []; + const fgEvent: ControlEvents = []; + fc1.events.subscribe(event => fc1Event.push(event)); + fc2.events.subscribe(event => fc2Event.push(event)); + fg.events.subscribe(event => fgEvent.push(event)); + + fc1.markAsDirty(); + expect(fc1Event.length).toBe(1); + expect(fc2Event.length).toBe(0); + expect(fgEvent.length).toBe(1); + + fg.markAsPristine(); + + // Marking children as pristine does not emit an event on the parent + // Source control is itself + expectPristineChangeEvent(fgEvent.at(-1), true, fg); + expect(fgEvent.length).toBe(2); + + // Child that was dirty emits a pristine change + // Source control is itself, as even are only bubbled up + expectPristineChangeEvent(fc1Event.at(-1), true, fc1); + expect(fc1Event.length).toBe(2); + + // This child was already pristine, it doesn't emit a pristine change + expect(fc2Event.length).toBe(0); + }); + + it('Nested formControl should emit dirty', () => { + const fc1 = new FormControl('foo', Validators.required); + const fc2 = new FormControl('bar', Validators.required); + const fg = new FormGroup({fc1, fc2}); + + const fc1Events: ControlEvents = []; + const fc2Events: ControlEvents = []; + const fgEvents: ControlEvents = []; + fc1.events.subscribe(event => fc1Events.push(event)); + fc2.events.subscribe(event => fc2Events.push(event)); + fg.events.subscribe(event => fgEvents.push(event)); + + fc1.markAsDirty(); + expect(fc1Events.length).toBe(1); + expect(fgEvents.length).toBe(1); + expect(fc2Events.length).toBe(0); + + // sourceControl is the child control + expectPristineChangeEvent(fgEvents.at(-1), false, fc1); + expectPristineChangeEvent(fc1Events.at(-1), false, fc1); + }); + + it('Nested formControl should emit touched', () => { + const fc1 = new FormControl('foo', Validators.required); + const fc2 = new FormControl('bar', Validators.required); + const fg = new FormGroup({fc1, fc2}); + + const fc1Events: ControlEvent[] = []; + const fc2Events: ControlEvent[] = []; + const fgEvents: ControlEvent[] = []; + fc1.events.subscribe(event => fc1Events.push(event)); + fc2.events.subscribe(event => fc2Events.push(event)); + fg.events.subscribe(event => fgEvents.push(event)); + + fc1.markAsTouched(); + expect(fc1Events.length).toBe(1); + expect(fgEvents.length).toBe(1); + expect(fc2Events.length).toBe(0); + + // sourceControl is the child control + expectTouchedChangeEvent(fgEvents.at(-1), true, fc1); + expectTouchedChangeEvent(fc1Events.at(-1), true, fc1); + }); + + it('Nested formControl should emit disabled', () => { + const fc1 = new FormControl('foo', Validators.required); + const fc2 = new FormControl('bar', Validators.required); + const fg = new FormGroup({fc1, fc2}); + + const fc1Events: ControlEvent[] = []; + const fc2Events: ControlEvent[] = []; + const fgEvents: ControlEvent[] = []; + fc1.events.subscribe(event => fc1Events.push(event)); + fc2.events.subscribe(event => fc2Events.push(event)); + fg.events.subscribe(event => fgEvents.push(event)); + + expect(fg.pristine).toBeTrue(); + + fc1.disable(); + expect(fc1Events.length).toBe(2); + expect(fgEvents.length).toBe(3); + expect(fc2Events.length).toBe(0); + + expectValueChangeEvent(fgEvents.at(-3), {fc2: 'bar'}, fg); + expectStatusChangeEvent(fgEvents.at(-2), 'VALID', fg); + expectTouchedChangeEvent(fgEvents.at(-1), false, fc1); + // Not prisitine event sent as fg was already pristine + + expectStatusChangeEvent(fc1Events.at(-2), 'DISABLED', fc1); + expectValueChangeEvent(fc1Events.at(-1), 'foo', fc1); + }); + + it('Nested formControl should emit PENDING', () => { + const fc1 = new FormControl('foo', Validators.required); + const fc2 = new FormControl('bar', Validators.required); + const fg = new FormGroup({fc1, fc2}); + + const fc1Events: ControlEvent[] = []; + const fc2Events: ControlEvent[] = []; + const fgEvents: ControlEvent[] = []; + fc1.events.subscribe(event => fc1Events.push(event)); + fc2.events.subscribe(event => fc2Events.push(event)); + fg.events.subscribe(event => fgEvents.push(event)); + + fc1.markAsPending(); + expectStatusChangeEvent(fgEvents.at(-1), 'PENDING', fc1); + expectStatusChangeEvent(fc1Events.at(-1), 'PENDING', fc1); + + expect(fc1Events.length).toBe(1); + expect(fgEvents.length).toBe(1); + expect(fc2Events.length).toBe(0); + + + // Reseting to VALID + fc1.updateValueAndValidity(); + expectValueChangeEvent(fgEvents.at(-2), {fc1: 'foo', fc2: 'bar'}, fc1); + expectStatusChangeEvent(fgEvents.at(-1), 'VALID', fc1); + + expectValueChangeEvent(fc1Events.at(-2), 'foo', fc1); + expectStatusChangeEvent(fc1Events.at(-1), 'VALID', fc1); + expect(fc1Events.length).toBe(3); + expect(fgEvents.length).toBe(3); + expect(fc2Events.length).toBe(0); + + // Triggering PENDING again with the async validator now + const subject = new Subject(); + const asyncValidator = () => subject.pipe(map(() => null)); + fc1.addAsyncValidators(asyncValidator); + fc1.updateValueAndValidity(); + + expect(fc1Events.length).toBe(5); + expect(fgEvents.length).toBe(5); + expect(fc2Events.length).toBe(0); + + expectValueChangeEvent(fgEvents.at(-2), {fc1: 'foo', fc2: 'bar'}, fc1); + expectStatusChangeEvent(fgEvents.at(-1), 'PENDING', fc1); + expectValueChangeEvent(fc1Events.at(-2), 'foo', fc1); + expectStatusChangeEvent(fc1Events.at(-1), 'PENDING', fc1); + + subject.next(''); + subject.complete(); + expectStatusChangeEvent(fgEvents.at(-1), 'VALID', fc1); + expectStatusChangeEvent(fc1Events.at(-1), 'VALID', fc1); + }); + + it('formContorl should not emit when emitEvent is false', () => { + const fc = new FormControl('foo', Validators.required); + const fcEvents: ControlEvent[] = []; + fc.events.subscribe(event => fcEvents.push(event)); + + expect(fcEvents.length).toBe(0); + fc.markAsTouched({emitEvent: false}); + expect(fcEvents.length).toBe(0); + fc.markAllAsTouched({emitEvent: false}); + expect(fcEvents.length).toBe(0); + fc.markAsUntouched({emitEvent: false}); + expect(fcEvents.length).toBe(0); + fc.markAsPristine({emitEvent: false}); + expect(fcEvents.length).toBe(0); + fc.markAsDirty({emitEvent: false}); + expect(fcEvents.length).toBe(0); + }); + }); + describe('setting status classes', () => { it('should not assign status on standalone

element', () => { @Component({