fix(forms): update validity when validator dir changes

closes #11116
This commit is contained in:
Kara Erickson 2016-08-29 11:33:49 -07:00 committed by Victor Berchet
parent 0b665c0ece
commit d2ad871279
12 changed files with 324 additions and 44 deletions

View File

@ -9,7 +9,7 @@
import {AbstractControlDirective} from './abstract_control_directive'; import {AbstractControlDirective} from './abstract_control_directive';
import {ControlValueAccessor} from './control_value_accessor'; import {ControlValueAccessor} from './control_value_accessor';
import {AsyncValidatorFn, ValidatorFn} from './validators'; import {AsyncValidatorFn, Validator, ValidatorFn} from './validators';
function unimplemented(): any { function unimplemented(): any {
throw new Error('unimplemented'); throw new Error('unimplemented');
@ -26,6 +26,10 @@ function unimplemented(): any {
export abstract class NgControl extends AbstractControlDirective { export abstract class NgControl extends AbstractControlDirective {
name: string = null; name: string = null;
valueAccessor: ControlValueAccessor = null; valueAccessor: ControlValueAccessor = null;
/** @internal */
_rawValidators: Array<Validator|ValidatorFn> = [];
/** @internal */
_rawAsyncValidators: Array<Validator|ValidatorFn> = [];
get validator(): ValidatorFn { return <ValidatorFn>unimplemented(); } get validator(): ValidatorFn { return <ValidatorFn>unimplemented(); }
get asyncValidator(): AsyncValidatorFn { return <AsyncValidatorFn>unimplemented(); } get asyncValidator(): AsyncValidatorFn { return <AsyncValidatorFn>unimplemented(); }

View File

@ -20,7 +20,7 @@ import {NgForm} from './ng_form';
import {NgModelGroup} from './ng_model_group'; import {NgModelGroup} from './ng_model_group';
import {composeAsyncValidators, composeValidators, controlPath, isPropertyUpdated, selectValueAccessor, setUpControl} from './shared'; import {composeAsyncValidators, composeValidators, controlPath, isPropertyUpdated, selectValueAccessor, setUpControl} from './shared';
import {TemplateDrivenErrors} from './template_driven_errors'; import {TemplateDrivenErrors} from './template_driven_errors';
import {AsyncValidatorFn, ValidatorFn} from './validators'; import {AsyncValidatorFn, Validator, ValidatorFn} from './validators';
export const formControlBinding: any = { export const formControlBinding: any = {
provide: NgControl, provide: NgControl,
@ -72,11 +72,13 @@ export class NgModel extends NgControl implements OnChanges,
@Output('ngModelChange') update = new EventEmitter(); @Output('ngModelChange') update = new EventEmitter();
constructor(@Optional() @Host() private _parent: ControlContainer, constructor(@Optional() @Host() private _parent: ControlContainer,
@Optional() @Self() @Inject(NG_VALIDATORS) private _validators: any[], @Optional() @Self() @Inject(NG_VALIDATORS) validators: Array<Validator|ValidatorFn>,
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) private _asyncValidators: any[], @Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: Array<Validator|AsyncValidatorFn>,
@Optional() @Self() @Inject(NG_VALUE_ACCESSOR) @Optional() @Self() @Inject(NG_VALUE_ACCESSOR)
valueAccessors: ControlValueAccessor[]) { valueAccessors: ControlValueAccessor[]) {
super(); super();
this._rawValidators = validators || [];
this._rawAsyncValidators = asyncValidators || [];
this.valueAccessor = selectValueAccessor(this, valueAccessors); this.valueAccessor = selectValueAccessor(this, valueAccessors);
} }
@ -103,10 +105,10 @@ export class NgModel extends NgControl implements OnChanges,
get formDirective(): any { return this._parent ? this._parent.formDirective : null; } 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 { get asyncValidator(): AsyncValidatorFn {
return composeAsyncValidators(this._asyncValidators); return composeAsyncValidators(this._rawAsyncValidators);
} }
viewToModelUpdate(newValue: any): void { viewToModelUpdate(newValue: any): void {

View File

@ -16,7 +16,7 @@ import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '../control_value_accessor
import {NgControl} from '../ng_control'; import {NgControl} from '../ng_control';
import {ReactiveErrors} from '../reactive_errors'; import {ReactiveErrors} from '../reactive_errors';
import {composeAsyncValidators, composeValidators, isPropertyUpdated, selectValueAccessor, setUpControl} from '../shared'; import {composeAsyncValidators, composeValidators, isPropertyUpdated, selectValueAccessor, setUpControl} from '../shared';
import {AsyncValidatorFn, ValidatorFn} from '../validators'; import {AsyncValidatorFn, Validator, ValidatorFn} from '../validators';
export const formControlBinding: any = { export const formControlBinding: any = {
provide: NgControl, provide: NgControl,
@ -84,13 +84,13 @@ export class FormControlDirective extends NgControl implements OnChanges {
@Input('disabled') @Input('disabled')
set isDisabled(isDisabled: boolean) { ReactiveErrors.disabledAttrWarning(); } set isDisabled(isDisabled: boolean) { ReactiveErrors.disabledAttrWarning(); }
constructor(@Optional() @Self() @Inject(NG_VALIDATORS) private _validators: constructor(@Optional() @Self() @Inject(NG_VALIDATORS) validators: Array<Validator|ValidatorFn>,
/* Array<Validator|Function> */ any[], @Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: Array<Validator|AsyncValidatorFn>,
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) private _asyncValidators:
/* Array<Validator|Function> */ any[],
@Optional() @Self() @Inject(NG_VALUE_ACCESSOR) @Optional() @Self() @Inject(NG_VALUE_ACCESSOR)
valueAccessors: ControlValueAccessor[]) { valueAccessors: ControlValueAccessor[]) {
super(); super();
this._rawValidators = validators || [];
this._rawAsyncValidators = asyncValidators || [];
this.valueAccessor = selectValueAccessor(this, valueAccessors); this.valueAccessor = selectValueAccessor(this, valueAccessors);
} }
@ -108,10 +108,10 @@ export class FormControlDirective extends NgControl implements OnChanges {
get path(): string[] { return []; } get path(): string[] { return []; }
get validator(): ValidatorFn { return composeValidators(this._validators); } get validator(): ValidatorFn { return composeValidators(this._rawValidators); }
get asyncValidator(): AsyncValidatorFn { get asyncValidator(): AsyncValidatorFn {
return composeAsyncValidators(this._asyncValidators); return composeAsyncValidators(this._rawAsyncValidators);
} }
get control(): FormControl { return this.form; } get control(): FormControl { return this.form; }

View File

@ -17,7 +17,7 @@ import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '../control_value_accessor
import {NgControl} from '../ng_control'; import {NgControl} from '../ng_control';
import {ReactiveErrors} from '../reactive_errors'; import {ReactiveErrors} from '../reactive_errors';
import {composeAsyncValidators, composeValidators, controlPath, isPropertyUpdated, selectValueAccessor} from '../shared'; 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 {FormGroupDirective} from './form_group_directive';
import {FormArrayName, FormGroupName} from './form_group_name'; import {FormArrayName, FormGroupName} from './form_group_name';
@ -110,12 +110,13 @@ export class FormControlName extends NgControl implements OnChanges, OnDestroy {
constructor( constructor(
@Optional() @Host() @SkipSelf() private _parent: ControlContainer, @Optional() @Host() @SkipSelf() private _parent: ControlContainer,
@Optional() @Self() @Inject(NG_VALIDATORS) private _validators: @Optional() @Self() @Inject(NG_VALIDATORS) validators: Array<Validator|ValidatorFn>,
/* Array<Validator|Function> */ any[], @Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators:
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) private _asyncValidators: Array<Validator|AsyncValidatorFn>,
/* Array<Validator|Function> */ any[],
@Optional() @Self() @Inject(NG_VALUE_ACCESSOR) valueAccessors: ControlValueAccessor[]) { @Optional() @Self() @Inject(NG_VALUE_ACCESSOR) valueAccessors: ControlValueAccessor[]) {
super(); super();
this._rawValidators = validators || [];
this._rawAsyncValidators = asyncValidators || [];
this.valueAccessor = selectValueAccessor(this, valueAccessors); this.valueAccessor = selectValueAccessor(this, valueAccessors);
} }
@ -147,9 +148,11 @@ export class FormControlName extends NgControl implements OnChanges, OnDestroy {
get formDirective(): any { return this._parent ? this._parent.formDirective : null; } 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); } get control(): FormControl { return this.formDirective.getControl(this); }

View File

@ -124,7 +124,6 @@ export class FormGroupDirective extends ControlContainer implements Form,
var async = composeAsyncValidators(this._asyncValidators); var async = composeAsyncValidators(this._asyncValidators);
this.form.asyncValidator = Validators.composeAsync([this.form.asyncValidator, async]); this.form.asyncValidator = Validators.composeAsync([this.form.asyncValidator, async]);
this.form.updateValueAndValidity({onlySelf: true, emitEvent: false});
this._updateDomValue(changes); this._updateDomValue(changes);
} }
} }
@ -189,6 +188,7 @@ export class FormGroupDirective extends ControlContainer implements Form,
/** @internal */ /** @internal */
_updateDomValue(changes: SimpleChanges) { _updateDomValue(changes: SimpleChanges) {
const oldForm = changes['form'].previousValue; const oldForm = changes['form'].previousValue;
this.directives.forEach(dir => { this.directives.forEach(dir => {
const newCtrl: any = this.form.get(dir.path); const newCtrl: any = this.form.get(dir.path);
const oldCtrl = oldForm.get(dir.path); const oldCtrl = oldForm.get(dir.path);
@ -197,6 +197,8 @@ export class FormGroupDirective extends ControlContainer implements Form,
if (newCtrl) setUpControl(newCtrl, dir); if (newCtrl) setUpControl(newCtrl, dir);
} }
}); });
this.form._updateTreeValidity({emitEvent: false});
} }
private _checkFormPresent() { private _checkFormPresent() {

View File

@ -25,7 +25,7 @@ import {RadioControlValueAccessor} from './radio_control_value_accessor';
import {FormArrayName} from './reactive_directives/form_group_name'; import {FormArrayName} from './reactive_directives/form_group_name';
import {SelectControlValueAccessor} from './select_control_value_accessor'; import {SelectControlValueAccessor} from './select_control_value_accessor';
import {SelectMultipleControlValueAccessor} from './select_multiple_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[] { export function controlPath(name: string, parent: ControlContainer): string[] {
@ -49,6 +49,9 @@ export function setUpControl(control: FormControl, dir: NgControl): void {
control.setValue(newValue, {emitModelToViewChange: false}); control.setValue(newValue, {emitModelToViewChange: false});
}); });
// touched
dir.valueAccessor.registerOnTouched(() => control.markAsTouched());
control.registerOnChange((newValue: any, emitModelEvent: boolean) => { control.registerOnChange((newValue: any, emitModelEvent: boolean) => {
// control -> view // control -> view
dir.valueAccessor.writeValue(newValue); dir.valueAccessor.writeValue(newValue);
@ -62,13 +65,23 @@ export function setUpControl(control: FormControl, dir: NgControl): void {
(isDisabled: boolean) => { dir.valueAccessor.setDisabledState(isDisabled); }); (isDisabled: boolean) => { dir.valueAccessor.setDisabledState(isDisabled); });
} }
// touched // re-run validation when validator binding changes, e.g. minlength=3 -> minlength=4
dir.valueAccessor.registerOnTouched(() => control.markAsTouched()); 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) { export function cleanUpControl(control: FormControl, dir: NgControl) {
dir.valueAccessor.registerOnChange(() => _noControlError(dir)); dir.valueAccessor.registerOnChange(() => _noControlError(dir));
dir.valueAccessor.registerOnTouched(() => _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(); if (control) control._clearChangeFns();
} }

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license * 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 {isPresent} from '../facade/lang';
import {AbstractControl} from '../model'; import {AbstractControl} from '../model';
@ -33,7 +33,10 @@ import {NG_VALIDATORS, Validators} from '../validators';
* *
* @stable * @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 = { export const REQUIRED_VALIDATOR: any = {
provide: NG_VALIDATORS, provide: NG_VALIDATORS,
@ -60,15 +63,21 @@ export const REQUIRED_VALIDATOR: any = {
}) })
export class RequiredValidator implements Validator { export class RequiredValidator implements Validator {
private _required: boolean; private _required: boolean;
private _onChange: () => void;
@Input() @Input()
get required(): boolean { return this._required; } 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} { validate(c: AbstractControl): {[key: string]: any} {
return this.required ? Validators.required(c) : null; return this.required ? Validators.required(c) : null;
} }
registerOnChange(fn: () => void) { this._onChange = fn; }
} }
/** /**
@ -110,6 +119,7 @@ export const MIN_LENGTH_VALIDATOR: any = {
export class MinLengthValidator implements Validator, export class MinLengthValidator implements Validator,
OnChanges { OnChanges {
private _validator: ValidatorFn; private _validator: ValidatorFn;
private _onChange: () => void;
@Input() minlength: string; @Input() minlength: string;
@ -118,15 +128,17 @@ export class MinLengthValidator implements Validator,
} }
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
const minlengthChange = changes['minlength']; if (changes['minlength']) {
if (minlengthChange) {
this._createValidator(); this._createValidator();
if (this._onChange) this._onChange();
} }
} }
validate(c: AbstractControl): {[key: string]: any} { validate(c: AbstractControl): {[key: string]: any} {
return isPresent(this.minlength) ? this._validator(c) : null; return isPresent(this.minlength) ? this._validator(c) : null;
} }
registerOnChange(fn: () => void) { this._onChange = fn; }
} }
/** /**
@ -157,6 +169,7 @@ export const MAX_LENGTH_VALIDATOR: any = {
export class MaxLengthValidator implements Validator, export class MaxLengthValidator implements Validator,
OnChanges { OnChanges {
private _validator: ValidatorFn; private _validator: ValidatorFn;
private _onChange: () => void;
@Input() maxlength: string; @Input() maxlength: string;
@ -165,15 +178,17 @@ export class MaxLengthValidator implements Validator,
} }
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
const maxlengthChange = changes['maxlength']; if (changes['maxlength']) {
if (maxlengthChange) {
this._createValidator(); this._createValidator();
if (this._onChange) this._onChange();
} }
} }
validate(c: AbstractControl): {[key: string]: any} { validate(c: AbstractControl): {[key: string]: any} {
return isPresent(this.maxlength) ? this._validator(c) : null; return isPresent(this.maxlength) ? this._validator(c) : null;
} }
registerOnChange(fn: () => void) { this._onChange = fn; }
} }
@ -205,19 +220,22 @@ export const PATTERN_VALIDATOR: any = {
export class PatternValidator implements Validator, export class PatternValidator implements Validator,
OnChanges { OnChanges {
private _validator: ValidatorFn; private _validator: ValidatorFn;
private _onChange: () => void;
@Input() pattern: string; @Input() pattern: string;
private _createValidator() { this._validator = Validators.pattern(this.pattern); } private _createValidator() { this._validator = Validators.pattern(this.pattern); }
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
const patternChange = changes['pattern']; if (changes['pattern']) {
if (patternChange) {
this._createValidator(); this._createValidator();
if (this._onChange) this._onChange();
} }
} }
validate(c: AbstractControl): {[key: string]: any} { validate(c: AbstractControl): {[key: string]: any} {
return isPresent(this.pattern) ? this._validator(c) : null; return isPresent(this.pattern) ? this._validator(c) : null;
} }
registerOnChange(fn: () => void) { this._onChange = fn; }
} }

View File

@ -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} { private _runValidator(): {[key: string]: any} {
return isPresent(this.validator) ? this.validator(this) : null; return isPresent(this.validator) ? this.validator(this) : null;
} }

View File

@ -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([]);
});
});
}); });
} }

View File

@ -160,6 +160,30 @@ export function main() {
expect(inputs[0].nativeElement.value).toEqual('Bess'); expect(inputs[0].nativeElement.value).toEqual('Bess');
}); });
it('should pick up dir validators from form controls', () => {
const fixture = TestBed.createComponent(LoginIsEmptyWrapper);
const form = new FormGroup({
'login': new FormControl(''),
'min': new FormControl(''),
'max': new FormControl(''),
'pattern': new FormControl('')
});
fixture.debugElement.componentInstance.form = form;
fixture.detectChanges();
expect(form.get('login').errors).toEqual({required: true});
const newForm = new FormGroup({
'login': new FormControl(''),
'min': new FormControl(''),
'max': new FormControl(''),
'pattern': new FormControl('')
});
fixture.debugElement.componentInstance.form = newForm;
fixture.detectChanges();
expect(newForm.get('login').errors).toEqual({required: true});
});
it('should pick up dir validators from nested form groups', () => { it('should pick up dir validators from nested form groups', () => {
const fixture = TestBed.createComponent(NestedFormGroupComp); const fixture = TestBed.createComponent(NestedFormGroupComp);
const form = new FormGroup({ const form = new FormGroup({
@ -1024,7 +1048,7 @@ export function main() {
expect(form.valid).toEqual(true); expect(form.valid).toEqual(true);
}); });
it('changes on binded properties should change the validation state of the form', () => { it('changes on bound properties should change the validation state of the form', () => {
const fixture = TestBed.createComponent(ValidationBindingsForm); const fixture = TestBed.createComponent(ValidationBindingsForm);
const form = new FormGroup({ const form = new FormGroup({
'login': new FormControl(''), 'login': new FormControl(''),
@ -1087,11 +1111,6 @@ export function main() {
fixture.debugElement.componentInstance.pattern = null; fixture.debugElement.componentInstance.pattern = null;
fixture.detectChanges(); fixture.detectChanges();
dispatchEvent(required.nativeElement, 'input');
dispatchEvent(minLength.nativeElement, 'input');
dispatchEvent(maxLength.nativeElement, 'input');
dispatchEvent(pattern.nativeElement, 'input');
expect(form.hasError('required', ['login'])).toEqual(false); expect(form.hasError('required', ['login'])).toEqual(false);
expect(form.hasError('minlength', ['min'])).toEqual(false); expect(form.hasError('minlength', ['min'])).toEqual(false);
expect(form.hasError('maxlength', ['max'])).toEqual(false); expect(form.hasError('maxlength', ['max'])).toEqual(false);
@ -1104,6 +1123,43 @@ export function main() {
expect(required.nativeElement.getAttribute('pattern')).toEqual(null); expect(required.nativeElement.getAttribute('pattern')).toEqual(null);
}); });
it('should support rebound controls with rebound validators', () => {
const fixture = TestBed.createComponent(ValidationBindingsForm);
const form = new FormGroup({
'login': new FormControl(''),
'min': new FormControl(''),
'max': new FormControl(''),
'pattern': new FormControl('')
});
fixture.debugElement.componentInstance.form = form;
fixture.debugElement.componentInstance.required = true;
fixture.debugElement.componentInstance.minLen = 3;
fixture.debugElement.componentInstance.maxLen = 3;
fixture.debugElement.componentInstance.pattern = '.{3,}';
fixture.detectChanges();
const newForm = new FormGroup({
'login': new FormControl(''),
'min': new FormControl(''),
'max': new FormControl(''),
'pattern': new FormControl('')
});
fixture.debugElement.componentInstance.form = newForm;
fixture.detectChanges();
fixture.debugElement.componentInstance.required = false;
fixture.debugElement.componentInstance.minLen = null;
fixture.debugElement.componentInstance.maxLen = null;
fixture.debugElement.componentInstance.pattern = null;
fixture.detectChanges();
expect(newForm.hasError('required', ['login'])).toEqual(false);
expect(newForm.hasError('minlength', ['min'])).toEqual(false);
expect(newForm.hasError('maxlength', ['max'])).toEqual(false);
expect(newForm.hasError('pattern', ['pattern'])).toEqual(false);
expect(newForm.valid).toEqual(true);
});
it('should use async validators defined in the html', fakeAsync(() => { it('should use async validators defined in the html', fakeAsync(() => {
const fixture = TestBed.createComponent(UniqLoginWrapper); const fixture = TestBed.createComponent(UniqLoginWrapper);
const form = new FormGroup({'login': new FormControl('')}); const form = new FormGroup({'login': new FormControl('')});

View File

@ -9,7 +9,6 @@
import {NgFor, NgIf} from '@angular/common'; import {NgFor, NgIf} from '@angular/common';
import {Component, Input} from '@angular/core'; import {Component, Input} from '@angular/core';
import {TestBed, async, fakeAsync, tick} from '@angular/core/testing'; import {TestBed, async, fakeAsync, tick} from '@angular/core/testing';
import {beforeEach, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '@angular/core/testing/testing_internal';
import {ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR, NgForm} from '@angular/forms'; import {ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR, NgForm} from '@angular/forms';
import {By} from '@angular/platform-browser/src/dom/debug/by'; import {By} from '@angular/platform-browser/src/dom/debug/by';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
@ -25,7 +24,8 @@ export function main() {
declarations: [ declarations: [
StandaloneNgModel, NgModelForm, NgModelGroupForm, NgModelValidBinding, NgModelNgIfForm, StandaloneNgModel, NgModelForm, NgModelGroupForm, NgModelValidBinding, NgModelNgIfForm,
NgModelRadioForm, NgModelSelectForm, NgNoFormComp, InvalidNgModelNoName, NgModelRadioForm, NgModelSelectForm, NgNoFormComp, InvalidNgModelNoName,
NgModelOptionsStandalone, NgModelCustomComp, NgModelCustomWrapper NgModelOptionsStandalone, NgModelCustomComp, NgModelCustomWrapper,
NgModelValidationBindings
], ],
imports: [FormsModule] imports: [FormsModule]
}); });
@ -574,6 +574,125 @@ export function main() {
}); });
describe('validation directives', () => {
it('should support dir validators using bindings', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelValidationBindings);
fixture.debugElement.componentInstance.required = true;
fixture.debugElement.componentInstance.minLen = 3;
fixture.debugElement.componentInstance.maxLen = 3;
fixture.debugElement.componentInstance.pattern = '.{3,}';
fixture.detectChanges();
tick();
const required = fixture.debugElement.query(By.css('[name=required]'));
const minLength = fixture.debugElement.query(By.css('[name=minlength]'));
const maxLength = fixture.debugElement.query(By.css('[name=maxlength]'));
const pattern = fixture.debugElement.query(By.css('[name=pattern]'));
required.nativeElement.value = '';
minLength.nativeElement.value = '1';
maxLength.nativeElement.value = '1234';
pattern.nativeElement.value = '12';
dispatchEvent(required.nativeElement, 'input');
dispatchEvent(minLength.nativeElement, 'input');
dispatchEvent(maxLength.nativeElement, 'input');
dispatchEvent(pattern.nativeElement, 'input');
fixture.detectChanges();
const form = fixture.debugElement.children[0].injector.get(NgForm);
expect(form.control.hasError('required', ['required'])).toEqual(true);
expect(form.control.hasError('minlength', ['minlength'])).toEqual(true);
expect(form.control.hasError('maxlength', ['maxlength'])).toEqual(true);
expect(form.control.hasError('pattern', ['pattern'])).toEqual(true);
required.nativeElement.value = '1';
minLength.nativeElement.value = '123';
maxLength.nativeElement.value = '123';
pattern.nativeElement.value = '123';
dispatchEvent(required.nativeElement, 'input');
dispatchEvent(minLength.nativeElement, 'input');
dispatchEvent(maxLength.nativeElement, 'input');
dispatchEvent(pattern.nativeElement, 'input');
expect(form.valid).toEqual(true);
}));
it('changes on bound properties should change the validation state of the form',
fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelValidationBindings);
fixture.detectChanges();
tick();
const required = fixture.debugElement.query(By.css('[name=required]'));
const minLength = fixture.debugElement.query(By.css('[name=minlength]'));
const maxLength = fixture.debugElement.query(By.css('[name=maxlength]'));
const pattern = fixture.debugElement.query(By.css('[name=pattern]'));
required.nativeElement.value = '';
minLength.nativeElement.value = '1';
maxLength.nativeElement.value = '1234';
pattern.nativeElement.value = '12';
dispatchEvent(required.nativeElement, 'input');
dispatchEvent(minLength.nativeElement, 'input');
dispatchEvent(maxLength.nativeElement, 'input');
dispatchEvent(pattern.nativeElement, 'input');
const form = fixture.debugElement.children[0].injector.get(NgForm);
expect(form.control.hasError('required', ['required'])).toEqual(false);
expect(form.control.hasError('minlength', ['minlength'])).toEqual(false);
expect(form.control.hasError('maxlength', ['maxlength'])).toEqual(false);
expect(form.control.hasError('pattern', ['pattern'])).toEqual(false);
expect(form.valid).toEqual(true);
fixture.debugElement.componentInstance.required = true;
fixture.debugElement.componentInstance.minLen = 3;
fixture.debugElement.componentInstance.maxLen = 3;
fixture.debugElement.componentInstance.pattern = '.{3,}';
fixture.detectChanges();
dispatchEvent(required.nativeElement, 'input');
dispatchEvent(minLength.nativeElement, 'input');
dispatchEvent(maxLength.nativeElement, 'input');
dispatchEvent(pattern.nativeElement, 'input');
expect(form.control.hasError('required', ['required'])).toEqual(true);
expect(form.control.hasError('minlength', ['minlength'])).toEqual(true);
expect(form.control.hasError('maxlength', ['maxlength'])).toEqual(true);
expect(form.control.hasError('pattern', ['pattern'])).toEqual(true);
expect(form.valid).toEqual(false);
expect(required.nativeElement.getAttribute('required')).toEqual('');
expect(fixture.debugElement.componentInstance.minLen.toString())
.toEqual(minLength.nativeElement.getAttribute('minlength'));
expect(fixture.debugElement.componentInstance.maxLen.toString())
.toEqual(maxLength.nativeElement.getAttribute('maxlength'));
expect(fixture.debugElement.componentInstance.pattern.toString())
.toEqual(pattern.nativeElement.getAttribute('pattern'));
fixture.debugElement.componentInstance.required = false;
fixture.debugElement.componentInstance.minLen = null;
fixture.debugElement.componentInstance.maxLen = null;
fixture.debugElement.componentInstance.pattern = null;
fixture.detectChanges();
expect(form.control.hasError('required', ['required'])).toEqual(false);
expect(form.control.hasError('minlength', ['minlength'])).toEqual(false);
expect(form.control.hasError('maxlength', ['maxlength'])).toEqual(false);
expect(form.control.hasError('pattern', ['pattern'])).toEqual(false);
expect(form.valid).toEqual(true);
expect(required.nativeElement.getAttribute('required')).toEqual(null);
expect(required.nativeElement.getAttribute('minlength')).toEqual(null);
expect(required.nativeElement.getAttribute('maxlength')).toEqual(null);
expect(required.nativeElement.getAttribute('pattern')).toEqual(null);
}));
});
describe('ngModel corner cases', () => { describe('ngModel corner cases', () => {
it('should update the view when the model is set back to what used to be in the view', it('should update the view when the model is set back to what used to be in the view',
fakeAsync(() => { fakeAsync(() => {
@ -791,6 +910,24 @@ class NgModelCustomWrapper {
isDisabled = false; isDisabled = false;
} }
@Component({
selector: 'ng-model-validation-bindings',
template: `
<form>
<input name="required" ngModel [required]="required">
<input name="minlength" ngModel [minlength]="minLen">
<input name="maxlength" ngModel [maxlength]="maxLen">
<input name="pattern" ngModel [pattern]="pattern">
</form>
`
})
class NgModelValidationBindings {
required: boolean;
minLen: number;
maxLen: number;
pattern: string;
}
function sortedClassList(el: HTMLElement) { function sortedClassList(el: HTMLElement) {
var l = getDOM().classList(el); var l = getDOM().classList(el);
ListWrapper.sort(l); ListWrapper.sort(l);

View File

@ -229,7 +229,7 @@ export declare class FormControlDirective extends NgControl implements OnChanges
update: EventEmitter<{}>; update: EventEmitter<{}>;
validator: ValidatorFn; validator: ValidatorFn;
viewModel: any; viewModel: any;
constructor(_validators: any[], _asyncValidators: any[], valueAccessors: ControlValueAccessor[]); constructor(validators: Array<Validator | ValidatorFn>, asyncValidators: Array<Validator | AsyncValidatorFn>, valueAccessors: ControlValueAccessor[]);
ngOnChanges(changes: SimpleChanges): void; ngOnChanges(changes: SimpleChanges): void;
viewToModelUpdate(newValue: any): void; viewToModelUpdate(newValue: any): void;
} }
@ -245,7 +245,7 @@ export declare class FormControlName extends NgControl implements OnChanges, OnD
path: string[]; path: string[];
update: EventEmitter<{}>; update: EventEmitter<{}>;
validator: ValidatorFn; validator: ValidatorFn;
constructor(_parent: ControlContainer, _validators: any[], _asyncValidators: any[], valueAccessors: ControlValueAccessor[]); constructor(_parent: ControlContainer, validators: Array<Validator | ValidatorFn>, asyncValidators: Array<Validator | AsyncValidatorFn>, valueAccessors: ControlValueAccessor[]);
ngOnChanges(changes: SimpleChanges): void; ngOnChanges(changes: SimpleChanges): void;
ngOnDestroy(): void; ngOnDestroy(): void;
viewToModelUpdate(newValue: any): void; viewToModelUpdate(newValue: any): void;
@ -319,6 +319,7 @@ export declare class FormsModule {
export declare class MaxLengthValidator implements Validator, OnChanges { export declare class MaxLengthValidator implements Validator, OnChanges {
maxlength: string; maxlength: string;
ngOnChanges(changes: SimpleChanges): void; ngOnChanges(changes: SimpleChanges): void;
registerOnChange(fn: () => void): void;
validate(c: AbstractControl): { validate(c: AbstractControl): {
[key: string]: any; [key: string]: any;
}; };
@ -328,6 +329,7 @@ export declare class MaxLengthValidator implements Validator, OnChanges {
export declare class MinLengthValidator implements Validator, OnChanges { export declare class MinLengthValidator implements Validator, OnChanges {
minlength: string; minlength: string;
ngOnChanges(changes: SimpleChanges): void; ngOnChanges(changes: SimpleChanges): void;
registerOnChange(fn: () => void): void;
validate(c: AbstractControl): { validate(c: AbstractControl): {
[key: string]: any; [key: string]: any;
}; };
@ -404,7 +406,7 @@ export declare class NgModel extends NgControl implements OnChanges, OnDestroy {
update: EventEmitter<{}>; update: EventEmitter<{}>;
validator: ValidatorFn; validator: ValidatorFn;
viewModel: any; viewModel: any;
constructor(_parent: ControlContainer, _validators: any[], _asyncValidators: any[], valueAccessors: ControlValueAccessor[]); constructor(_parent: ControlContainer, validators: Array<Validator | ValidatorFn>, asyncValidators: Array<Validator | AsyncValidatorFn>, valueAccessors: ControlValueAccessor[]);
ngOnChanges(changes: SimpleChanges): void; ngOnChanges(changes: SimpleChanges): void;
ngOnDestroy(): void; ngOnDestroy(): void;
viewToModelUpdate(newValue: any): void; viewToModelUpdate(newValue: any): void;
@ -429,6 +431,7 @@ export declare class NgSelectOption implements OnDestroy {
export declare class PatternValidator implements Validator, OnChanges { export declare class PatternValidator implements Validator, OnChanges {
pattern: string; pattern: string;
ngOnChanges(changes: SimpleChanges): void; ngOnChanges(changes: SimpleChanges): void;
registerOnChange(fn: () => void): void;
validate(c: AbstractControl): { validate(c: AbstractControl): {
[key: string]: any; [key: string]: any;
}; };
@ -441,6 +444,7 @@ export declare class ReactiveFormsModule {
/** @stable */ /** @stable */
export declare class RequiredValidator implements Validator { export declare class RequiredValidator implements Validator {
required: boolean; required: boolean;
registerOnChange(fn: () => void): void;
validate(c: AbstractControl): { validate(c: AbstractControl): {
[key: string]: any; [key: string]: any;
}; };
@ -472,6 +476,7 @@ export declare class SelectMultipleControlValueAccessor implements ControlValueA
/** @stable */ /** @stable */
export interface Validator { export interface Validator {
registerOnChange?(fn: () => void): void;
validate(c: AbstractControl): { validate(c: AbstractControl): {
[key: string]: any; [key: string]: any;
}; };