Skip to content

Commit

Permalink
fix(forms): update validity when validator dir changes
Browse files Browse the repository at this point in the history
closes #11116
  • Loading branch information
kara authored and vicb committed Aug 29, 2016
1 parent 0b665c0 commit d2ad871
Show file tree
Hide file tree
Showing 12 changed files with 324 additions and 44 deletions.
6 changes: 5 additions & 1 deletion modules/@angular/forms/src/directives/ng_control.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import {AbstractControlDirective} from './abstract_control_directive';
import {ControlValueAccessor} from './control_value_accessor';
import {AsyncValidatorFn, ValidatorFn} from './validators';
import {AsyncValidatorFn, Validator, ValidatorFn} from './validators';

function unimplemented(): any {
throw new Error('unimplemented');
Expand All @@ -26,6 +26,10 @@ function unimplemented(): any {
export abstract class NgControl extends AbstractControlDirective {
name: string = null;
valueAccessor: ControlValueAccessor = null;
/** @internal */
_rawValidators: Array<Validator|ValidatorFn> = [];
/** @internal */
_rawAsyncValidators: Array<Validator|ValidatorFn> = [];

get validator(): ValidatorFn { return <ValidatorFn>unimplemented(); }
get asyncValidator(): AsyncValidatorFn { return <AsyncValidatorFn>unimplemented(); }
Expand Down
12 changes: 7 additions & 5 deletions modules/@angular/forms/src/directives/ng_model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {NgForm} from './ng_form';
import {NgModelGroup} from './ng_model_group';
import {composeAsyncValidators, composeValidators, controlPath, isPropertyUpdated, selectValueAccessor, setUpControl} from './shared';
import {TemplateDrivenErrors} from './template_driven_errors';
import {AsyncValidatorFn, ValidatorFn} from './validators';
import {AsyncValidatorFn, Validator, ValidatorFn} from './validators';

export const formControlBinding: any = {
provide: NgControl,
Expand Down Expand Up @@ -72,11 +72,13 @@ export class NgModel extends NgControl implements OnChanges,
@Output('ngModelChange') update = new EventEmitter();

constructor(@Optional() @Host() private _parent: ControlContainer,
@Optional() @Self() @Inject(NG_VALIDATORS) private _validators: any[],
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) private _asyncValidators: any[],
@Optional() @Self() @Inject(NG_VALIDATORS) validators: Array<Validator|ValidatorFn>,
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: Array<Validator|AsyncValidatorFn>,
@Optional() @Self() @Inject(NG_VALUE_ACCESSOR)
valueAccessors: ControlValueAccessor[]) {
super();
this._rawValidators = validators || [];
this._rawAsyncValidators = asyncValidators || [];
this.valueAccessor = selectValueAccessor(this, valueAccessors);
}

Expand All @@ -103,10 +105,10 @@ export class NgModel extends NgControl implements OnChanges,

get formDirective(): any { return this._parent ? this._parent.formDirective : null; }

get validator(): ValidatorFn { return composeValidators(this._validators); }
get validator(): ValidatorFn { return composeValidators(this._rawValidators); }

get asyncValidator(): AsyncValidatorFn {
return composeAsyncValidators(this._asyncValidators);
return composeAsyncValidators(this._rawAsyncValidators);
}

viewToModelUpdate(newValue: any): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '../control_value_accessor
import {NgControl} from '../ng_control';
import {ReactiveErrors} from '../reactive_errors';
import {composeAsyncValidators, composeValidators, isPropertyUpdated, selectValueAccessor, setUpControl} from '../shared';
import {AsyncValidatorFn, ValidatorFn} from '../validators';
import {AsyncValidatorFn, Validator, ValidatorFn} from '../validators';

export const formControlBinding: any = {
provide: NgControl,
Expand Down Expand Up @@ -84,13 +84,13 @@ export class FormControlDirective extends NgControl implements OnChanges {
@Input('disabled')
set isDisabled(isDisabled: boolean) { ReactiveErrors.disabledAttrWarning(); }

constructor(@Optional() @Self() @Inject(NG_VALIDATORS) private _validators:
/* Array<Validator|Function> */ any[],
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) private _asyncValidators:
/* Array<Validator|Function> */ any[],
constructor(@Optional() @Self() @Inject(NG_VALIDATORS) validators: Array<Validator|ValidatorFn>,
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: Array<Validator|AsyncValidatorFn>,
@Optional() @Self() @Inject(NG_VALUE_ACCESSOR)
valueAccessors: ControlValueAccessor[]) {
super();
this._rawValidators = validators || [];
this._rawAsyncValidators = asyncValidators || [];
this.valueAccessor = selectValueAccessor(this, valueAccessors);
}

Expand All @@ -108,10 +108,10 @@ export class FormControlDirective extends NgControl implements OnChanges {

get path(): string[] { return []; }

get validator(): ValidatorFn { return composeValidators(this._validators); }
get validator(): ValidatorFn { return composeValidators(this._rawValidators); }

get asyncValidator(): AsyncValidatorFn {
return composeAsyncValidators(this._asyncValidators);
return composeAsyncValidators(this._rawAsyncValidators);
}

get control(): FormControl { return this.form; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '../control_value_accessor
import {NgControl} from '../ng_control';
import {ReactiveErrors} from '../reactive_errors';
import {composeAsyncValidators, composeValidators, controlPath, isPropertyUpdated, selectValueAccessor} from '../shared';
import {AsyncValidatorFn, ValidatorFn} from '../validators';
import {AsyncValidatorFn, Validator, ValidatorFn} from '../validators';

import {FormGroupDirective} from './form_group_directive';
import {FormArrayName, FormGroupName} from './form_group_name';
Expand Down Expand Up @@ -110,12 +110,13 @@ export class FormControlName extends NgControl implements OnChanges, OnDestroy {

constructor(
@Optional() @Host() @SkipSelf() private _parent: ControlContainer,
@Optional() @Self() @Inject(NG_VALIDATORS) private _validators:
/* Array<Validator|Function> */ any[],
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) private _asyncValidators:
/* Array<Validator|Function> */ any[],
@Optional() @Self() @Inject(NG_VALIDATORS) validators: Array<Validator|ValidatorFn>,
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators:
Array<Validator|AsyncValidatorFn>,
@Optional() @Self() @Inject(NG_VALUE_ACCESSOR) valueAccessors: ControlValueAccessor[]) {
super();
this._rawValidators = validators || [];
this._rawAsyncValidators = asyncValidators || [];
this.valueAccessor = selectValueAccessor(this, valueAccessors);
}

Expand Down Expand Up @@ -147,9 +148,11 @@ export class FormControlName extends NgControl implements OnChanges, OnDestroy {

get formDirective(): any { return this._parent ? this._parent.formDirective : null; }

get validator(): ValidatorFn { return composeValidators(this._validators); }
get validator(): ValidatorFn { return composeValidators(this._rawValidators); }

get asyncValidator(): AsyncValidatorFn { return composeAsyncValidators(this._asyncValidators); }
get asyncValidator(): AsyncValidatorFn {
return composeAsyncValidators(this._rawAsyncValidators);
}

get control(): FormControl { return this.formDirective.getControl(this); }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,6 @@ export class FormGroupDirective extends ControlContainer implements Form,

var async = composeAsyncValidators(this._asyncValidators);
this.form.asyncValidator = Validators.composeAsync([this.form.asyncValidator, async]);
this.form.updateValueAndValidity({onlySelf: true, emitEvent: false});
this._updateDomValue(changes);
}
}
Expand Down Expand Up @@ -189,6 +188,7 @@ export class FormGroupDirective extends ControlContainer implements Form,
/** @internal */
_updateDomValue(changes: SimpleChanges) {
const oldForm = changes['form'].previousValue;

this.directives.forEach(dir => {
const newCtrl: any = this.form.get(dir.path);
const oldCtrl = oldForm.get(dir.path);
Expand All @@ -197,6 +197,8 @@ export class FormGroupDirective extends ControlContainer implements Form,
if (newCtrl) setUpControl(newCtrl, dir);
}
});

this.form._updateTreeValidity({emitEvent: false});
}

private _checkFormPresent() {
Expand Down
19 changes: 16 additions & 3 deletions modules/@angular/forms/src/directives/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {RadioControlValueAccessor} from './radio_control_value_accessor';
import {FormArrayName} from './reactive_directives/form_group_name';
import {SelectControlValueAccessor} from './select_control_value_accessor';
import {SelectMultipleControlValueAccessor} from './select_multiple_control_value_accessor';
import {AsyncValidatorFn, ValidatorFn} from './validators';
import {AsyncValidatorFn, Validator, ValidatorFn} from './validators';


export function controlPath(name: string, parent: ControlContainer): string[] {
Expand All @@ -49,6 +49,9 @@ export function setUpControl(control: FormControl, dir: NgControl): void {
control.setValue(newValue, {emitModelToViewChange: false});
});

// touched
dir.valueAccessor.registerOnTouched(() => control.markAsTouched());

control.registerOnChange((newValue: any, emitModelEvent: boolean) => {
// control -> view
dir.valueAccessor.writeValue(newValue);
Expand All @@ -62,13 +65,23 @@ export function setUpControl(control: FormControl, dir: NgControl): void {
(isDisabled: boolean) => { dir.valueAccessor.setDisabledState(isDisabled); });
}

// touched
dir.valueAccessor.registerOnTouched(() => control.markAsTouched());
// re-run validation when validator binding changes, e.g. minlength=3 -> minlength=4
dir._rawValidators.forEach((validator: Validator | ValidatorFn) => {
if ((<Validator>validator).registerOnChange)
(<Validator>validator).registerOnChange(() => control.updateValueAndValidity());
});

dir._rawAsyncValidators.forEach((validator: Validator | ValidatorFn) => {
if ((<Validator>validator).registerOnChange)
(<Validator>validator).registerOnChange(() => control.updateValueAndValidity());
});
}

export function cleanUpControl(control: FormControl, dir: NgControl) {
dir.valueAccessor.registerOnChange(() => _noControlError(dir));
dir.valueAccessor.registerOnTouched(() => _noControlError(dir));
dir._rawValidators.forEach((validator: Validator) => validator.registerOnChange(null));
dir._rawAsyncValidators.forEach((validator: Validator) => validator.registerOnChange(null));
if (control) control._clearChangeFns();
}

Expand Down
36 changes: 27 additions & 9 deletions modules/@angular/forms/src/directives/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {Attribute, Directive, HostBinding, Input, OnChanges, SimpleChanges, forwardRef} from '@angular/core';
import {Directive, Input, OnChanges, SimpleChanges, forwardRef} from '@angular/core';

import {isPresent} from '../facade/lang';
import {AbstractControl} from '../model';
Expand All @@ -33,7 +33,10 @@ import {NG_VALIDATORS, Validators} from '../validators';
*
* @stable
*/
export interface Validator { validate(c: AbstractControl): {[key: string]: any}; }
export interface Validator {
validate(c: AbstractControl): {[key: string]: any};
registerOnChange?(fn: () => void): void;
}

export const REQUIRED_VALIDATOR: any = {
provide: NG_VALIDATORS,
Expand All @@ -60,15 +63,21 @@ export const REQUIRED_VALIDATOR: any = {
})
export class RequiredValidator implements Validator {
private _required: boolean;
private _onChange: () => void;

@Input()
get required(): boolean { return this._required; }

set required(value: boolean) { this._required = isPresent(value) && `${value}` !== 'false'; }
set required(value: boolean) {
this._required = isPresent(value) && `${value}` !== 'false';
if (this._onChange) this._onChange();
}

validate(c: AbstractControl): {[key: string]: any} {
return this.required ? Validators.required(c) : null;
}

registerOnChange(fn: () => void) { this._onChange = fn; }
}

/**
Expand Down Expand Up @@ -110,6 +119,7 @@ export const MIN_LENGTH_VALIDATOR: any = {
export class MinLengthValidator implements Validator,
OnChanges {
private _validator: ValidatorFn;
private _onChange: () => void;

@Input() minlength: string;

Expand All @@ -118,15 +128,17 @@ export class MinLengthValidator implements Validator,
}

ngOnChanges(changes: SimpleChanges) {
const minlengthChange = changes['minlength'];
if (minlengthChange) {
if (changes['minlength']) {
this._createValidator();
if (this._onChange) this._onChange();
}
}

validate(c: AbstractControl): {[key: string]: any} {
return isPresent(this.minlength) ? this._validator(c) : null;
}

registerOnChange(fn: () => void) { this._onChange = fn; }
}

/**
Expand Down Expand Up @@ -157,6 +169,7 @@ export const MAX_LENGTH_VALIDATOR: any = {
export class MaxLengthValidator implements Validator,
OnChanges {
private _validator: ValidatorFn;
private _onChange: () => void;

@Input() maxlength: string;

Expand All @@ -165,15 +178,17 @@ export class MaxLengthValidator implements Validator,
}

ngOnChanges(changes: SimpleChanges) {
const maxlengthChange = changes['maxlength'];
if (maxlengthChange) {
if (changes['maxlength']) {
this._createValidator();
if (this._onChange) this._onChange();
}
}

validate(c: AbstractControl): {[key: string]: any} {
return isPresent(this.maxlength) ? this._validator(c) : null;
}

registerOnChange(fn: () => void) { this._onChange = fn; }
}


Expand Down Expand Up @@ -205,19 +220,22 @@ export const PATTERN_VALIDATOR: any = {
export class PatternValidator implements Validator,
OnChanges {
private _validator: ValidatorFn;
private _onChange: () => void;

@Input() pattern: string;

private _createValidator() { this._validator = Validators.pattern(this.pattern); }

ngOnChanges(changes: SimpleChanges) {
const patternChange = changes['pattern'];
if (patternChange) {
if (changes['pattern']) {
this._createValidator();
if (this._onChange) this._onChange();
}
}

validate(c: AbstractControl): {[key: string]: any} {
return isPresent(this.pattern) ? this._validator(c) : null;
}

registerOnChange(fn: () => void) { this._onChange = fn; }
}
6 changes: 6 additions & 0 deletions modules/@angular/forms/src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,12 @@ export abstract class AbstractControl {
}
}

/** @internal */
_updateTreeValidity({emitEvent}: {emitEvent?: boolean} = {emitEvent: true}) {
this._forEachChild((ctrl: AbstractControl) => ctrl._updateTreeValidity({emitEvent}));
this.updateValueAndValidity({onlySelf: true, emitEvent});
}

private _runValidator(): {[key: string]: any} {
return isPresent(this.validator) ? this.validator(this) : null;
}
Expand Down
34 changes: 34 additions & 0 deletions modules/@angular/forms/test/form_group_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -809,5 +809,39 @@ export function main() {
});

});

describe('updateTreeValidity()', () => {
let c: FormControl, c2: FormControl, c3: FormControl;
let nested: FormGroup, form: FormGroup;
let logger: string[];

beforeEach(() => {
c = new FormControl('one');
c2 = new FormControl('two');
c3 = new FormControl('three');
nested = new FormGroup({one: c, two: c2});
form = new FormGroup({nested: nested, three: c3});
logger = [];

c.statusChanges.subscribe(() => logger.push('one'));
c2.statusChanges.subscribe(() => logger.push('two'));
c3.statusChanges.subscribe(() => logger.push('three'));
nested.statusChanges.subscribe(() => logger.push('nested'));
form.statusChanges.subscribe(() => logger.push('form'));
});

it('should update tree validity', () => {
form._updateTreeValidity();
expect(logger).toEqual(['one', 'two', 'nested', 'three', 'form']);
});

it('should not emit events when turned off', () => {
form._updateTreeValidity({emitEvent: false});
expect(logger).toEqual([]);
});

});


});
}
Loading

0 comments on commit d2ad871

Please sign in to comment.