Skip to content

feat(forms): support updateOn in ngModelOptions #18577

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 15, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/forms/src/directives/ng_form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {Form} from './form_interface';
import {NgControl} from './ng_control';
import {NgModel} from './ng_model';
import {NgModelGroup} from './ng_model_group';
import {composeAsyncValidators, composeValidators, setUpControl, setUpFormContainer} from './shared';
import {composeAsyncValidators, composeValidators, removeDir, setUpControl, setUpFormContainer, syncPendingControls} from './shared';

export const formDirectiveProvider: any = {
provide: ControlContainer,
Expand Down Expand Up @@ -65,6 +65,7 @@ const resolvedPromise = Promise.resolve(null);
})
export class NgForm extends ControlContainer implements Form {
private _submitted: boolean = false;
private _directives: NgModel[] = [];

form: FormGroup;
ngSubmit = new EventEmitter();
Expand Down Expand Up @@ -93,6 +94,7 @@ export class NgForm extends ControlContainer implements Form {
dir._control = <FormControl>container.registerControl(dir.name, dir.control);
setUpControl(dir.control, dir);
dir.control.updateValueAndValidity({emitEvent: false});
this._directives.push(dir);
});
}

Expand All @@ -104,6 +106,7 @@ export class NgForm extends ControlContainer implements Form {
if (container) {
container.removeControl(dir.name);
}
removeDir<NgModel>(this._directives, dir);
});
}

Expand Down Expand Up @@ -139,6 +142,7 @@ export class NgForm extends ControlContainer implements Form {

onSubmit($event: Event): boolean {
this._submitted = true;
syncPendingControls(this.form, this._directives);
this.ngSubmit.emit($event);
return false;
}
Expand Down
49 changes: 47 additions & 2 deletions packages/forms/src/directives/ng_model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import {Directive, EventEmitter, Host, Inject, Input, OnChanges, OnDestroy, Optional, Output, Self, SimpleChanges, forwardRef} from '@angular/core';

import {FormControl} from '../model';
import {FormControl, FormHooks} from '../model';
import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../validators';

import {AbstractFormGroupDirective} from './abstract_form_group_directive';
Expand Down Expand Up @@ -119,7 +119,45 @@ export class NgModel extends NgControl implements OnChanges,
@Input() name: string;
@Input('disabled') isDisabled: boolean;
@Input('ngModel') model: any;
@Input('ngModelOptions') options: {name?: string, standalone?: boolean};

/**
* Options object for this `ngModel` instance. You can configure the following properties:
*
* **name**: An alternative to setting the name attribute on the form control element.
* Sometimes, especially with custom form components, the name attribute might be used
* as an `@Input` property for a different purpose. In cases like these, you can configure
* the `ngModel` name through this option.
*
* ```html
* <form>
* <my-person-control name="Nancy" ngModel [ngModelOptions]="{name: 'user'}">
* </my-person-control>
* </form>
* <!-- form value: {user: ''} -->
* ```
*
* **standalone**: Defaults to false. If this is set to true, the `ngModel` will not
* register itself with its parent form, and will act as if it's not in the form. This
* can be handy if you have form meta-controls, a.k.a. form elements nested in
* the `<form>` tag that control the display of the form, but don't contain form data.
*
* ```html
* <form>
* <input name="login" ngModel placeholder="Login">
* <input type="checkbox" ngModel [ngModelOptions]="{standalone: true}"> Show more options?
* </form>
* <!-- form value: {login: ''} -->
* ```
*
* **updateOn**: Defaults to `'change'`. Defines the event upon which the form control
* value and validity will update. Also accepts `'blur'` and `'submit'`.
*
* ```html
* <input [(ngModel)]="firstName" [ngModelOptions]="{updateOn: 'blur'}">
* ```
*
*/
@Input('ngModelOptions') options: {name?: string, standalone?: boolean, updateOn?: FormHooks};

@Output('ngModelChange') update = new EventEmitter();

Expand Down Expand Up @@ -170,11 +208,18 @@ export class NgModel extends NgControl implements OnChanges,
}

private _setUpControl(): void {
this._setUpdateStrategy();
this._isStandalone() ? this._setUpStandalone() :
this.formDirective.addControl(this);
this._registered = true;
}

private _setUpdateStrategy(): void {
if (this.options && this.options.updateOn != null) {
this._control._updateOn = this.options.updateOn;
}
}

private _isStandalone(): boolean {
return !this._parent || !!(this.options && this.options.standalone);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {NG_ASYNC_VALIDATORS, NG_VALIDATORS, Validators} from '../../validators';
import {ControlContainer} from '../control_container';
import {Form} from '../form_interface';
import {ReactiveErrors} from '../reactive_errors';
import {cleanUpControl, composeAsyncValidators, composeValidators, setUpControl, setUpFormContainer} from '../shared';
import {cleanUpControl, composeAsyncValidators, composeValidators, removeDir, setUpControl, setUpFormContainer, syncPendingControls} from '../shared';

import {FormControlName} from './form_control_name';
import {FormArrayName, FormGroupName} from './form_group_name';
Expand Down Expand Up @@ -105,7 +105,7 @@ export class FormGroupDirective extends ControlContainer implements Form,

getControl(dir: FormControlName): FormControl { return <FormControl>this.form.get(dir.path); }

removeControl(dir: FormControlName): void { remove(this.directives, dir); }
removeControl(dir: FormControlName): void { removeDir<FormControlName>(this.directives, dir); }

addFormGroup(dir: FormGroupName): void {
const ctrl: any = this.form.get(dir.path);
Expand Down Expand Up @@ -134,7 +134,7 @@ export class FormGroupDirective extends ControlContainer implements Form,

onSubmit($event: Event): boolean {
this._submitted = true;
this._syncPendingControls();
syncPendingControls(this.form, this.directives);
this.ngSubmit.emit($event);
return false;
}
Expand All @@ -146,15 +146,6 @@ export class FormGroupDirective extends ControlContainer implements Form,
this._submitted = false;
}

/** @internal */
_syncPendingControls() {
this.form._syncPendingControls();
this.directives.forEach(dir => {
if (dir.control.updateOn === 'submit') {
dir.viewToModelUpdate(dir.control._pendingValue);
}
});
}

/** @internal */
_updateDomValue() {
Expand Down Expand Up @@ -190,10 +181,3 @@ export class FormGroupDirective extends ControlContainer implements Form,
}
}
}

function remove<T>(list: T[], el: T): void {
const index = list.indexOf(el);
if (index > -1) {
list.splice(index, 1);
}
}
15 changes: 15 additions & 0 deletions packages/forms/src/directives/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,16 @@ export function isBuiltInAccessor(valueAccessor: ControlValueAccessor): boolean
return BUILTIN_ACCESSORS.some(a => valueAccessor.constructor === a);
}

export function syncPendingControls(form: FormGroup, directives: NgControl[]): void {
form._syncPendingControls();
directives.forEach(dir => {
const control = dir.control as FormControl;
if (control.updateOn === 'submit') {
dir.viewToModelUpdate(control._pendingValue);
}
});
}

// TODO: vsavkin remove it once https://github.com/angular/angular/issues/3011 is implemented
export function selectValueAccessor(
dir: NgControl, valueAccessors: ControlValueAccessor[]): ControlValueAccessor|null {
Expand Down Expand Up @@ -198,3 +208,8 @@ export function selectValueAccessor(
_throwError(dir, 'No valid value accessor for form control with');
return null;
}

export function removeDir<T>(list: T[], el: T): void {
const index = list.indexOf(el);
if (index > -1) list.splice(index, 1);
}
16 changes: 16 additions & 0 deletions packages/forms/test/reactive_integration_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -819,6 +819,22 @@ export function main() {
expect(control.dirty).toBe(true, 'Expected control to update dirty state when blurred.');
});

it('should update touched when control is blurred', () => {
const fixture = initTest(FormControlComp);
const control = new FormControl('', {updateOn: 'blur'});
fixture.componentInstance.control = control;
fixture.detectChanges();

expect(control.touched).toBe(false, 'Expected control to start out untouched.');

const input = fixture.debugElement.query(By.css('input')).nativeElement;
dispatchEvent(input, 'blur');
fixture.detectChanges();

expect(control.touched)
.toBe(true, 'Expected control to update touched state when blurred.');
});

it('should continue waiting for blur to update if previously blurred', () => {
const fixture = initTest(FormControlComp);
const control =
Expand Down
Loading