diff --git a/modules/@angular/forms/src/directives/abstract_control_directive.ts b/modules/@angular/forms/src/directives/abstract_control_directive.ts index a10adabcf9b71..d9db25bccb829 100644 --- a/modules/@angular/forms/src/directives/abstract_control_directive.ts +++ b/modules/@angular/forms/src/directives/abstract_control_directive.ts @@ -48,4 +48,8 @@ export abstract class AbstractControlDirective { } get path(): string[] { return null; } + + reset(value: any = undefined): void { + if (isPresent(this.control)) this.control.reset(value); + } } diff --git a/modules/@angular/forms/src/directives/ng_form.ts b/modules/@angular/forms/src/directives/ng_form.ts index 15f7afc6db879..48d69cd80331b 100644 --- a/modules/@angular/forms/src/directives/ng_form.ts +++ b/modules/@angular/forms/src/directives/ng_form.ts @@ -86,9 +86,7 @@ export const formDirectiveProvider: any = @Directive({ selector: 'form:not([ngNoForm]):not([formGroup]),ngForm,[ngForm]', providers: [formDirectiveProvider], - host: { - '(submit)': 'onSubmit()', - }, + host: {'(submit)': 'onSubmit()', '(reset)': 'onReset()'}, outputs: ['ngSubmit'], exportAs: 'ngForm' }) @@ -172,6 +170,8 @@ export class NgForm extends ControlContainer implements Form { return false; } + onReset(): void { this.form.reset(); } + /** @internal */ _findContainer(path: string[]): FormGroup { path.pop(); diff --git a/modules/@angular/forms/src/directives/ng_model.ts b/modules/@angular/forms/src/directives/ng_model.ts index 4df297a2b4f41..64df9d83b80d5 100644 --- a/modules/@angular/forms/src/directives/ng_model.ts +++ b/modules/@angular/forms/src/directives/ng_model.ts @@ -135,6 +135,7 @@ export class NgModel extends NgControl implements OnChanges, } private _updateValue(value: any): void { - PromiseWrapper.scheduleMicrotask(() => { this.control.updateValue(value); }); + PromiseWrapper.scheduleMicrotask( + () => { this.control.updateValue(value, {emitViewToModelChange: false}); }); } } diff --git a/modules/@angular/forms/src/directives/reactive_directives/form_group_directive.ts b/modules/@angular/forms/src/directives/reactive_directives/form_group_directive.ts index 96e7f49a5c315..13cca21f371f3 100644 --- a/modules/@angular/forms/src/directives/reactive_directives/form_group_directive.ts +++ b/modules/@angular/forms/src/directives/reactive_directives/form_group_directive.ts @@ -105,7 +105,7 @@ export const formDirectiveProvider: any = @Directive({ selector: '[formGroup]', providers: [formDirectiveProvider], - host: {'(submit)': 'onSubmit()'}, + host: {'(submit)': 'onSubmit()', '(reset)': 'onReset()'}, exportAs: 'ngForm' }) export class FormGroupDirective extends ControlContainer implements Form, @@ -187,6 +187,8 @@ export class FormGroupDirective extends ControlContainer implements Form, return false; } + onReset(): void { this.form.reset(); } + /** @internal */ _updateDomValue() { this.directives.forEach(dir => { diff --git a/modules/@angular/forms/src/directives/shared.ts b/modules/@angular/forms/src/directives/shared.ts index 67cc48e097782..3cd4c8046d59b 100644 --- a/modules/@angular/forms/src/directives/shared.ts +++ b/modules/@angular/forms/src/directives/shared.ts @@ -49,8 +49,13 @@ export function setUpControl(control: FormControl, dir: NgControl): void { control.markAsDirty(); }); - // model -> view - control.registerOnChange((newValue: any) => dir.valueAccessor.writeValue(newValue)); + control.registerOnChange((newValue: any, emitModelEvent: boolean) => { + // control -> view + dir.valueAccessor.writeValue(newValue); + + // control -> ngModel + if (emitModelEvent) dir.viewToModelUpdate(newValue); + }); // touched dir.valueAccessor.registerOnTouched(() => control.markAsTouched()); diff --git a/modules/@angular/forms/src/model.ts b/modules/@angular/forms/src/model.ts index fa99e02ef111a..cbbe12c222ee2 100644 --- a/modules/@angular/forms/src/model.ts +++ b/modules/@angular/forms/src/model.ts @@ -83,6 +83,7 @@ export abstract class AbstractControl { private _parent: FormGroup|FormArray; private _asyncValidationSubscription: any; + constructor(public validator: ValidatorFn, public asyncValidator: AsyncValidatorFn) {} get value(): any { return this._value; } @@ -140,6 +141,27 @@ export abstract class AbstractControl { } } + markAsPristine({onlySelf}: {onlySelf?: boolean} = {}): void { + this._pristine = true; + + this._forEachChild((control: AbstractControl) => { control.markAsPristine({onlySelf: true}); }); + + if (isPresent(this._parent) && !onlySelf) { + this._parent._updatePristine({onlySelf: onlySelf}); + } + } + + markAsUntouched({onlySelf}: {onlySelf?: boolean} = {}): void { + this._touched = false; + + this._forEachChild( + (control: AbstractControl) => { control.markAsUntouched({onlySelf: true}); }); + + if (isPresent(this._parent) && !onlySelf) { + this._parent._updateTouched({onlySelf: onlySelf}); + } + } + markAsPending({onlySelf}: {onlySelf?: boolean} = {}): void { onlySelf = normalizeBool(onlySelf); this._status = PENDING; @@ -153,6 +175,8 @@ export abstract class AbstractControl { abstract updateValue(value: any, options?: Object): void; + abstract reset(value?: any, options?: Object): void; + updateValueAndValidity({onlySelf, emitEvent}: {onlySelf?: boolean, emitEvent?: boolean} = {}): void { onlySelf = normalizeBool(onlySelf); @@ -283,7 +307,43 @@ export abstract class AbstractControl { abstract _updateValue(): void; /** @internal */ - abstract _anyControlsHaveStatus(status: string): boolean; + abstract _forEachChild(cb: Function): void; + + /** @internal */ + abstract _anyControls(condition: Function): boolean; + + /** @internal */ + _anyControlsHaveStatus(status: string): boolean { + return this._anyControls((control: AbstractControl) => control.status == status); + } + + /** @internal */ + _anyControlsDirty(): boolean { + return this._anyControls((control: AbstractControl) => control.dirty); + } + + /** @internal */ + _anyControlsTouched(): boolean { + return this._anyControls((control: AbstractControl) => control.touched); + } + + /** @internal */ + _updatePristine({onlySelf}: {onlySelf?: boolean} = {}): void { + this._pristine = !this._anyControlsDirty(); + + if (isPresent(this._parent) && !onlySelf) { + this._parent._updatePristine({onlySelf: onlySelf}); + } + } + + /** @internal */ + _updateTouched({onlySelf}: {onlySelf?: boolean} = {}): void { + this._touched = this._anyControlsTouched(); + + if (isPresent(this._parent) && !onlySelf) { + this._parent._updateTouched({onlySelf: onlySelf}); + } + } } /** @@ -328,20 +388,32 @@ export class FormControl extends AbstractControl { * If `emitModelToViewChange` is `true`, the view will be notified about the new value * via an `onChange` event. This is the default behavior if `emitModelToViewChange` is not * specified. + * + * If `emitViewToModelChange` is `true`, an ngModelChange event will be fired to update the + * model. This is the default behavior if `emitViewToModelChange` is not specified. */ - updateValue(value: any, {onlySelf, emitEvent, emitModelToViewChange}: { + updateValue(value: any, {onlySelf, emitEvent, emitModelToViewChange, emitViewToModelChange}: { onlySelf?: boolean, emitEvent?: boolean, - emitModelToViewChange?: boolean + emitModelToViewChange?: boolean, + emitViewToModelChange?: boolean } = {}): void { emitModelToViewChange = isPresent(emitModelToViewChange) ? emitModelToViewChange : true; + emitViewToModelChange = isPresent(emitViewToModelChange) ? emitViewToModelChange : true; + this._value = value; if (this._onChange.length && emitModelToViewChange) { - this._onChange.forEach((changeFn) => changeFn(this._value)); + this._onChange.forEach((changeFn) => changeFn(this._value, emitViewToModelChange)); } this.updateValueAndValidity({onlySelf: onlySelf, emitEvent: emitEvent}); } + reset(value: any = null, {onlySelf}: {onlySelf?: boolean} = {}): void { + this.updateValue(value, {onlySelf: onlySelf}); + this.markAsPristine({onlySelf: onlySelf}); + this.markAsUntouched({onlySelf: onlySelf}); + } + /** * @internal */ @@ -350,12 +422,17 @@ export class FormControl extends AbstractControl { /** * @internal */ - _anyControlsHaveStatus(status: string): boolean { return false; } + _anyControls(condition: Function): boolean { return false; } /** * Register a listener for change events. */ registerOnChange(fn: Function): void { this._onChange.push(fn); } + + /** + * @internal + */ + _forEachChild(cb: Function): void {} } /** @@ -445,6 +522,15 @@ export class FormGroup extends AbstractControl { this.updateValueAndValidity({onlySelf: onlySelf}); } + reset(value: any = {}, {onlySelf}: {onlySelf?: boolean} = {}): void { + this._forEachChild((control: AbstractControl, name: string) => { + control.reset(value[name], {onlySelf: true}); + }); + this.updateValueAndValidity({onlySelf: onlySelf}); + this._updatePristine({onlySelf: onlySelf}); + this._updateTouched({onlySelf: onlySelf}); + } + /** @internal */ _throwIfControlMissing(name: string): void { if (!this.controls[name]) { @@ -452,20 +538,22 @@ export class FormGroup extends AbstractControl { } } + /** @internal */ + _forEachChild(cb: Function): void { StringMapWrapper.forEach(this.controls, cb); } + /** @internal */ _setParentForControls() { - StringMapWrapper.forEach( - this.controls, (control: AbstractControl, name: string) => { control.setParent(this); }); + this._forEachChild((control: AbstractControl, name: string) => { control.setParent(this); }); } /** @internal */ _updateValue() { this._value = this._reduceValue(); } /** @internal */ - _anyControlsHaveStatus(status: string): boolean { + _anyControls(condition: Function): boolean { var res = false; - StringMapWrapper.forEach(this.controls, (control: AbstractControl, name: string) => { - res = res || (this.contains(name) && control.status == status); + this._forEachChild((control: AbstractControl, name: string) => { + res = res || (this.contains(name) && condition(control)); }); return res; } @@ -482,7 +570,7 @@ export class FormGroup extends AbstractControl { /** @internal */ _reduceChildren(initValue: any, fn: Function) { var res = initValue; - StringMapWrapper.forEach(this.controls, (control: AbstractControl, name: string) => { + this._forEachChild((control: AbstractControl, name: string) => { if (this._included(name)) { res = fn(res, control, name); } @@ -575,6 +663,15 @@ export class FormArray extends AbstractControl { this.updateValueAndValidity({onlySelf: onlySelf}); } + reset(value: any = [], {onlySelf}: {onlySelf?: boolean} = {}): void { + this._forEachChild((control: AbstractControl, index: number) => { + control.reset(value[index], {onlySelf: true}); + }); + this.updateValueAndValidity({onlySelf: onlySelf}); + this._updatePristine({onlySelf: onlySelf}); + this._updateTouched({onlySelf: onlySelf}); + } + /** @internal */ _throwIfControlMissing(index: number): void { if (!this.at(index)) { @@ -582,17 +679,21 @@ export class FormArray extends AbstractControl { } } + /** @internal */ + _forEachChild(cb: Function): void { + this.controls.forEach((control: AbstractControl, index: number) => { cb(control, index); }); + } + /** @internal */ _updateValue(): void { this._value = this.controls.map((control) => control.value); } /** @internal */ - _anyControlsHaveStatus(status: string): boolean { - return this.controls.some(c => c.status == status); + _anyControls(condition: Function): boolean { + return this.controls.some((control: AbstractControl) => condition(control)); } - /** @internal */ _setParentForControls(): void { - this.controls.forEach((control) => { control.setParent(this); }); + this._forEachChild((control: AbstractControl) => { control.setParent(this); }); } } diff --git a/modules/@angular/forms/test/integration_spec.ts b/modules/@angular/forms/test/integration_spec.ts index 739cdaa6ae330..26b2800851011 100644 --- a/modules/@angular/forms/test/integration_spec.ts +++ b/modules/@angular/forms/test/integration_spec.ts @@ -278,6 +278,59 @@ export function main() { }); })); + it('should clear value in UI when form resets programmatically', + inject( + [TestComponentBuilder, AsyncTestCompleter], + (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { + const login = new FormControl('oldValue'); + const form = new FormGroup({'login': login}); + + const t = `
+ +
`; + + tcb.overrideTemplate(MyComp8, t).createAsync(MyComp8).then((fixture) => { + fixture.debugElement.componentInstance.form = form; + fixture.detectChanges(); + + login.updateValue('new value'); + + const loginEl = fixture.debugElement.query(By.css('input')).nativeElement; + expect(loginEl.value).toBe('new value'); + + form.reset(); + expect(loginEl.value).toBe(''); + async.done(); + }); + })); + + it('should set value in UI when form resets to that value programmatically', + inject( + [TestComponentBuilder, AsyncTestCompleter], + (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { + const login = new FormControl('oldValue'); + const form = new FormGroup({'login': login}); + + const t = `
+ +
`; + + tcb.overrideTemplate(MyComp8, t).createAsync(MyComp8).then((fixture) => { + fixture.debugElement.componentInstance.form = form; + fixture.detectChanges(); + + login.updateValue('new value'); + + const loginEl = fixture.debugElement.query(By.css('input')).nativeElement; + expect(loginEl.value).toBe('new value'); + + form.reset({'login': 'oldValue'}); + + expect(loginEl.value).toBe('oldValue'); + async.done(); + }); + })); + it('should support form arrays', fakeAsync(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { const cityArray = new FormArray([new FormControl('SF'), new FormControl('NY')]); @@ -1283,6 +1336,32 @@ export function main() { expect(fixture.debugElement.componentInstance.name).toEqual('updated'); }))); + it('should reset the form to empty when reset button is clicked', + fakeAsync(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { + const t = ` +
+ +
+ `; + + const fixture = tcb.overrideTemplate(MyComp8, t).createFakeAsync(MyComp8); + tick(); + fixture.debugElement.componentInstance.name = 'should be cleared'; + fixture.detectChanges(); + tick(); + + const form = fixture.debugElement.children[0].injector.get(NgForm); + const formEl = fixture.debugElement.query(By.css('form')); + + dispatchEvent(formEl.nativeElement, 'reset'); + fixture.detectChanges(); + tick(); + + expect(fixture.debugElement.componentInstance.name).toBe(null); + expect(form.value.name).toEqual(null); + }))); + + it('should emit valueChanges and statusChanges on init', fakeAsync(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { const t = `
diff --git a/modules/@angular/forms/test/model_spec.ts b/modules/@angular/forms/test/model_spec.ts index 9e3c903e8e912..0518f34de7433 100644 --- a/modules/@angular/forms/test/model_spec.ts +++ b/modules/@angular/forms/test/model_spec.ts @@ -312,6 +312,130 @@ export function main() { })); }); + describe('reset()', () => { + let c: FormControl; + + beforeEach(() => { c = new FormControl('initial value'); }); + + it('should restore the initial value of the control if passed', () => { + c.updateValue('new value'); + expect(c.value).toBe('new value'); + + c.reset('initial value'); + expect(c.value).toBe('initial value'); + }); + + it('should clear the control value if no value is passed', () => { + c.updateValue('new value'); + expect(c.value).toBe('new value'); + + c.reset(); + expect(c.value).toBe(null); + }); + + it('should update the value of any parent controls with passed value', () => { + const g = new FormGroup({'one': c}); + c.updateValue('new value'); + expect(g.value).toEqual({'one': 'new value'}); + + c.reset('initial value'); + expect(g.value).toEqual({'one': 'initial value'}); + }); + + it('should update the value of any parent controls with null value', () => { + const g = new FormGroup({'one': c}); + c.updateValue('new value'); + expect(g.value).toEqual({'one': 'new value'}); + + c.reset(); + expect(g.value).toEqual({'one': null}); + }); + + + it('should mark the control as pristine', () => { + c.markAsDirty(); + expect(c.pristine).toBe(false); + + c.reset(); + expect(c.pristine).toBe(true); + }); + + it('should set the parent pristine state if all pristine', () => { + const g = new FormGroup({'one': c}); + c.markAsDirty(); + expect(g.pristine).toBe(false); + + c.reset(); + expect(g.pristine).toBe(true); + }); + + it('should not set the parent pristine state if it has other dirty controls', () => { + const c2 = new FormControl('two'); + const g = new FormGroup({'one': c, 'two': c2}); + c.markAsDirty(); + c2.markAsDirty(); + + c.reset(); + expect(g.pristine).toBe(false); + }); + + it('should mark the control as untouched', () => { + c.markAsTouched(); + expect(c.untouched).toBe(false); + + c.reset(); + expect(c.untouched).toBe(true); + }); + + it('should set the parent untouched state if all untouched', () => { + const g = new FormGroup({'one': c}); + c.markAsTouched(); + expect(g.untouched).toBe(false); + + c.reset(); + expect(g.untouched).toBe(true); + }); + + it('should not set the parent untouched state if other touched controls', () => { + const c2 = new FormControl('two'); + const g = new FormGroup({'one': c, 'two': c2}); + c.markAsTouched(); + c2.markAsTouched(); + + c.reset(); + expect(g.untouched).toBe(false); + }); + + describe('reset() events', () => { + let g: FormGroup, c2: FormControl, logger: any[]; + + beforeEach(() => { + c2 = new FormControl('two'); + g = new FormGroup({'one': c, 'two': c2}); + logger = []; + }); + + it('should emit one valueChange event per reset control', () => { + g.valueChanges.subscribe(() => logger.push('group')); + c.valueChanges.subscribe(() => logger.push('control1')); + c2.valueChanges.subscribe(() => logger.push('control2')); + + c.reset(); + expect(logger).toEqual(['control1', 'group']); + }); + + it('should emit one statusChange event per reset control', () => { + g.statusChanges.subscribe(() => logger.push('group')); + c.statusChanges.subscribe(() => logger.push('control1')); + c2.statusChanges.subscribe(() => logger.push('control2')); + + c.reset(); + expect(logger).toEqual(['control1', 'group']); + }); + }); + + }); + describe('valueChanges & statusChanges', () => { var c: any /** TODO #9100 */; @@ -634,6 +758,179 @@ export function main() { }); }); + describe('reset()', () => { + let c: FormControl, c2: FormControl, g: FormGroup; + + beforeEach(() => { + c = new FormControl('initial value'); + c2 = new FormControl(''); + g = new FormGroup({'one': c, 'two': c2}); + }); + + it('should set its own value if value passed', () => { + g.updateValue({'one': 'new value', 'two': 'new value'}); + + g.reset({'one': 'initial value', 'two': ''}); + expect(g.value).toEqual({'one': 'initial value', 'two': ''}); + }); + + it('should clear its own value if no value passed', () => { + g.updateValue({'one': 'new value', 'two': 'new value'}); + + g.reset(); + expect(g.value).toEqual({'one': null, 'two': null}); + }); + + it('should set the value of each of its child controls if value passed', () => { + g.updateValue({'one': 'new value', 'two': 'new value'}); + + g.reset({'one': 'initial value', 'two': ''}); + expect(c.value).toBe('initial value'); + expect(c2.value).toBe(''); + }); + + it('should clear the value of each of its child controls if no value passed', () => { + g.updateValue({'one': 'new value', 'two': 'new value'}); + + g.reset(); + expect(c.value).toBe(null); + expect(c2.value).toBe(null); + }); + + it('should set the value of its parent if value passed', () => { + const form = new FormGroup({'g': g}); + g.updateValue({'one': 'new value', 'two': 'new value'}); + + g.reset({'one': 'initial value', 'two': ''}); + expect(form.value).toEqual({'g': {'one': 'initial value', 'two': ''}}); + }); + + it('should clear the value of its parent if no value passed', () => { + const form = new FormGroup({'g': g}); + g.updateValue({'one': 'new value', 'two': 'new value'}); + + g.reset(); + expect(form.value).toEqual({'g': {'one': null, 'two': null}}); + }); + + it('should mark itself as pristine', () => { + g.markAsDirty(); + expect(g.pristine).toBe(false); + + g.reset(); + expect(g.pristine).toBe(true); + }); + + it('should mark all child controls as pristine', () => { + c.markAsDirty(); + c2.markAsDirty(); + expect(c.pristine).toBe(false); + expect(c2.pristine).toBe(false); + + g.reset(); + expect(c.pristine).toBe(true); + expect(c2.pristine).toBe(true); + }); + + it('should mark the parent as pristine if all siblings pristine', () => { + const c3 = new FormControl(''); + const form = new FormGroup({'g': g, 'c3': c3}); + + g.markAsDirty(); + expect(form.pristine).toBe(false); + + g.reset(); + expect(form.pristine).toBe(true); + }); + + it('should not mark the parent pristine if any dirty siblings', () => { + const c3 = new FormControl(''); + const form = new FormGroup({'g': g, 'c3': c3}); + + g.markAsDirty(); + c3.markAsDirty(); + expect(form.pristine).toBe(false); + + g.reset(); + expect(form.pristine).toBe(false); + }); + + it('should mark itself as untouched', () => { + g.markAsTouched(); + expect(g.untouched).toBe(false); + + g.reset(); + expect(g.untouched).toBe(true); + }); + + it('should mark all child controls as untouched', () => { + c.markAsTouched(); + c2.markAsTouched(); + expect(c.untouched).toBe(false); + expect(c2.untouched).toBe(false); + + g.reset(); + expect(c.untouched).toBe(true); + expect(c2.untouched).toBe(true); + }); + + it('should mark the parent untouched if all siblings untouched', () => { + const c3 = new FormControl(''); + const form = new FormGroup({'g': g, 'c3': c3}); + + g.markAsTouched(); + expect(form.untouched).toBe(false); + + g.reset(); + expect(form.untouched).toBe(true); + }); + + it('should not mark the parent untouched if any touched siblings', () => { + const c3 = new FormControl(''); + const form = new FormGroup({'g': g, 'c3': c3}); + + g.markAsTouched(); + c3.markAsTouched(); + expect(form.untouched).toBe(false); + + g.reset(); + expect(form.untouched).toBe(false); + }); + + describe('reset() events', () => { + let form: FormGroup, c3: FormControl, logger: any[]; + + beforeEach(() => { + c3 = new FormControl(''); + form = new FormGroup({'g': g, 'c3': c3}); + logger = []; + }); + + it('should emit one valueChange event per reset control', () => { + form.valueChanges.subscribe(() => logger.push('form')); + g.valueChanges.subscribe(() => logger.push('group')); + c.valueChanges.subscribe(() => logger.push('control1')); + c2.valueChanges.subscribe(() => logger.push('control2')); + c3.valueChanges.subscribe(() => logger.push('control3')); + + g.reset(); + expect(logger).toEqual(['control1', 'control2', 'group', 'form']); + }); + + it('should emit one statusChange event per reset control', () => { + form.statusChanges.subscribe(() => logger.push('form')); + g.statusChanges.subscribe(() => logger.push('group')); + c.statusChanges.subscribe(() => logger.push('control1')); + c2.statusChanges.subscribe(() => logger.push('control2')); + c3.statusChanges.subscribe(() => logger.push('control3')); + + g.reset(); + expect(logger).toEqual(['control1', 'control2', 'group', 'form']); + }); + }); + + }); + describe('optional components', () => { describe('contains', () => { var group: any /** TODO #9100 */; @@ -982,6 +1279,179 @@ export function main() { }); }); + describe('reset()', () => { + let c: FormControl, c2: FormControl, a: FormArray; + + beforeEach(() => { + c = new FormControl('initial value'); + c2 = new FormControl(''); + a = new FormArray([c, c2]); + }); + + it('should set its own value if value passed', () => { + a.updateValue(['new value', 'new value']); + + a.reset(['initial value', '']); + expect(a.value).toEqual(['initial value', '']); + }); + + + it('should clear its own value if no value passed', () => { + a.updateValue(['new value', 'new value']); + + a.reset(); + expect(a.value).toEqual([null, null]); + }); + + it('should set the value of each of its child controls if value passed', () => { + a.updateValue(['new value', 'new value']); + + a.reset(['initial value', '']); + expect(c.value).toBe('initial value'); + expect(c2.value).toBe(''); + }); + + it('should clear the value of each of its child controls if no value', () => { + a.updateValue(['new value', 'new value']); + + a.reset(); + expect(c.value).toBe(null); + expect(c2.value).toBe(null); + }); + + it('should set the value of its parent if value passed', () => { + const form = new FormGroup({'a': a}); + a.updateValue(['new value', 'new value']); + + a.reset(['initial value', '']); + expect(form.value).toEqual({'a': ['initial value', '']}); + }); + + it('should clear the value of its parent if no value passed', () => { + const form = new FormGroup({'a': a}); + a.updateValue(['new value', 'new value']); + + a.reset(); + expect(form.value).toEqual({'a': [null, null]}); + }); + + it('should mark itself as pristine', () => { + a.markAsDirty(); + expect(a.pristine).toBe(false); + + a.reset(); + expect(a.pristine).toBe(true); + }); + + it('should mark all child controls as pristine', () => { + c.markAsDirty(); + c2.markAsDirty(); + expect(c.pristine).toBe(false); + expect(c2.pristine).toBe(false); + + a.reset(); + expect(c.pristine).toBe(true); + expect(c2.pristine).toBe(true); + }); + + it('should mark the parent as pristine if all siblings pristine', () => { + const c3 = new FormControl(''); + const form = new FormGroup({'a': a, 'c3': c3}); + + a.markAsDirty(); + expect(form.pristine).toBe(false); + + a.reset(); + expect(form.pristine).toBe(true); + }); + + it('should not mark the parent pristine if any dirty siblings', () => { + const c3 = new FormControl(''); + const form = new FormGroup({'a': a, 'c3': c3}); + + a.markAsDirty(); + c3.markAsDirty(); + expect(form.pristine).toBe(false); + + a.reset(); + expect(form.pristine).toBe(false); + }); + + it('should mark itself as untouched', () => { + a.markAsTouched(); + expect(a.untouched).toBe(false); + + a.reset(); + expect(a.untouched).toBe(true); + }); + + it('should mark all child controls as untouched', () => { + c.markAsTouched(); + c2.markAsTouched(); + expect(c.untouched).toBe(false); + expect(c2.untouched).toBe(false); + + a.reset(); + expect(c.untouched).toBe(true); + expect(c2.untouched).toBe(true); + }); + + it('should mark the parent untouched if all siblings untouched', () => { + const c3 = new FormControl(''); + const form = new FormGroup({'a': a, 'c3': c3}); + + a.markAsTouched(); + expect(form.untouched).toBe(false); + + a.reset(); + expect(form.untouched).toBe(true); + }); + + it('should not mark the parent untouched if any touched siblings', () => { + const c3 = new FormControl(''); + const form = new FormGroup({'a': a, 'c3': c3}); + + a.markAsTouched(); + c3.markAsTouched(); + expect(form.untouched).toBe(false); + + a.reset(); + expect(form.untouched).toBe(false); + }); + + describe('reset() events', () => { + let form: FormGroup, c3: FormControl, logger: any[]; + + beforeEach(() => { + c3 = new FormControl(''); + form = new FormGroup({'a': a, 'c3': c3}); + logger = []; + }); + + it('should emit one valueChange event per reset control', () => { + form.valueChanges.subscribe(() => logger.push('form')); + a.valueChanges.subscribe(() => logger.push('array')); + c.valueChanges.subscribe(() => logger.push('control1')); + c2.valueChanges.subscribe(() => logger.push('control2')); + c3.valueChanges.subscribe(() => logger.push('control3')); + + a.reset(); + expect(logger).toEqual(['control1', 'control2', 'array', 'form']); + }); + + it('should emit one statusChange event per reset control', () => { + form.statusChanges.subscribe(() => logger.push('form')); + a.statusChanges.subscribe(() => logger.push('array')); + c.statusChanges.subscribe(() => logger.push('control1')); + c2.statusChanges.subscribe(() => logger.push('control2')); + c3.statusChanges.subscribe(() => logger.push('control3')); + + a.reset(); + expect(logger).toEqual(['control1', 'control2', 'array', 'form']); + }); + }); + }); + describe('errors', () => { it('should run the validator when the value changes', () => { var simpleValidator = (c: any /** TODO #9100 */) => diff --git a/tools/public_api_guard/forms/index.d.ts b/tools/public_api_guard/forms/index.d.ts index 69da191b58695..901d1a8ba4555 100644 --- a/tools/public_api_guard/forms/index.d.ts +++ b/tools/public_api_guard/forms/index.d.ts @@ -28,9 +28,16 @@ export declare abstract class AbstractControl { markAsPending({onlySelf}?: { onlySelf?: boolean; }): void; + markAsPristine({onlySelf}?: { + onlySelf?: boolean; + }): void; markAsTouched({onlySelf}?: { onlySelf?: boolean; }): void; + markAsUntouched({onlySelf}?: { + onlySelf?: boolean; + }): void; + abstract reset(value?: any, options?: Object): void; setAsyncValidators(newValidator: AsyncValidatorFn | AsyncValidatorFn[]): void; setErrors(errors: { [key: string]: any; @@ -61,6 +68,7 @@ export declare abstract class AbstractControlDirective { valid: boolean; value: any; valueChanges: Observable; + reset(value?: any): void; } /** @experimental */ @@ -131,6 +139,9 @@ export declare class FormArray extends AbstractControl { insert(index: number, control: AbstractControl): void; push(control: AbstractControl): void; removeAt(index: number): void; + reset(value?: any, {onlySelf}?: { + onlySelf?: boolean; + }): void; updateValue(value: any[], {onlySelf}?: { onlySelf?: boolean; }): void; @@ -164,10 +175,14 @@ export declare class FormBuilder { export declare class FormControl extends AbstractControl { constructor(value?: any, validator?: ValidatorFn | ValidatorFn[], asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[]); registerOnChange(fn: Function): void; - updateValue(value: any, {onlySelf, emitEvent, emitModelToViewChange}?: { + reset(value?: any, {onlySelf}?: { + onlySelf?: boolean; + }): void; + updateValue(value: any, {onlySelf, emitEvent, emitModelToViewChange, emitViewToModelChange}?: { onlySelf?: boolean; emitEvent?: boolean; emitModelToViewChange?: boolean; + emitViewToModelChange?: boolean; }): void; } @@ -218,6 +233,9 @@ export declare class FormGroup extends AbstractControl { include(controlName: string): void; registerControl(name: string, control: AbstractControl): AbstractControl; removeControl(name: string): void; + reset(value?: any, {onlySelf}?: { + onlySelf?: boolean; + }): void; updateValue(value: { [key: string]: any; }, {onlySelf}?: { @@ -242,6 +260,7 @@ export declare class FormGroupDirective extends ControlContainer implements Form getFormArray(dir: FormArrayName): FormArray; getFormGroup(dir: FormGroupName): FormGroup; ngOnChanges(changes: SimpleChanges): void; + onReset(): void; onSubmit(): boolean; removeControl(dir: NgControl): void; removeFormArray(dir: FormArrayName): void; @@ -320,6 +339,7 @@ export declare class NgForm extends ControlContainer implements Form { addFormGroup(dir: NgModelGroup): void; getControl(dir: NgModel): FormControl; getFormGroup(dir: NgModelGroup): FormGroup; + onReset(): void; onSubmit(): boolean; removeControl(dir: NgModel): void; removeFormGroup(dir: NgModelGroup): void;