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 {ControlValueAccessor} from './control_value_accessor';
import {AsyncValidatorFn, ValidatorFn} from './validators';
import {AsyncValidatorFn, Validator, ValidatorFn} from './validators';
function unimplemented(): any {
throw new Error('unimplemented');
@ -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(); }

View File

@ -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,
@ -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);
}
@ -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 {

View File

@ -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,
@ -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);
}
@ -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; }

View File

@ -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';
@ -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);
}
@ -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); }

View File

@ -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);
}
}
@ -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);
@ -197,6 +197,8 @@ export class FormGroupDirective extends ControlContainer implements Form,
if (newCtrl) setUpControl(newCtrl, dir);
}
});
this.form._updateTreeValidity({emitEvent: false});
}
private _checkFormPresent() {

View File

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

View File

@ -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';
@ -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,
@ -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; }
}
/**
@ -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;
@ -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; }
}
/**
@ -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;
@ -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; }
}
@ -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; }
}

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} {
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');
});
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', () => {
const fixture = TestBed.createComponent(NestedFormGroupComp);
const form = new FormGroup({
@ -1024,7 +1048,7 @@ export function main() {
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 form = new FormGroup({
'login': new FormControl(''),
@ -1087,11 +1111,6 @@ export function main() {
fixture.debugElement.componentInstance.pattern = null;
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('minlength', ['min'])).toEqual(false);
expect(form.hasError('maxlength', ['max'])).toEqual(false);
@ -1104,6 +1123,43 @@ export function main() {
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(() => {
const fixture = TestBed.createComponent(UniqLoginWrapper);
const form = new FormGroup({'login': new FormControl('')});

View File

@ -9,7 +9,6 @@
import {NgFor, NgIf} from '@angular/common';
import {Component, Input} from '@angular/core';
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 {By} from '@angular/platform-browser/src/dom/debug/by';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
@ -25,7 +24,8 @@ export function main() {
declarations: [
StandaloneNgModel, NgModelForm, NgModelGroupForm, NgModelValidBinding, NgModelNgIfForm,
NgModelRadioForm, NgModelSelectForm, NgNoFormComp, InvalidNgModelNoName,
NgModelOptionsStandalone, NgModelCustomComp, NgModelCustomWrapper
NgModelOptionsStandalone, NgModelCustomComp, NgModelCustomWrapper,
NgModelValidationBindings
],
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', () => {
it('should update the view when the model is set back to what used to be in the view',
fakeAsync(() => {
@ -791,6 +910,24 @@ class NgModelCustomWrapper {
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) {
var l = getDOM().classList(el);
ListWrapper.sort(l);

View File

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