diff --git a/modules/@angular/forms/src/directives/abstract_control_directive.ts b/modules/@angular/forms/src/directives/abstract_control_directive.ts index a10adabcf9..d9db25bccb 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 15f7afc6db..48d69cd803 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 4df297a2b4..64df9d83b8 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 96e7f49a5c..13cca21f37 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 67cc48e097..3cd4c8046d 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 fa99e02ef1..cbbe12c222 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 739cdaa6ae..26b2800851 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 = `