fix(forms): allow optional fields with pattern and minlength validators (#12147)

This commit is contained in:
Pawel Kozlowski 2016-10-10 18:17:45 +02:00 committed by Tobias Bosch
parent aa92512ac6
commit d22eeb70b8
3 changed files with 98 additions and 46 deletions

View File

@ -11,11 +11,13 @@ import {toPromise} from 'rxjs/operator/toPromise';
import {AsyncValidatorFn, ValidatorFn} from './directives/validators'; import {AsyncValidatorFn, ValidatorFn} from './directives/validators';
import {StringMapWrapper} from './facade/collection'; import {StringMapWrapper} from './facade/collection';
import {isBlank, isPresent, isString} from './facade/lang'; import {isPresent} from './facade/lang';
import {AbstractControl} from './model'; import {AbstractControl} from './model';
import {isPromise} from './private_import_core'; import {isPromise} from './private_import_core';
function isEmptyInputValue(value: any) {
return value == null || typeof value === 'string' && value.length === 0;
}
/** /**
* Providers for validators to be used for {@link FormControl}s in a form. * Providers for validators to be used for {@link FormControl}s in a form.
@ -60,9 +62,7 @@ export class Validators {
* Validator that requires controls to have a non-empty value. * Validator that requires controls to have a non-empty value.
*/ */
static required(control: AbstractControl): {[key: string]: boolean} { static required(control: AbstractControl): {[key: string]: boolean} {
return isBlank(control.value) || (isString(control.value) && control.value == '') ? return isEmptyInputValue(control.value) ? {'required': true} : null;
{'required': true} :
null;
} }
/** /**
@ -70,6 +70,9 @@ export class Validators {
*/ */
static minLength(minLength: number): ValidatorFn { static minLength(minLength: number): ValidatorFn {
return (control: AbstractControl): {[key: string]: any} => { return (control: AbstractControl): {[key: string]: any} => {
if (isEmptyInputValue(control.value)) {
return null; // don't validate empty values to allow optional controls
}
const length = typeof control.value === 'string' ? control.value.length : 0; const length = typeof control.value === 'string' ? control.value.length : 0;
return length < minLength ? return length < minLength ?
{'minlength': {'requiredLength': minLength, 'actualLength': length}} : {'minlength': {'requiredLength': minLength, 'actualLength': length}} :
@ -94,10 +97,14 @@ export class Validators {
*/ */
static pattern(pattern: string): ValidatorFn { static pattern(pattern: string): ValidatorFn {
return (control: AbstractControl): {[key: string]: any} => { return (control: AbstractControl): {[key: string]: any} => {
let regex = new RegExp(`^${pattern}$`); if (isEmptyInputValue(control.value)) {
let v: string = control.value; return null; // don't validate empty values to allow optional controls
return regex.test(v) ? null : }
{'pattern': {'requiredPattern': `^${pattern}$`, 'actualValue': v}}; const regex = new RegExp(`^${pattern}$`);
const value: string = control.value;
return regex.test(value) ?
null :
{'pattern': {'requiredPattern': `^${pattern}$`, 'actualValue': value}};
}; };
} }

View File

@ -22,7 +22,7 @@ export function main() {
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 NgModelValidationBindings, NgModelMultipleValidators
], ],
imports: [FormsModule] imports: [FormsModule]
}); });
@ -728,6 +728,50 @@ export function main() {
expect(form.valid).toEqual(true); expect(form.valid).toEqual(true);
})); }));
it('should support optional fields with pattern validator', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelMultipleValidators);
fixture.componentInstance.required = false;
fixture.componentInstance.pattern = '[a-z]+';
fixture.detectChanges();
tick();
const form = fixture.debugElement.children[0].injector.get(NgForm);
const input = fixture.debugElement.query(By.css('input'));
input.nativeElement.value = '';
dispatchEvent(input.nativeElement, 'input');
fixture.detectChanges();
expect(form.valid).toBeTruthy();
input.nativeElement.value = '1';
dispatchEvent(input.nativeElement, 'input');
fixture.detectChanges();
expect(form.valid).toBeFalsy();
expect(form.control.hasError('pattern', ['tovalidate'])).toBeTruthy();
}));
it('should support optional fields with minlength validator', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelMultipleValidators);
fixture.componentInstance.required = false;
fixture.componentInstance.minLen = 2;
fixture.detectChanges();
tick();
const form = fixture.debugElement.children[0].injector.get(NgForm);
const input = fixture.debugElement.query(By.css('input'));
input.nativeElement.value = '';
dispatchEvent(input.nativeElement, 'input');
fixture.detectChanges();
expect(form.valid).toBeTruthy();
input.nativeElement.value = '1';
dispatchEvent(input.nativeElement, 'input');
fixture.detectChanges();
expect(form.valid).toBeFalsy();
expect(form.control.hasError('minlength', ['tovalidate'])).toBeTruthy();
}));
it('changes on bound properties should change the validation state of the form', it('changes on bound properties should change the validation state of the form',
fakeAsync(() => { fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelValidationBindings); const fixture = TestBed.createComponent(NgModelValidationBindings);
@ -1037,6 +1081,20 @@ class NgModelValidationBindings {
pattern: string; pattern: string;
} }
@Component({
selector: 'ng-model-multiple-validators',
template: `
<form>
<input name="tovalidate" ngModel [required]="required" [minlength]="minLen" [pattern]="pattern">
</form>
`
})
class NgModelMultipleValidators {
required: boolean;
minLen: number;
pattern: string;
}
function sortedClassList(el: HTMLElement) { function sortedClassList(el: HTMLElement) {
const l = getDOM().classList(el); const l = getDOM().classList(el);
l.sort(); l.sort();

View File

@ -44,33 +44,24 @@ export function main() {
() => { expect(Validators.required(new FormControl(null))).toEqual({'required': true}); }); () => { expect(Validators.required(new FormControl(null))).toEqual({'required': true}); });
it('should not error on a non-empty string', it('should not error on a non-empty string',
() => { expect(Validators.required(new FormControl('not empty'))).toEqual(null); }); () => { expect(Validators.required(new FormControl('not empty'))).toBeNull(); });
it('should accept zero as valid', it('should accept zero as valid',
() => { expect(Validators.required(new FormControl(0))).toEqual(null); }); () => { expect(Validators.required(new FormControl(0))).toBeNull(); });
}); });
describe('minLength', () => { describe('minLength', () => {
it('should error on an empty string', () => { it('should not error on an empty string',
expect(Validators.minLength(2)(new FormControl(''))).toEqual({ () => { expect(Validators.minLength(2)(new FormControl(''))).toBeNull(); });
'minlength': {'requiredLength': 2, 'actualLength': 0}
});
});
it('should error on null', () => { it('should not error on null',
expect(Validators.minLength(2)(new FormControl(null))).toEqual({ () => { expect(Validators.minLength(2)(new FormControl(null))).toBeNull(); });
'minlength': {'requiredLength': 2, 'actualLength': 0}
});
});
it('should error on undefined', () => { it('should not error on undefined',
expect(Validators.minLength(2)(new FormControl(null))).toEqual({ () => { expect(Validators.minLength(2)(new FormControl(null))).toBeNull(); });
'minlength': {'requiredLength': 2, 'actualLength': 0}
});
});
it('should not error on valid strings', it('should not error on valid strings',
() => { expect(Validators.minLength(2)(new FormControl('aa'))).toEqual(null); }); () => { expect(Validators.minLength(2)(new FormControl('aa'))).toBeNull(); });
it('should error on short strings', () => { it('should error on short strings', () => {
expect(Validators.minLength(2)(new FormControl('a'))).toEqual({ expect(Validators.minLength(2)(new FormControl('a'))).toEqual({
@ -81,13 +72,13 @@ export function main() {
describe('maxLength', () => { describe('maxLength', () => {
it('should not error on an empty string', it('should not error on an empty string',
() => { expect(Validators.maxLength(2)(new FormControl(''))).toEqual(null); }); () => { expect(Validators.maxLength(2)(new FormControl(''))).toBeNull(); });
it('should not error on null', it('should not error on null',
() => { expect(Validators.maxLength(2)(new FormControl(null))).toEqual(null); }); () => { expect(Validators.maxLength(2)(new FormControl(null))).toBeNull(); });
it('should not error on valid strings', it('should not error on valid strings',
() => { expect(Validators.maxLength(2)(new FormControl('aa'))).toEqual(null); }); () => { expect(Validators.maxLength(2)(new FormControl('aa'))).toBeNull(); });
it('should error on long strings', () => { it('should error on long strings', () => {
expect(Validators.maxLength(2)(new FormControl('aaa'))).toEqual({ expect(Validators.maxLength(2)(new FormControl('aaa'))).toEqual({
@ -98,29 +89,25 @@ export function main() {
describe('pattern', () => { describe('pattern', () => {
it('should not error on an empty string', it('should not error on an empty string',
() => { expect(Validators.pattern('[a-zA-Z ]*')(new FormControl(''))).toEqual(null); }); () => { expect(Validators.pattern('[a-zA-Z ]+')(new FormControl(''))).toBeNull(); });
it('should not error on null', it('should not error on null',
() => { expect(Validators.pattern('[a-zA-Z ]*')(new FormControl(null))).toEqual(null); }); () => { expect(Validators.pattern('[a-zA-Z ]+')(new FormControl(null))).toBeNull(); });
it('should not error on undefined',
() => { expect(Validators.pattern('[a-zA-Z ]+')(new FormControl(null))).toBeNull(); });
it('should not error on null value and "null" pattern', it('should not error on null value and "null" pattern',
() => { expect(Validators.pattern('null')(new FormControl(null))).toEqual(null); }); () => { expect(Validators.pattern('null')(new FormControl(null))).toBeNull(); });
it('should not error on valid strings', () => { it('should not error on valid strings',
expect(Validators.pattern('[a-zA-Z ]*')(new FormControl('aaAA'))).toEqual(null); () => { expect(Validators.pattern('[a-zA-Z ]*')(new FormControl('aaAA'))).toBeNull(); });
});
it('should error on failure to match string', () => { it('should error on failure to match string', () => {
expect(Validators.pattern('[a-zA-Z ]*')(new FormControl('aaa0'))).toEqual({ expect(Validators.pattern('[a-zA-Z ]*')(new FormControl('aaa0'))).toEqual({
'pattern': {'requiredPattern': '^[a-zA-Z ]*$', 'actualValue': 'aaa0'} 'pattern': {'requiredPattern': '^[a-zA-Z ]*$', 'actualValue': 'aaa0'}
}); });
}); });
it('should error on failure to match empty string', () => {
expect(Validators.pattern('[a-zA-Z]+')(new FormControl(''))).toEqual({
'pattern': {'requiredPattern': '^[a-zA-Z]+$', 'actualValue': ''}
});
});
}); });
describe('compose', () => { describe('compose', () => {
@ -139,7 +126,7 @@ export function main() {
it('should return null when no errors', () => { it('should return null when no errors', () => {
var c = Validators.compose([Validators.nullValidator, Validators.nullValidator]); var c = Validators.compose([Validators.nullValidator, Validators.nullValidator]);
expect(c(new FormControl(''))).toEqual(null); expect(c(new FormControl(''))).toBeNull();
}); });
it('should ignore nulls', () => { it('should ignore nulls', () => {
@ -166,7 +153,7 @@ export function main() {
} }
it('should return null when given null', it('should return null when given null',
() => { expect(Validators.composeAsync(null)).toEqual(null); }); () => { expect(Validators.composeAsync(null)).toBeNull(); });
it('should collect errors from all the validators', fakeAsync(() => { it('should collect errors from all the validators', fakeAsync(() => {
var c = Validators.composeAsync([ var c = Validators.composeAsync([
@ -199,7 +186,7 @@ export function main() {
(<Promise<any>>c(new FormControl('expected'))).then(v => value = v); (<Promise<any>>c(new FormControl('expected'))).then(v => value = v);
tick(1); tick(1);
expect(value).toEqual(null); expect(value).toBeNull();
})); }));
it('should ignore nulls', fakeAsync(() => { it('should ignore nulls', fakeAsync(() => {