diff --git a/packages/forms/src/model.ts b/packages/forms/src/model.ts index e4c1665ecf..c8ee0d5d3b 100644 --- a/packages/forms/src/model.ts +++ b/packages/forms/src/model.ts @@ -495,6 +495,10 @@ export abstract class AbstractControl { * When false, no events are emitted. */ disable(opts: {onlySelf?: boolean, emitEvent?: boolean} = {}): void { + // If parent has been marked artificially dirty we don't want to re-calculate the + // parent's dirtiness based on the children. + const skipPristineCheck = this._parentMarkedDirty(opts.onlySelf); + (this as{status: string}).status = DISABLED; (this as{errors: ValidationErrors | null}).errors = null; this._forEachChild( @@ -506,7 +510,7 @@ export abstract class AbstractControl { (this.statusChanges as EventEmitter).emit(this.status); } - this._updateAncestors(opts); + this._updateAncestors({...opts, skipPristineCheck}); this._onDisabledChange.forEach((changeFn) => changeFn(true)); } @@ -529,19 +533,26 @@ export abstract class AbstractControl { * When false, no events are emitted. */ enable(opts: {onlySelf?: boolean, emitEvent?: boolean} = {}): void { + // If parent has been marked artificially dirty we don't want to re-calculate the + // parent's dirtiness based on the children. + const skipPristineCheck = this._parentMarkedDirty(opts.onlySelf); + (this as{status: string}).status = VALID; this._forEachChild( (control: AbstractControl) => { control.enable({...opts, onlySelf: true}); }); this.updateValueAndValidity({onlySelf: true, emitEvent: opts.emitEvent}); - this._updateAncestors(opts); + this._updateAncestors({...opts, skipPristineCheck}); this._onDisabledChange.forEach((changeFn) => changeFn(false)); } - private _updateAncestors(opts: {onlySelf?: boolean, emitEvent?: boolean}) { + private _updateAncestors( + opts: {onlySelf?: boolean, emitEvent?: boolean, skipPristineCheck?: boolean}) { if (this._parent && !opts.onlySelf) { this._parent.updateValueAndValidity(opts); - this._parent._updatePristine(); + if (!opts.skipPristineCheck) { + this._parent._updatePristine(); + } this._parent._updateTouched(); } } @@ -852,6 +863,16 @@ export abstract class AbstractControl { this._updateOn = (opts as AbstractControlOptions).updateOn !; } } + + /** + * Check to see if parent has been marked artificially dirty. + * + * @internal + */ + private _parentMarkedDirty(onlySelf?: boolean): boolean { + const parentDirty = this._parent && this._parent.dirty; + return !onlySelf && parentDirty && !this._parent._anyControlsDirty(); + } } /** diff --git a/packages/forms/test/form_control_spec.ts b/packages/forms/test/form_control_spec.ts index c125700cd3..7b90a1e5a4 100644 --- a/packages/forms/test/form_control_spec.ts +++ b/packages/forms/test/form_control_spec.ts @@ -1000,6 +1000,37 @@ import {FormArray} from '@angular/forms/src/model'; expect(a.value).toEqual(['one']); }); + it('should ignore disabled array controls when determining dirtiness', () => { + const c = new FormControl('one'); + const c2 = new FormControl('two'); + const a = new FormArray([c, c2]); + c.markAsDirty(); + expect(a.dirty).toBe(true); + + c.disable(); + expect(c.dirty).toBe(true); + expect(a.dirty).toBe(false); + + c.enable(); + expect(a.dirty).toBe(true); + }); + + it('should not make a dirty array not dirty when disabling controls', () => { + const c = new FormControl('one'); + const c2 = new FormControl('two'); + const a = new FormArray([c, c2]); + + a.markAsDirty(); + expect(a.dirty).toBe(true); + expect(c.dirty).toBe(false); + + c.disable(); + expect(a.dirty).toBe(true); + + c.enable(); + expect(a.dirty).toBe(true); + }); + it('should ignore disabled controls in validation', () => { const c = new FormControl(null, Validators.required); const c2 = new FormControl(null); @@ -1054,6 +1085,22 @@ import {FormArray} from '@angular/forms/src/model'; expect(g.dirty).toBe(true); }); + it('should not make a dirty group not dirty when disabling controls', () => { + const c = new FormControl('one'); + const c2 = new FormControl('two'); + const g = new FormGroup({one: c, two: c2}); + + g.markAsDirty(); + expect(g.dirty).toBe(true); + expect(c.dirty).toBe(false); + + c.disable(); + expect(g.dirty).toBe(true); + + c.enable(); + expect(g.dirty).toBe(true); + }); + it('should ignore disabled controls when determining touched state', () => { const c = new FormControl('one'); const c2 = new FormControl('two');