diff --git a/modules/@angular/forms/src/directives/ng_control.ts b/modules/@angular/forms/src/directives/ng_control.ts index bec931662d..0e42fe1d3c 100644 --- a/modules/@angular/forms/src/directives/ng_control.ts +++ b/modules/@angular/forms/src/directives/ng_control.ts @@ -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 = []; + /** @internal */ + _rawAsyncValidators: Array = []; get validator(): ValidatorFn { return unimplemented(); } get asyncValidator(): AsyncValidatorFn { return unimplemented(); } diff --git a/modules/@angular/forms/src/directives/ng_model.ts b/modules/@angular/forms/src/directives/ng_model.ts index 5e96bcf857..5e1d23db63 100644 --- a/modules/@angular/forms/src/directives/ng_model.ts +++ b/modules/@angular/forms/src/directives/ng_model.ts @@ -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, + @Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: Array, @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 { diff --git a/modules/@angular/forms/src/directives/reactive_directives/form_control_directive.ts b/modules/@angular/forms/src/directives/reactive_directives/form_control_directive.ts index f7bcd2a15e..07acf6db9a 100644 --- a/modules/@angular/forms/src/directives/reactive_directives/form_control_directive.ts +++ b/modules/@angular/forms/src/directives/reactive_directives/form_control_directive.ts @@ -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 */ any[], - @Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) private _asyncValidators: - /* Array */ any[], + constructor(@Optional() @Self() @Inject(NG_VALIDATORS) validators: Array, + @Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: Array, @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; } diff --git a/modules/@angular/forms/src/directives/reactive_directives/form_control_name.ts b/modules/@angular/forms/src/directives/reactive_directives/form_control_name.ts index 668f2533fe..1569c5ebd1 100644 --- a/modules/@angular/forms/src/directives/reactive_directives/form_control_name.ts +++ b/modules/@angular/forms/src/directives/reactive_directives/form_control_name.ts @@ -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 */ any[], - @Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) private _asyncValidators: - /* Array */ any[], + @Optional() @Self() @Inject(NG_VALIDATORS) validators: Array, + @Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: + Array, @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); } diff --git a/modules/@angular/forms/src/directives/reactive_directives/form_group_directive.ts b/modules/@angular/forms/src/directives/reactive_directives/form_group_directive.ts index 14d140c5a0..b99a3536b5 100644 --- a/modules/@angular/forms/src/directives/reactive_directives/form_group_directive.ts +++ b/modules/@angular/forms/src/directives/reactive_directives/form_group_directive.ts @@ -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() { diff --git a/modules/@angular/forms/src/directives/shared.ts b/modules/@angular/forms/src/directives/shared.ts index 12b753073a..2ad20684ea 100644 --- a/modules/@angular/forms/src/directives/shared.ts +++ b/modules/@angular/forms/src/directives/shared.ts @@ -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).registerOnChange) + (validator).registerOnChange(() => control.updateValueAndValidity()); + }); + + dir._rawAsyncValidators.forEach((validator: Validator | ValidatorFn) => { + if ((validator).registerOnChange) + (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(); } diff --git a/modules/@angular/forms/src/directives/validators.ts b/modules/@angular/forms/src/directives/validators.ts index abfd16500b..7726cba257 100644 --- a/modules/@angular/forms/src/directives/validators.ts +++ b/modules/@angular/forms/src/directives/validators.ts @@ -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; } } diff --git a/modules/@angular/forms/src/model.ts b/modules/@angular/forms/src/model.ts index 159760b5ac..24aaffad8f 100644 --- a/modules/@angular/forms/src/model.ts +++ b/modules/@angular/forms/src/model.ts @@ -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; } diff --git a/modules/@angular/forms/test/form_group_spec.ts b/modules/@angular/forms/test/form_group_spec.ts index 4d9f6d326c..07aa646ee2 100644 --- a/modules/@angular/forms/test/form_group_spec.ts +++ b/modules/@angular/forms/test/form_group_spec.ts @@ -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([]); + }); + + }); + + }); } diff --git a/modules/@angular/forms/test/reactive_integration_spec.ts b/modules/@angular/forms/test/reactive_integration_spec.ts index 3a0df12547..3b6f9f435c 100644 --- a/modules/@angular/forms/test/reactive_integration_spec.ts +++ b/modules/@angular/forms/test/reactive_integration_spec.ts @@ -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('')}); diff --git a/modules/@angular/forms/test/template_integration_spec.ts b/modules/@angular/forms/test/template_integration_spec.ts index 1e857e0760..336e545134 100644 --- a/modules/@angular/forms/test/template_integration_spec.ts +++ b/modules/@angular/forms/test/template_integration_spec.ts @@ -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: ` +
+ + + + +
+ ` +}) +class NgModelValidationBindings { + required: boolean; + minLen: number; + maxLen: number; + pattern: string; +} + function sortedClassList(el: HTMLElement) { var l = getDOM().classList(el); ListWrapper.sort(l); diff --git a/tools/public_api_guard/forms/index.d.ts b/tools/public_api_guard/forms/index.d.ts index 0439da1371..b3fc571fdf 100644 --- a/tools/public_api_guard/forms/index.d.ts +++ b/tools/public_api_guard/forms/index.d.ts @@ -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, asyncValidators: Array, 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, asyncValidators: Array, 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, asyncValidators: Array, 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; };