diff --git a/packages/forms/src/directives/abstract_control_directive.ts b/packages/forms/src/directives/abstract_control_directive.ts index 5c20cda551..61e9bf84d9 100644 --- a/packages/forms/src/directives/abstract_control_directive.ts +++ b/packages/forms/src/directives/abstract_control_directive.ts @@ -31,7 +31,9 @@ export abstract class AbstractControlDirective { * @description * Reports the value of the control if it is present, otherwise null. */ - get value(): any { return this.control ? this.control.value : null; } + get value(): any { + return this.control ? this.control.value : null; + } /** * @description @@ -39,14 +41,18 @@ export abstract class AbstractControlDirective { * validation errors exist with the current value. * If the control is not present, null is returned. */ - get valid(): boolean|null { return this.control ? this.control.valid : null; } + get valid(): boolean|null { + return this.control ? this.control.valid : null; + } /** * @description * Reports whether the control is invalid, meaning that an error exists in the input value. * If the control is not present, null is returned. */ - get invalid(): boolean|null { return this.control ? this.control.invalid : null; } + get invalid(): boolean|null { + return this.control ? this.control.invalid : null; + } /** * @description @@ -54,7 +60,9 @@ export abstract class AbstractControlDirective { * errors are not yet available for the input value. If the control is not present, null is * returned. */ - get pending(): boolean|null { return this.control ? this.control.pending : null; } + get pending(): boolean|null { + return this.control ? this.control.pending : null; + } /** * @description @@ -62,41 +70,53 @@ export abstract class AbstractControlDirective { * in the UI and is exempt from validation checks and excluded from aggregate * values of ancestor controls. If the control is not present, null is returned. */ - get disabled(): boolean|null { return this.control ? this.control.disabled : null; } + get disabled(): boolean|null { + return this.control ? this.control.disabled : null; + } /** * @description * Reports whether the control is enabled, meaning that the control is included in ancestor * calculations of validity or value. If the control is not present, null is returned. */ - get enabled(): boolean|null { return this.control ? this.control.enabled : null; } + get enabled(): boolean|null { + return this.control ? this.control.enabled : null; + } /** * @description * Reports the control's validation errors. If the control is not present, null is returned. */ - get errors(): ValidationErrors|null { return this.control ? this.control.errors : null; } + get errors(): ValidationErrors|null { + return this.control ? this.control.errors : null; + } /** * @description * Reports whether the control is pristine, meaning that the user has not yet changed * the value in the UI. If the control is not present, null is returned. */ - get pristine(): boolean|null { return this.control ? this.control.pristine : null; } + get pristine(): boolean|null { + return this.control ? this.control.pristine : null; + } /** * @description * Reports whether the control is dirty, meaning that the user has changed * the value in the UI. If the control is not present, null is returned. */ - get dirty(): boolean|null { return this.control ? this.control.dirty : null; } + get dirty(): boolean|null { + return this.control ? this.control.dirty : null; + } /** * @description * Reports whether the control is touched, meaning that the user has triggered * a `blur` event on it. If the control is not present, null is returned. */ - get touched(): boolean|null { return this.control ? this.control.touched : null; } + get touched(): boolean|null { + return this.control ? this.control.touched : null; + } /** * @description @@ -104,14 +124,18 @@ export abstract class AbstractControlDirective { * 'VALID', 'INVALID', 'DISABLED', and 'PENDING'. * If the control is not present, null is returned. */ - get status(): string|null { return this.control ? this.control.status : null; } + get status(): string|null { + return this.control ? this.control.status : null; + } /** * @description * Reports whether the control is untouched, meaning that the user has not yet triggered * a `blur` event on it. If the control is not present, null is returned. */ - get untouched(): boolean|null { return this.control ? this.control.untouched : null; } + get untouched(): boolean|null { + return this.control ? this.control.untouched : null; + } /** * @description @@ -137,7 +161,9 @@ export abstract class AbstractControlDirective { * Returns an array that represents the path from the top-level form to this control. * Each index is the string name of the control on that level. */ - get path(): string[]|null { return null; } + get path(): string[]|null { + return null; + } /** * @description diff --git a/packages/forms/src/directives/abstract_form_group_directive.ts b/packages/forms/src/directives/abstract_form_group_directive.ts index 9a73f32656..6d7e5caddc 100644 --- a/packages/forms/src/directives/abstract_form_group_directive.ts +++ b/packages/forms/src/directives/abstract_form_group_directive.ts @@ -31,7 +31,7 @@ export class AbstractFormGroupDirective extends ControlContainer implements OnIn * @internal */ // TODO(issue/24571): remove '!'. - _parent !: ControlContainer; + _parent!: ControlContainer; /** * @description @@ -40,7 +40,7 @@ export class AbstractFormGroupDirective extends ControlContainer implements OnIn * @internal */ // TODO(issue/24571): remove '!'. - _validators !: any[]; + _validators!: any[]; /** * @description @@ -49,7 +49,7 @@ export class AbstractFormGroupDirective extends ControlContainer implements OnIn * @internal */ // TODO(issue/24571): remove '!'. - _asyncValidators !: any[]; + _asyncValidators!: any[]; /** * @description @@ -58,7 +58,7 @@ export class AbstractFormGroupDirective extends ControlContainer implements OnIn */ ngOnInit(): void { this._checkParentType(); - this.formDirective !.addFormGroup(this); + this.formDirective!.addFormGroup(this); } /** @@ -76,7 +76,9 @@ export class AbstractFormGroupDirective extends ControlContainer implements OnIn * @description * The `FormGroup` bound to this directive. */ - get control(): FormGroup { return this.formDirective !.getFormGroup(this); } + get control(): FormGroup { + return this.formDirective!.getFormGroup(this); + } /** * @description @@ -90,13 +92,17 @@ export class AbstractFormGroupDirective extends ControlContainer implements OnIn * @description * The top-level directive for this group if present, otherwise null. */ - get formDirective(): Form|null { return this._parent ? this._parent.formDirective : null; } + get formDirective(): Form|null { + return this._parent ? this._parent.formDirective : null; + } /** * @description * The synchronous validators registered with this group. */ - get validator(): ValidatorFn|null { return composeValidators(this._validators); } + get validator(): ValidatorFn|null { + return composeValidators(this._validators); + } /** * @description diff --git a/packages/forms/src/directives/checkbox_value_accessor.ts b/packages/forms/src/directives/checkbox_value_accessor.ts index 9a52622266..4a1dd17654 100644 --- a/packages/forms/src/directives/checkbox_value_accessor.ts +++ b/packages/forms/src/directives/checkbox_value_accessor.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directive, ElementRef, Renderer2, forwardRef} from '@angular/core'; +import {Directive, ElementRef, forwardRef, Renderer2} from '@angular/core'; import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor'; @@ -75,7 +75,9 @@ export class CheckboxControlValueAccessor implements ControlValueAccessor { * * @param fn The callback function */ - registerOnChange(fn: (_: any) => {}): void { this.onChange = fn; } + registerOnChange(fn: (_: any) => {}): void { + this.onChange = fn; + } /** * @description @@ -83,7 +85,9 @@ export class CheckboxControlValueAccessor implements ControlValueAccessor { * * @param fn The callback function */ - registerOnTouched(fn: () => {}): void { this.onTouched = fn; } + registerOnTouched(fn: () => {}): void { + this.onTouched = fn; + } /** * Sets the "disabled" property on the input element. diff --git a/packages/forms/src/directives/control_container.ts b/packages/forms/src/directives/control_container.ts index 254d3c7827..678b18f473 100644 --- a/packages/forms/src/directives/control_container.ts +++ b/packages/forms/src/directives/control_container.ts @@ -23,17 +23,21 @@ export abstract class ControlContainer extends AbstractControlDirective { * The name for the control */ // TODO(issue/24571): remove '!'. - name !: string | number | null; + name!: string|number|null; /** * @description * The top-level form directive for the control. */ - get formDirective(): Form|null { return null; } + get formDirective(): Form|null { + return null; + } /** * @description * The path to this group. */ - get path(): string[]|null { return null; } + get path(): string[]|null { + return null; + } } diff --git a/packages/forms/src/directives/default_value_accessor.ts b/packages/forms/src/directives/default_value_accessor.ts index 1e946e6795..b900b5fbe3 100644 --- a/packages/forms/src/directives/default_value_accessor.ts +++ b/packages/forms/src/directives/default_value_accessor.ts @@ -7,7 +7,8 @@ */ import {ɵgetDOM as getDOM} from '@angular/common'; -import {Directive, ElementRef, Inject, InjectionToken, Optional, Renderer2, forwardRef} from '@angular/core'; +import {Directive, ElementRef, forwardRef, Inject, InjectionToken, Optional, Renderer2} from '@angular/core'; + import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor'; export const DEFAULT_VALUE_ACCESSOR: any = { @@ -112,7 +113,9 @@ export class DefaultValueAccessor implements ControlValueAccessor { * * @param fn The callback function */ - registerOnChange(fn: (_: any) => void): void { this.onChange = fn; } + registerOnChange(fn: (_: any) => void): void { + this.onChange = fn; + } /** * @description @@ -120,7 +123,9 @@ export class DefaultValueAccessor implements ControlValueAccessor { * * @param fn The callback function */ - registerOnTouched(fn: () => void): void { this.onTouched = fn; } + registerOnTouched(fn: () => void): void { + this.onTouched = fn; + } /** * Sets the "disabled" property on the input element. @@ -139,7 +144,9 @@ export class DefaultValueAccessor implements ControlValueAccessor { } /** @internal */ - _compositionStart(): void { this._composing = true; } + _compositionStart(): void { + this._composing = true; + } /** @internal */ _compositionEnd(value: any): void { diff --git a/packages/forms/src/directives/ng_control.ts b/packages/forms/src/directives/ng_control.ts index de09f7ff83..47e92f6db4 100644 --- a/packages/forms/src/directives/ng_control.ts +++ b/packages/forms/src/directives/ng_control.ts @@ -66,7 +66,9 @@ export abstract class NgControl extends AbstractControlDirective { * * @throws An exception that this method is not implemented */ - get validator(): ValidatorFn|null { return unimplemented(); } + get validator(): ValidatorFn|null { + return unimplemented(); + } /** * @description @@ -74,7 +76,9 @@ export abstract class NgControl extends AbstractControlDirective { * * @throws An exception that this method is not implemented */ - get asyncValidator(): AsyncValidatorFn|null { return unimplemented(); } + get asyncValidator(): AsyncValidatorFn|null { + return unimplemented(); + } /** * @description diff --git a/packages/forms/src/directives/ng_control_status.ts b/packages/forms/src/directives/ng_control_status.ts index 735c1b5938..12c7f5d525 100644 --- a/packages/forms/src/directives/ng_control_status.ts +++ b/packages/forms/src/directives/ng_control_status.ts @@ -15,15 +15,31 @@ import {NgControl} from './ng_control'; export class AbstractControlStatus { private _cd: AbstractControlDirective; - constructor(cd: AbstractControlDirective) { this._cd = cd; } + constructor(cd: AbstractControlDirective) { + this._cd = cd; + } - get ngClassUntouched(): boolean { return this._cd.control ? this._cd.control.untouched : false; } - get ngClassTouched(): boolean { return this._cd.control ? this._cd.control.touched : false; } - get ngClassPristine(): boolean { return this._cd.control ? this._cd.control.pristine : false; } - get ngClassDirty(): boolean { return this._cd.control ? this._cd.control.dirty : false; } - get ngClassValid(): boolean { return this._cd.control ? this._cd.control.valid : false; } - get ngClassInvalid(): boolean { return this._cd.control ? this._cd.control.invalid : false; } - get ngClassPending(): boolean { return this._cd.control ? this._cd.control.pending : false; } + get ngClassUntouched(): boolean { + return this._cd.control ? this._cd.control.untouched : false; + } + get ngClassTouched(): boolean { + return this._cd.control ? this._cd.control.touched : false; + } + get ngClassPristine(): boolean { + return this._cd.control ? this._cd.control.pristine : false; + } + get ngClassDirty(): boolean { + return this._cd.control ? this._cd.control.dirty : false; + } + get ngClassValid(): boolean { + return this._cd.control ? this._cd.control.valid : false; + } + get ngClassInvalid(): boolean { + return this._cd.control ? this._cd.control.invalid : false; + } + get ngClassPending(): boolean { + return this._cd.control ? this._cd.control.pending : false; + } } export const ngControlStatusHost = { @@ -61,14 +77,16 @@ export const ngControlStatusHost = { */ @Directive({selector: '[formControlName],[ngModel],[formControl]', host: ngControlStatusHost}) export class NgControlStatus extends AbstractControlStatus { - constructor(@Self() cd: NgControl) { super(cd); } + constructor(@Self() cd: NgControl) { + super(cd); + } } /** * @description * Directive automatically applied to Angular form groups that sets CSS classes * based on control status (valid/invalid/dirty/etc). - * + * * @see `NgControlStatus` * * @ngModule ReactiveFormsModule @@ -81,5 +99,7 @@ export class NgControlStatus extends AbstractControlStatus { host: ngControlStatusHost }) export class NgControlStatusGroup extends AbstractControlStatus { - constructor(@Self() cd: ControlContainer) { super(cd); } + constructor(@Self() cd: ControlContainer) { + super(cd); + } } diff --git a/packages/forms/src/directives/ng_form.ts b/packages/forms/src/directives/ng_form.ts index 9720671b11..5952d02637 100644 --- a/packages/forms/src/directives/ng_form.ts +++ b/packages/forms/src/directives/ng_form.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {AfterViewInit, Directive, EventEmitter, Inject, Input, Optional, Self, forwardRef} from '@angular/core'; +import {AfterViewInit, Directive, EventEmitter, forwardRef, Inject, Input, Optional, Self} from '@angular/core'; import {AbstractControl, FormControl, FormGroup, FormHooks} from '../model'; import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../validators'; @@ -96,8 +96,7 @@ const resolvedPromise = (() => Promise.resolve(null))(); outputs: ['ngSubmit'], exportAs: 'ngForm' }) -export class NgForm extends ControlContainer implements Form, - AfterViewInit { +export class NgForm extends ControlContainer implements Form, AfterViewInit { /** * @description * Returns whether the form submission has been triggered. @@ -128,7 +127,7 @@ export class NgForm extends ControlContainer implements Form, * */ // TODO(issue/24571): remove '!'. - @Input('ngFormOptions') options !: {updateOn?: FormHooks}; + @Input('ngFormOptions') options!: {updateOn?: FormHooks}; constructor( @Optional() @Self() @Inject(NG_VALIDATORS) validators: any[], @@ -142,32 +141,42 @@ export class NgForm extends ControlContainer implements Form, * @description * Lifecycle method called after the view is initialized. For internal use only. */ - ngAfterViewInit() { this._setUpdateStrategy(); } + ngAfterViewInit() { + this._setUpdateStrategy(); + } /** * @description * The directive instance. */ - get formDirective(): Form { return this; } + get formDirective(): Form { + return this; + } /** * @description * The internal `FormGroup` instance. */ - get control(): FormGroup { return this.form; } + get control(): FormGroup { + return this.form; + } /** * @description * Returns an array representing the path to this group. Because this directive * always lives at the top level of a form, it is always an empty array. */ - get path(): string[] { return []; } + get path(): string[] { + return []; + } /** * @description * Returns a map of the controls in this group. */ - get controls(): {[key: string]: AbstractControl} { return this.form.controls; } + get controls(): {[key: string]: AbstractControl} { + return this.form.controls; + } /** * @description @@ -179,7 +188,7 @@ export class NgForm extends ControlContainer implements Form, addControl(dir: NgModel): void { resolvedPromise.then(() => { const container = this._findContainer(dir.path); - (dir as{control: FormControl}).control = + (dir as {control: FormControl}).control = container.registerControl(dir.name, dir.control); setUpControl(dir.control, dir); dir.control.updateValueAndValidity({emitEvent: false}); @@ -193,7 +202,9 @@ export class NgForm extends ControlContainer implements Form, * * @param dir The `NgModel` directive instance. */ - getControl(dir: NgModel): FormControl { return this.form.get(dir.path); } + getControl(dir: NgModel): FormControl { + return this.form.get(dir.path); + } /** * @description @@ -248,7 +259,9 @@ export class NgForm extends ControlContainer implements Form, * * @param dir The `NgModelGroup` directive instance. */ - getFormGroup(dir: NgModelGroup): FormGroup { return this.form.get(dir.path); } + getFormGroup(dir: NgModelGroup): FormGroup { + return this.form.get(dir.path); + } /** * Sets the new value for the provided `NgControl` directive. @@ -258,7 +271,7 @@ export class NgForm extends ControlContainer implements Form, */ updateModel(dir: NgControl, value: any): void { resolvedPromise.then(() => { - const ctrl = this.form.get(dir.path !); + const ctrl = this.form.get(dir.path!); ctrl.setValue(value); }); } @@ -269,7 +282,9 @@ export class NgForm extends ControlContainer implements Form, * * @param value The new value */ - setValue(value: {[key: string]: any}): void { this.control.setValue(value); } + setValue(value: {[key: string]: any}): void { + this.control.setValue(value); + } /** * @description @@ -279,7 +294,7 @@ export class NgForm extends ControlContainer implements Form, * @param $event The "submit" event object */ onSubmit($event: Event): boolean { - (this as{submitted: boolean}).submitted = true; + (this as {submitted: boolean}).submitted = true; syncPendingControls(this.form, this._directives); this.ngSubmit.emit($event); return false; @@ -289,7 +304,9 @@ export class NgForm extends ControlContainer implements Form, * @description * Method called when the "reset" event is triggered on the form. */ - onReset(): void { this.resetForm(); } + onReset(): void { + this.resetForm(); + } /** * @description @@ -299,7 +316,7 @@ export class NgForm extends ControlContainer implements Form, */ resetForm(value: any = undefined): void { this.form.reset(value); - (this as{submitted: boolean}).submitted = false; + (this as {submitted: boolean}).submitted = false; } private _setUpdateStrategy() { diff --git a/packages/forms/src/directives/ng_model.ts b/packages/forms/src/directives/ng_model.ts index b85aa6f240..67cf5c0ecd 100644 --- a/packages/forms/src/directives/ng_model.ts +++ b/packages/forms/src/directives/ng_model.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directive, EventEmitter, Host, Inject, Input, OnChanges, OnDestroy, Optional, Output, Self, SimpleChanges, forwardRef} from '@angular/core'; +import {Directive, EventEmitter, forwardRef, Host, Inject, Input, OnChanges, OnDestroy, Optional, Output, Self, SimpleChanges} from '@angular/core'; import {FormControl, FormHooks} from '../model'; import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../validators'; @@ -64,17 +64,17 @@ const resolvedPromise = (() => Promise.resolve(null))(); * (also known as 'banana-box syntax'), the value in the UI always syncs back to * the domain model in your class. * - * To inspect the properties of the associated `FormControl` (like validity state), - * export the directive into a local template variable using `ngModel` as the key (ex: `#myVar="ngModel"`). - * You then access the control using the directive's `control` property, - * but most properties used (like `valid` and `dirty`) fall through to the control anyway for direct access. - * See a full list of properties directly available in `AbstractControlDirective`. + * To inspect the properties of the associated `FormControl` (like validity state), + * export the directive into a local template variable using `ngModel` as the key (ex: + * `#myVar="ngModel"`). You then access the control using the directive's `control` property, but + * most properties used (like `valid` and `dirty`) fall through to the control anyway for direct + * access. See a full list of properties directly available in `AbstractControlDirective`. * - * @see `RadioControlValueAccessor` + * @see `RadioControlValueAccessor` * @see `SelectControlValueAccessor` - * + * * @usageNotes - * + * * ### Using ngModel on a standalone control * * The following examples show a simple standalone control using `ngModel`: @@ -84,23 +84,23 @@ const resolvedPromise = (() => Promise.resolve(null))(); * When using the `ngModel` within `
` tags, you'll also need to supply a `name` attribute * so that the control can be registered with the parent form under that name. * - * In the context of a parent form, it's often unnecessary to include one-way or two-way binding, - * as the parent form syncs the value for you. You access its properties by exporting it into a - * local template variable using `ngForm` such as (`#f="ngForm"`). Use the variable where + * In the context of a parent form, it's often unnecessary to include one-way or two-way binding, + * as the parent form syncs the value for you. You access its properties by exporting it into a + * local template variable using `ngForm` such as (`#f="ngForm"`). Use the variable where * needed on form submission. * * If you do need to populate initial values into your form, using a one-way binding for * `ngModel` tends to be sufficient as long as you use the exported form's value rather * than the domain model's value on submit. - * + * * ### Using ngModel within a form * * The following example shows controls using `ngModel` within a form: * * {@example forms/ts/simpleForm/simple_form_example.ts region='Component'} - * + * * ### Using a standalone ngModel within a group - * + * * The following example shows you how to use a standalone ngModel control * within a form. This controls the display of the form, but doesn't contain form data. * @@ -111,11 +111,11 @@ const resolvedPromise = (() => Promise.resolve(null))(); *
* * ``` - * + * * ### Setting the ngModel name attribute through options - * - * The following example shows you an alternate way to set the name attribute. The name attribute is used - * within a custom form component, and the name `@Input` property serves a different purpose. + * + * The following example shows you an alternate way to set the name attribute. The name attribute is + * used within a custom form component, and the name `@Input` property serves a different purpose. * * ```html *
@@ -133,8 +133,7 @@ const resolvedPromise = (() => Promise.resolve(null))(); providers: [formControlBinding], exportAs: 'ngModel' }) -export class NgModel extends NgControl implements OnChanges, - OnDestroy { +export class NgModel extends NgControl implements OnChanges, OnDestroy { public readonly control: FormControl = new FormControl(); // At runtime we coerce arbitrary values assigned to the "disabled" input to a "boolean". @@ -161,14 +160,14 @@ export class NgModel extends NgControl implements OnChanges, * uses this name as a key to retrieve this control's value. */ // TODO(issue/24571): remove '!'. - @Input() name !: string; + @Input() name!: string; /** * @description * Tracks whether the control is disabled. */ // TODO(issue/24571): remove '!'. - @Input('disabled') isDisabled !: boolean; + @Input('disabled') isDisabled!: boolean; /** * @description @@ -192,8 +191,7 @@ export class NgModel extends NgControl implements OnChanges, * */ // TODO(issue/24571): remove '!'. - @Input('ngModelOptions') - options !: {name?: string, standalone?: boolean, updateOn?: FormHooks}; + @Input('ngModelOptions') options!: {name?: string, standalone?: boolean, updateOn?: FormHooks}; /** * @description @@ -202,151 +200,156 @@ export class NgModel extends NgControl implements OnChanges, */ @Output('ngModelChange') update = new EventEmitter(); - constructor(@Optional() @Host() parent: ControlContainer, - @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._parent = parent; - this._rawValidators = validators || []; - this._rawAsyncValidators = asyncValidators || []; - this.valueAccessor = selectValueAccessor(this, valueAccessors); - } + constructor( + @Optional() @Host() parent: ControlContainer, + @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._parent = parent; + this._rawValidators = validators || []; + this._rawAsyncValidators = asyncValidators || []; + this.valueAccessor = selectValueAccessor(this, valueAccessors); + } - /** - * @description - * A lifecycle method called when the directive's inputs change. For internal use - * only. - * - * @param changes A object of key/value pairs for the set of changed inputs. - */ - ngOnChanges(changes: SimpleChanges) { - this._checkForErrors(); - if (!this._registered) this._setUpControl(); - if ('isDisabled' in changes) { - this._updateDisabled(changes); - } + /** + * @description + * A lifecycle method called when the directive's inputs change. For internal use + * only. + * + * @param changes A object of key/value pairs for the set of changed inputs. + */ + ngOnChanges(changes: SimpleChanges) { + this._checkForErrors(); + if (!this._registered) this._setUpControl(); + if ('isDisabled' in changes) { + this._updateDisabled(changes); + } - if (isPropertyUpdated(changes, this.viewModel)) { - this._updateValue(this.model); - this.viewModel = this.model; - } - } + if (isPropertyUpdated(changes, this.viewModel)) { + this._updateValue(this.model); + this.viewModel = this.model; + } + } - /** - * @description - * Lifecycle method called before the directive's instance is destroyed. For internal - * use only. - */ - ngOnDestroy(): void { this.formDirective && this.formDirective.removeControl(this); } + /** + * @description + * Lifecycle method called before the directive's instance is destroyed. For internal + * use only. + */ + ngOnDestroy(): void { + this.formDirective && this.formDirective.removeControl(this); + } - /** - * @description - * Returns an array that represents the path from the top-level form to this control. - * Each index is the string name of the control on that level. - */ - get path(): string[] { - return this._parent ? controlPath(this.name, this._parent) : [this.name]; - } + /** + * @description + * Returns an array that represents the path from the top-level form to this control. + * Each index is the string name of the control on that level. + */ + get path(): string[] { + return this._parent ? controlPath(this.name, this._parent) : [this.name]; + } - /** - * @description - * The top-level directive for this control if present, otherwise null. - */ - get formDirective(): any { return this._parent ? this._parent.formDirective : null; } + /** + * @description + * The top-level directive for this control if present, otherwise null. + */ + get formDirective(): any { + return this._parent ? this._parent.formDirective : null; + } - /** - * @description - * Synchronous validator function composed of all the synchronous validators - * registered with this directive. - */ - get validator(): ValidatorFn|null { return composeValidators(this._rawValidators); } + /** + * @description + * Synchronous validator function composed of all the synchronous validators + * registered with this directive. + */ + get validator(): ValidatorFn|null { + return composeValidators(this._rawValidators); + } - /** - * @description - * Async validator function composed of all the async validators registered with this - * directive. - */ - get asyncValidator(): AsyncValidatorFn|null { - return composeAsyncValidators(this._rawAsyncValidators); - } + /** + * @description + * Async validator function composed of all the async validators registered with this + * directive. + */ + get asyncValidator(): AsyncValidatorFn|null { + return composeAsyncValidators(this._rawAsyncValidators); + } - /** - * @description - * Sets the new value for the view model and emits an `ngModelChange` event. - * - * @param newValue The new value emitted by `ngModelChange`. - */ - viewToModelUpdate(newValue: any): void { - this.viewModel = newValue; - this.update.emit(newValue); - } + /** + * @description + * Sets the new value for the view model and emits an `ngModelChange` event. + * + * @param newValue The new value emitted by `ngModelChange`. + */ + viewToModelUpdate(newValue: any): void { + this.viewModel = newValue; + this.update.emit(newValue); + } - private _setUpControl(): void { - this._setUpdateStrategy(); - this._isStandalone() ? this._setUpStandalone() : - this.formDirective.addControl(this); - this._registered = true; - } + private _setUpControl(): void { + this._setUpdateStrategy(); + this._isStandalone() ? this._setUpStandalone() : this.formDirective.addControl(this); + this._registered = true; + } - private _setUpdateStrategy(): void { - if (this.options && this.options.updateOn != null) { - this.control._updateOn = this.options.updateOn; - } - } + private _setUpdateStrategy(): void { + if (this.options && this.options.updateOn != null) { + this.control._updateOn = this.options.updateOn; + } + } - private _isStandalone(): boolean { - return !this._parent || !!(this.options && this.options.standalone); - } + private _isStandalone(): boolean { + return !this._parent || !!(this.options && this.options.standalone); + } - private _setUpStandalone(): void { - setUpControl(this.control, this); - this.control.updateValueAndValidity({emitEvent: false}); - } + private _setUpStandalone(): void { + setUpControl(this.control, this); + this.control.updateValueAndValidity({emitEvent: false}); + } - private _checkForErrors(): void { - if (!this._isStandalone()) { - this._checkParentType(); - } - this._checkName(); - } + private _checkForErrors(): void { + if (!this._isStandalone()) { + this._checkParentType(); + } + this._checkName(); + } - private _checkParentType(): void { - if (!(this._parent instanceof NgModelGroup) && - this._parent instanceof AbstractFormGroupDirective) { - TemplateDrivenErrors.formGroupNameException(); - } else if ( - !(this._parent instanceof NgModelGroup) && !(this._parent instanceof NgForm)) { - TemplateDrivenErrors.modelParentException(); - } - } + private _checkParentType(): void { + if (!(this._parent instanceof NgModelGroup) && + this._parent instanceof AbstractFormGroupDirective) { + TemplateDrivenErrors.formGroupNameException(); + } else if (!(this._parent instanceof NgModelGroup) && !(this._parent instanceof NgForm)) { + TemplateDrivenErrors.modelParentException(); + } + } - private _checkName(): void { - if (this.options && this.options.name) this.name = this.options.name; + private _checkName(): void { + if (this.options && this.options.name) this.name = this.options.name; - if (!this._isStandalone() && !this.name) { - TemplateDrivenErrors.missingNameException(); - } - } + if (!this._isStandalone() && !this.name) { + TemplateDrivenErrors.missingNameException(); + } + } - private _updateValue(value: any): void { - resolvedPromise.then( - () => { this.control.setValue(value, {emitViewToModelChange: false}); }); - } + private _updateValue(value: any): void { + resolvedPromise.then(() => { + this.control.setValue(value, {emitViewToModelChange: false}); + }); + } - private _updateDisabled(changes: SimpleChanges) { - const disabledValue = changes['isDisabled'].currentValue; + private _updateDisabled(changes: SimpleChanges) { + const disabledValue = changes['isDisabled'].currentValue; - const isDisabled = - disabledValue === '' || (disabledValue && disabledValue !== 'false'); + const isDisabled = disabledValue === '' || (disabledValue && disabledValue !== 'false'); - resolvedPromise.then(() => { - if (isDisabled && !this.control.disabled) { - this.control.disable(); - } else if (!isDisabled && this.control.disabled) { - this.control.enable(); - } - }); - } + resolvedPromise.then(() => { + if (isDisabled && !this.control.disabled) { + this.control.disable(); + } else if (!isDisabled && this.control.disabled) { + this.control.enable(); + } + }); + } } diff --git a/packages/forms/src/directives/ng_model_group.ts b/packages/forms/src/directives/ng_model_group.ts index c7cabf035e..55ec88d51d 100644 --- a/packages/forms/src/directives/ng_model_group.ts +++ b/packages/forms/src/directives/ng_model_group.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directive, Host, Inject, Input, OnDestroy, OnInit, Optional, Self, SkipSelf, forwardRef} from '@angular/core'; +import {Directive, forwardRef, Host, Inject, Input, OnDestroy, OnInit, Optional, Self, SkipSelf} from '@angular/core'; import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../validators'; @@ -54,7 +54,7 @@ export class NgModelGroup extends AbstractFormGroupDirective implements OnInit, * to a key in the parent `NgForm`. */ // TODO(issue/24571): remove '!'. - @Input('ngModelGroup') name !: string; + @Input('ngModelGroup') name!: string; constructor( @Host() @SkipSelf() parent: ControlContainer, diff --git a/packages/forms/src/directives/normalize_validator.ts b/packages/forms/src/directives/normalize_validator.ts index 3a08e61558..f12aa0aae5 100644 --- a/packages/forms/src/directives/normalize_validator.ts +++ b/packages/forms/src/directives/normalize_validator.ts @@ -9,7 +9,7 @@ import {AbstractControl} from '../model'; import {AsyncValidator, AsyncValidatorFn, Validator, ValidatorFn} from './validators'; -export function normalizeValidator(validator: ValidatorFn | Validator): ValidatorFn { +export function normalizeValidator(validator: ValidatorFn|Validator): ValidatorFn { if ((validator).validate) { return (c: AbstractControl) => (validator).validate(c); } else { @@ -17,8 +17,8 @@ export function normalizeValidator(validator: ValidatorFn | Validator): Validato } } -export function normalizeAsyncValidator(validator: AsyncValidatorFn | AsyncValidator): - AsyncValidatorFn { +export function normalizeAsyncValidator(validator: AsyncValidatorFn| + AsyncValidator): AsyncValidatorFn { if ((validator).validate) { return (c: AbstractControl) => (validator).validate(c); } else { diff --git a/packages/forms/src/directives/number_value_accessor.ts b/packages/forms/src/directives/number_value_accessor.ts index df5edc43fa..dc85ae7b0d 100644 --- a/packages/forms/src/directives/number_value_accessor.ts +++ b/packages/forms/src/directives/number_value_accessor.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directive, ElementRef, Renderer2, forwardRef} from '@angular/core'; +import {Directive, ElementRef, forwardRef, Renderer2} from '@angular/core'; import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor'; @@ -84,7 +84,9 @@ export class NumberValueAccessor implements ControlValueAccessor { * @param fn The callback function */ registerOnChange(fn: (_: number|null) => void): void { - this.onChange = (value) => { fn(value == '' ? null : parseFloat(value)); }; + this.onChange = (value) => { + fn(value == '' ? null : parseFloat(value)); + }; } /** @@ -93,7 +95,9 @@ export class NumberValueAccessor implements ControlValueAccessor { * * @param fn The callback function */ - registerOnTouched(fn: () => void): void { this.onTouched = fn; } + registerOnTouched(fn: () => void): void { + this.onTouched = fn; + } /** * Sets the "disabled" property on the input element. diff --git a/packages/forms/src/directives/radio_control_value_accessor.ts b/packages/forms/src/directives/radio_control_value_accessor.ts index bca1040d21..6f5ee4905b 100644 --- a/packages/forms/src/directives/radio_control_value_accessor.ts +++ b/packages/forms/src/directives/radio_control_value_accessor.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directive, ElementRef, Injectable, Injector, Input, OnDestroy, OnInit, Renderer2, forwardRef} from '@angular/core'; +import {Directive, ElementRef, forwardRef, Injectable, Injector, Input, OnDestroy, OnInit, Renderer2} from '@angular/core'; import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor'; import {NgControl} from './ng_control'; @@ -93,17 +93,16 @@ export class RadioControlRegistry { host: {'(change)': 'onChange()', '(blur)': 'onTouched()'}, providers: [RADIO_VALUE_ACCESSOR] }) -export class RadioControlValueAccessor implements ControlValueAccessor, - OnDestroy, OnInit { +export class RadioControlValueAccessor implements ControlValueAccessor, OnDestroy, OnInit { /** @internal */ // TODO(issue/24571): remove '!'. - _state !: boolean; + _state!: boolean; /** @internal */ // TODO(issue/24571): remove '!'. - _control !: NgControl; + _control!: NgControl; /** @internal */ // TODO(issue/24571): remove '!'. - _fn !: Function; + _fn!: Function; /** * @description @@ -122,7 +121,7 @@ export class RadioControlValueAccessor implements ControlValueAccessor, * Tracks the name of the radio input element. */ // TODO(issue/24571): remove '!'. - @Input() name !: string; + @Input() name!: string; /** * @description @@ -130,7 +129,7 @@ export class RadioControlValueAccessor implements ControlValueAccessor, * to a key in the parent `FormGroup` or `FormArray`. */ // TODO(issue/24571): remove '!'. - @Input() formControlName !: string; + @Input() formControlName!: string; /** * @description @@ -156,7 +155,9 @@ export class RadioControlValueAccessor implements ControlValueAccessor, * @description * Lifecycle method called before the directive's instance is destroyed. For internal use only. */ - ngOnDestroy(): void { this._registry.remove(this); } + ngOnDestroy(): void { + this._registry.remove(this); + } /** * @description @@ -188,7 +189,9 @@ export class RadioControlValueAccessor implements ControlValueAccessor, * * @param value */ - fireUncheck(value: any): void { this.writeValue(value); } + fireUncheck(value: any): void { + this.writeValue(value); + } /** * @description @@ -196,7 +199,9 @@ export class RadioControlValueAccessor implements ControlValueAccessor, * * @param fn The callback function */ - registerOnTouched(fn: () => {}): void { this.onTouched = fn; } + registerOnTouched(fn: () => {}): void { + this.onTouched = fn; + } /** * Sets the "disabled" property on the input element. diff --git a/packages/forms/src/directives/range_value_accessor.ts b/packages/forms/src/directives/range_value_accessor.ts index e386fe1444..7a01f7c1d8 100644 --- a/packages/forms/src/directives/range_value_accessor.ts +++ b/packages/forms/src/directives/range_value_accessor.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directive, ElementRef, Renderer2, StaticProvider, forwardRef} from '@angular/core'; +import {Directive, ElementRef, forwardRef, Renderer2, StaticProvider} from '@angular/core'; import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor'; @@ -82,7 +82,9 @@ export class RangeValueAccessor implements ControlValueAccessor { * @param fn The callback function */ registerOnChange(fn: (_: number|null) => void): void { - this.onChange = (value) => { fn(value == '' ? null : parseFloat(value)); }; + this.onChange = (value) => { + fn(value == '' ? null : parseFloat(value)); + }; } /** @@ -91,7 +93,9 @@ export class RangeValueAccessor implements ControlValueAccessor { * * @param fn The callback function */ - registerOnTouched(fn: () => void): void { this.onTouched = fn; } + registerOnTouched(fn: () => void): void { + this.onTouched = fn; + } /** * Sets the "disabled" property on the range input element. diff --git a/packages/forms/src/directives/reactive_directives/form_control_directive.ts b/packages/forms/src/directives/reactive_directives/form_control_directive.ts index c329d5ab31..28d1957a94 100644 --- a/packages/forms/src/directives/reactive_directives/form_control_directive.ts +++ b/packages/forms/src/directives/reactive_directives/form_control_directive.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directive, EventEmitter, Inject, InjectionToken, Input, OnChanges, Optional, Output, Self, SimpleChanges, forwardRef} from '@angular/core'; +import {Directive, EventEmitter, forwardRef, Inject, InjectionToken, Input, OnChanges, Optional, Output, Self, SimpleChanges} from '@angular/core'; import {FormControl} from '../../model'; import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../../validators'; @@ -31,7 +31,7 @@ export const formControlBinding: any = { /** * @description * * Syncs a standalone `FormControl` instance to a form control element. - * + * * @see [Reactive Forms Guide](guide/reactive-forms) * @see `FormControl` * @see `AbstractControl` @@ -39,7 +39,7 @@ export const formControlBinding: any = { * @usageNotes * * ### Registering a single form control - * + * * The following examples shows how to register a standalone control and set its value. * * {@example forms/ts/simpleFormControl/simple_form_control_example.ts region='Component'} @@ -129,14 +129,16 @@ export class FormControlDirective extends NgControl implements OnChanges { * Tracks the `FormControl` instance bound to the directive. */ // TODO(issue/24571): remove '!'. - @Input('formControl') form !: FormControl; + @Input('formControl') form!: FormControl; /** * @description * Triggers a warning that this input should not be used with reactive forms. */ @Input('disabled') - set isDisabled(isDisabled: boolean) { ReactiveErrors.disabledAttrWarning(); } + set isDisabled(isDisabled: boolean) { + ReactiveErrors.disabledAttrWarning(); + } // TODO(kara): remove next 4 properties once deprecation period is over @@ -164,81 +166,88 @@ export class FormControlDirective extends NgControl implements OnChanges { */ _ngModelWarningSent = false; - constructor(@Optional() @Self() @Inject(NG_VALIDATORS) validators: Array, - @Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: Array, - @Optional() @Self() @Inject(NG_VALUE_ACCESSOR) - valueAccessors: ControlValueAccessor[], - @Optional() @Inject(NG_MODEL_WITH_FORM_CONTROL_WARNING) private _ngModelWarningConfig: string|null) { - super(); - this._rawValidators = validators || []; - this._rawAsyncValidators = asyncValidators || []; - this.valueAccessor = selectValueAccessor(this, valueAccessors); - } + constructor( + @Optional() @Self() @Inject(NG_VALIDATORS) validators: Array, + @Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: + Array, + @Optional() @Self() @Inject(NG_VALUE_ACCESSOR) valueAccessors: ControlValueAccessor[], + @Optional() @Inject(NG_MODEL_WITH_FORM_CONTROL_WARNING) private _ngModelWarningConfig: string| + null) { + super(); + this._rawValidators = validators || []; + this._rawAsyncValidators = asyncValidators || []; + this.valueAccessor = selectValueAccessor(this, valueAccessors); + } - /** - * @description - * A lifecycle method called when the directive's inputs change. For internal use - * only. - * - * @param changes A object of key/value pairs for the set of changed inputs. - */ - ngOnChanges(changes: SimpleChanges): void { - if (this._isControlChanged(changes)) { - setUpControl(this.form, this); - if (this.control.disabled && this.valueAccessor !.setDisabledState) { - this.valueAccessor !.setDisabledState !(true); - } - this.form.updateValueAndValidity({emitEvent: false}); - } - if (isPropertyUpdated(changes, this.viewModel)) { - _ngModelWarning( - 'formControl', FormControlDirective, this, this._ngModelWarningConfig); - this.form.setValue(this.model); - this.viewModel = this.model; - } - } + /** + * @description + * A lifecycle method called when the directive's inputs change. For internal use + * only. + * + * @param changes A object of key/value pairs for the set of changed inputs. + */ + ngOnChanges(changes: SimpleChanges): void { + if (this._isControlChanged(changes)) { + setUpControl(this.form, this); + if (this.control.disabled && this.valueAccessor!.setDisabledState) { + this.valueAccessor!.setDisabledState!(true); + } + this.form.updateValueAndValidity({emitEvent: false}); + } + if (isPropertyUpdated(changes, this.viewModel)) { + _ngModelWarning('formControl', FormControlDirective, this, this._ngModelWarningConfig); + this.form.setValue(this.model); + this.viewModel = this.model; + } + } - /** - * @description - * Returns an array that represents the path from the top-level form to this control. - * Each index is the string name of the control on that level. - */ - get path(): string[] { return []; } + /** + * @description + * Returns an array that represents the path from the top-level form to this control. + * Each index is the string name of the control on that level. + */ + get path(): string[] { + return []; + } - /** - * @description - * Synchronous validator function composed of all the synchronous validators - * registered with this directive. - */ - get validator(): ValidatorFn|null { return composeValidators(this._rawValidators); } + /** + * @description + * Synchronous validator function composed of all the synchronous validators + * registered with this directive. + */ + get validator(): ValidatorFn|null { + return composeValidators(this._rawValidators); + } - /** - * @description - * Async validator function composed of all the async validators registered with this - * directive. - */ - get asyncValidator(): AsyncValidatorFn|null { - return composeAsyncValidators(this._rawAsyncValidators); - } + /** + * @description + * Async validator function composed of all the async validators registered with this + * directive. + */ + get asyncValidator(): AsyncValidatorFn|null { + return composeAsyncValidators(this._rawAsyncValidators); + } - /** - * @description - * The `FormControl` bound to this directive. - */ - get control(): FormControl { return this.form; } + /** + * @description + * The `FormControl` bound to this directive. + */ + get control(): FormControl { + return this.form; + } - /** - * @description - * Sets the new value for the view model and emits an `ngModelChange` event. - * - * @param newValue The new value for the view model. - */ - viewToModelUpdate(newValue: any): void { - this.viewModel = newValue; - this.update.emit(newValue); - } + /** + * @description + * Sets the new value for the view model and emits an `ngModelChange` event. + * + * @param newValue The new value for the view model. + */ + viewToModelUpdate(newValue: any): void { + this.viewModel = newValue; + this.update.emit(newValue); + } - private _isControlChanged(changes: {[key: string]: any}): boolean { - return changes.hasOwnProperty('form'); - } + private _isControlChanged(changes: {[key: string]: any}): boolean { + return changes.hasOwnProperty('form'); + } } diff --git a/packages/forms/src/directives/reactive_directives/form_control_name.ts b/packages/forms/src/directives/reactive_directives/form_control_name.ts index a466ea4f45..6a4e9da351 100644 --- a/packages/forms/src/directives/reactive_directives/form_control_name.ts +++ b/packages/forms/src/directives/reactive_directives/form_control_name.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directive, EventEmitter, Host, Inject, Input, OnChanges, OnDestroy, Optional, Output, Self, SimpleChanges, SkipSelf, forwardRef} from '@angular/core'; +import {Directive, EventEmitter, forwardRef, Host, Inject, Input, OnChanges, OnDestroy, Optional, Output, Self, SimpleChanges, SkipSelf} from '@angular/core'; import {FormControl} from '../../model'; import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../../validators'; @@ -31,13 +31,13 @@ export const controlNameBinding: any = { * @description * Syncs a `FormControl` in an existing `FormGroup` to a form control * element by name. - * + * * @see [Reactive Forms Guide](guide/reactive-forms) * @see `FormControl` * @see `AbstractControl` * * @usageNotes - * + * * ### Register `FormControl` within a group * * The following example shows how to register multiple form controls within a form group @@ -140,7 +140,7 @@ export class FormControlName extends NgControl implements OnChanges, OnDestroy { * Tracks the `FormControl` instance bound to the directive. */ // TODO(issue/24571): remove '!'. - readonly control !: FormControl; + readonly control!: FormControl; /** * @description @@ -152,14 +152,16 @@ export class FormControlName extends NgControl implements OnChanges, OnDestroy { * to indices when iterating over controls in a `FormArray`. */ // TODO(issue/24571): remove '!'. - @Input('formControlName') name !: string | number | null; + @Input('formControlName') name!: string|number|null; /** * @description * Triggers a warning that this input should not be used with reactive forms. */ @Input('disabled') - set isDisabled(isDisabled: boolean) { ReactiveErrors.disabledAttrWarning(); } + set isDisabled(isDisabled: boolean) { + ReactiveErrors.disabledAttrWarning(); + } // TODO(kara): remove next 4 properties once deprecation period is over @@ -244,21 +246,25 @@ export class FormControlName extends NgControl implements OnChanges, OnDestroy { * Each index is the string name of the control on that level. */ get path(): string[] { - return controlPath(this.name == null ? this.name : this.name.toString(), this._parent !); + return controlPath(this.name == null ? this.name : this.name.toString(), this._parent!); } /** * @description * The top-level directive for this group if present, otherwise null. */ - get formDirective(): any { return this._parent ? this._parent.formDirective : null; } + get formDirective(): any { + return this._parent ? this._parent.formDirective : null; + } /** * @description * Synchronous validator function composed of all the synchronous validators * registered with this directive. */ - get validator(): ValidatorFn|null { return composeValidators(this._rawValidators); } + get validator(): ValidatorFn|null { + return composeValidators(this._rawValidators); + } /** * @description @@ -266,7 +272,7 @@ export class FormControlName extends NgControl implements OnChanges, OnDestroy { * directive. */ get asyncValidator(): AsyncValidatorFn { - return composeAsyncValidators(this._rawAsyncValidators) !; + return composeAsyncValidators(this._rawAsyncValidators)!; } private _checkParentType(): void { @@ -282,9 +288,9 @@ export class FormControlName extends NgControl implements OnChanges, OnDestroy { private _setUpControl() { this._checkParentType(); - (this as{control: FormControl}).control = this.formDirective.addControl(this); - if (this.control.disabled && this.valueAccessor !.setDisabledState) { - this.valueAccessor !.setDisabledState !(true); + (this as {control: FormControl}).control = this.formDirective.addControl(this); + if (this.control.disabled && this.valueAccessor!.setDisabledState) { + this.valueAccessor!.setDisabledState!(true); } this._added = true; } diff --git a/packages/forms/src/directives/reactive_directives/form_group_directive.ts b/packages/forms/src/directives/reactive_directives/form_group_directive.ts index 2d3e617ade..4fabe81ea4 100644 --- a/packages/forms/src/directives/reactive_directives/form_group_directive.ts +++ b/packages/forms/src/directives/reactive_directives/form_group_directive.ts @@ -6,7 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directive, EventEmitter, Inject, Input, OnChanges, Optional, Output, Self, SimpleChanges, forwardRef} from '@angular/core'; +import {Directive, EventEmitter, forwardRef, Inject, Input, OnChanges, Optional, Output, Self, SimpleChanges} from '@angular/core'; + import {FormArray, FormControl, FormGroup} from '../../model'; import {NG_ASYNC_VALIDATORS, NG_VALIDATORS, Validators} from '../../validators'; import {ControlContainer} from '../control_container'; @@ -31,7 +32,7 @@ export const formDirectiveProvider: any = { * `FormGroup` instance to match any child `FormControl`, `FormGroup`, * and `FormArray` instances to child `FormControlName`, `FormGroupName`, * and `FormArrayName` directives. - * + * * @see [Reactive Forms Guide](guide/reactive-forms) * @see `AbstractControl` * @@ -51,8 +52,7 @@ export const formDirectiveProvider: any = { host: {'(submit)': 'onSubmit($event)', '(reset)': 'onReset()'}, exportAs: 'ngForm' }) -export class FormGroupDirective extends ControlContainer implements Form, - OnChanges { +export class FormGroupDirective extends ControlContainer implements Form, OnChanges { /** * @description * Reports whether the form submission has been triggered. @@ -60,7 +60,7 @@ export class FormGroupDirective extends ControlContainer implements Form, public readonly submitted: boolean = false; // TODO(issue/24571): remove '!'. - private _oldForm !: FormGroup; + private _oldForm!: FormGroup; /** * @description @@ -72,7 +72,7 @@ export class FormGroupDirective extends ControlContainer implements Form, * @description * Tracks the `FormGroup` bound to this directive. */ - @Input('formGroup') form: FormGroup = null !; + @Input('formGroup') form: FormGroup = null!; /** * @description @@ -105,20 +105,26 @@ export class FormGroupDirective extends ControlContainer implements Form, * @description * Returns this directive's instance. */ - get formDirective(): Form { return this; } + get formDirective(): Form { + return this; + } /** * @description * Returns the `FormGroup` bound to this directive. */ - get control(): FormGroup { return this.form; } + get control(): FormGroup { + return this.form; + } /** * @description * Returns an array representing the path to this group. Because this directive * always lives at the top level of a form, it always an empty array. */ - get path(): string[] { return []; } + get path(): string[] { + return []; + } /** * @description @@ -141,7 +147,9 @@ export class FormGroupDirective extends ControlContainer implements Form, * * @param dir The `FormControlName` directive instance. */ - getControl(dir: FormControlName): FormControl { return this.form.get(dir.path); } + getControl(dir: FormControlName): FormControl { + return this.form.get(dir.path); + } /** * @description @@ -149,7 +157,9 @@ export class FormGroupDirective extends ControlContainer implements Form, * * @param dir The `FormControlName` directive instance. */ - removeControl(dir: FormControlName): void { removeDir(this.directives, dir); } + removeControl(dir: FormControlName): void { + removeDir(this.directives, dir); + } /** * Adds a new `FormGroupName` directive instance to the form. @@ -175,7 +185,9 @@ export class FormGroupDirective extends ControlContainer implements Form, * * @param dir The `FormGroupName` directive instance. */ - getFormGroup(dir: FormGroupName): FormGroup { return this.form.get(dir.path); } + getFormGroup(dir: FormGroupName): FormGroup { + return this.form.get(dir.path); + } /** * Adds a new `FormArrayName` directive instance to the form. @@ -201,7 +213,9 @@ export class FormGroupDirective extends ControlContainer implements Form, * * @param dir The `FormArrayName` directive instance. */ - getFormArray(dir: FormArrayName): FormArray { return this.form.get(dir.path); } + getFormArray(dir: FormArrayName): FormArray { + return this.form.get(dir.path); + } /** * Sets the new value for the provided `FormControlName` directive. @@ -222,7 +236,7 @@ export class FormGroupDirective extends ControlContainer implements Form, * @param $event The "submit" event object */ onSubmit($event: Event): boolean { - (this as{submitted: boolean}).submitted = true; + (this as {submitted: boolean}).submitted = true; syncPendingControls(this.form, this.directives); this.ngSubmit.emit($event); return false; @@ -232,7 +246,9 @@ export class FormGroupDirective extends ControlContainer implements Form, * @description * Method called when the "reset" event is triggered on the form. */ - onReset(): void { this.resetForm(); } + onReset(): void { + this.resetForm(); + } /** * @description @@ -242,7 +258,7 @@ export class FormGroupDirective extends ControlContainer implements Form, */ resetForm(value: any = undefined): void { this.form.reset(value); - (this as{submitted: boolean}).submitted = false; + (this as {submitted: boolean}).submitted = false; } @@ -253,7 +269,7 @@ export class FormGroupDirective extends ControlContainer implements Form, if (dir.control !== newCtrl) { cleanUpControl(dir.control, dir); if (newCtrl) setUpControl(newCtrl, dir); - (dir as{control: FormControl}).control = newCtrl; + (dir as {control: FormControl}).control = newCtrl; } }); @@ -268,10 +284,10 @@ export class FormGroupDirective extends ControlContainer implements Form, private _updateValidators() { const sync = composeValidators(this._validators); - this.form.validator = Validators.compose([this.form.validator !, sync !]); + this.form.validator = Validators.compose([this.form.validator!, sync!]); const async = composeAsyncValidators(this._asyncValidators); - this.form.asyncValidator = Validators.composeAsync([this.form.asyncValidator !, async !]); + this.form.asyncValidator = Validators.composeAsync([this.form.asyncValidator!, async!]); } private _checkFormPresent() { diff --git a/packages/forms/src/directives/reactive_directives/form_group_name.ts b/packages/forms/src/directives/reactive_directives/form_group_name.ts index 92ca96a17b..c3f48352fe 100644 --- a/packages/forms/src/directives/reactive_directives/form_group_name.ts +++ b/packages/forms/src/directives/reactive_directives/form_group_name.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directive, Host, Inject, Input, OnDestroy, OnInit, Optional, Self, SkipSelf, forwardRef} from '@angular/core'; +import {Directive, forwardRef, Host, Inject, Input, OnDestroy, OnInit, Optional, Self, SkipSelf} from '@angular/core'; import {FormArray} from '../../model'; import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../../validators'; @@ -82,7 +82,7 @@ export class FormGroupName extends AbstractFormGroupDirective implements OnInit, * to indices when iterating over groups in a `FormArray`. */ // TODO(issue/24571): remove '!'. - @Input('formGroupName') name !: string | number | null; + @Input('formGroupName') name!: string|number|null; constructor( @Optional() @Host() @SkipSelf() parent: ControlContainer, @@ -152,7 +152,7 @@ export class FormArrayName extends ControlContainer implements OnInit, OnDestroy * to indices when iterating over arrays in a `FormArray`. */ // TODO(issue/24571): remove '!'. - @Input('formArrayName') name !: string | number | null; + @Input('formArrayName') name!: string|number|null; constructor( @Optional() @Host() @SkipSelf() parent: ControlContainer, @@ -172,7 +172,7 @@ export class FormArrayName extends ControlContainer implements OnInit, OnDestroy */ ngOnInit(): void { this._checkParentType(); - this.formDirective !.addFormArray(this); + this.formDirective!.addFormArray(this); } /** @@ -189,7 +189,9 @@ export class FormArrayName extends ControlContainer implements OnInit, OnDestroy * @description * The `FormArray` bound to this directive. */ - get control(): FormArray { return this.formDirective !.getFormArray(this); } + get control(): FormArray { + return this.formDirective!.getFormArray(this); + } /** * @description @@ -213,7 +215,9 @@ export class FormArrayName extends ControlContainer implements OnInit, OnDestroy * Synchronous validator function composed of all the synchronous validators registered with this * directive. */ - get validator(): ValidatorFn|null { return composeValidators(this._validators); } + get validator(): ValidatorFn|null { + return composeValidators(this._validators); + } /** * @description diff --git a/packages/forms/src/directives/reactive_errors.ts b/packages/forms/src/directives/reactive_errors.ts index acca6a319f..b7e46673e6 100644 --- a/packages/forms/src/directives/reactive_errors.ts +++ b/packages/forms/src/directives/reactive_errors.ts @@ -83,8 +83,9 @@ export class ReactiveErrors { in Angular v7. For more information on this, see our API docs here: - https://angular.io/api/forms/${directiveName === 'formControl' ? 'FormControlDirective' - : 'FormControlName'}#use-with-ngmodel + https://angular.io/api/forms/${ + directiveName === 'formControl' ? 'FormControlDirective' : + 'FormControlName'}#use-with-ngmodel `); } } diff --git a/packages/forms/src/directives/select_control_value_accessor.ts b/packages/forms/src/directives/select_control_value_accessor.ts index a8fa16e8cb..9edb9d47e6 100644 --- a/packages/forms/src/directives/select_control_value_accessor.ts +++ b/packages/forms/src/directives/select_control_value_accessor.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directive, ElementRef, Host, Input, OnDestroy, Optional, Renderer2, StaticProvider, forwardRef, ɵlooseIdentical as looseIdentical} from '@angular/core'; +import {Directive, ElementRef, forwardRef, Host, Input, OnDestroy, Optional, Renderer2, StaticProvider, ɵlooseIdentical as looseIdentical} from '@angular/core'; import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor'; @@ -16,7 +16,7 @@ export const SELECT_VALUE_ACCESSOR: StaticProvider = { multi: true }; -function _buildValueString(id: string | null, value: any): string { +function _buildValueString(id: string|null, value: any): string { if (id == null) return `${value}`; if (value && typeof value === 'object') value = 'Object'; return `${id}: ${value}`.slice(0, 50); @@ -160,7 +160,9 @@ export class SelectControlValueAccessor implements ControlValueAccessor { * * @param fn The callback function */ - registerOnTouched(fn: () => any): void { this.onTouched = fn; } + registerOnTouched(fn: () => any): void { + this.onTouched = fn; + } /** * Sets the "disabled" property on the select input element. @@ -172,7 +174,9 @@ export class SelectControlValueAccessor implements ControlValueAccessor { } /** @internal */ - _registerOption(): string { return (this._idCounter++).toString(); } + _registerOption(): string { + return (this._idCounter++).toString(); + } /** @internal */ _getOptionId(value: any): string|null { @@ -206,7 +210,7 @@ export class NgSelectOption implements OnDestroy { * ID of the option element */ // TODO(issue/24571): remove '!'. - id !: string; + id!: string; constructor( private _element: ElementRef, private _renderer: Renderer2, diff --git a/packages/forms/src/directives/select_multiple_control_value_accessor.ts b/packages/forms/src/directives/select_multiple_control_value_accessor.ts index 5e0800947b..5bc28e20c0 100644 --- a/packages/forms/src/directives/select_multiple_control_value_accessor.ts +++ b/packages/forms/src/directives/select_multiple_control_value_accessor.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directive, ElementRef, Host, Input, OnDestroy, Optional, Renderer2, StaticProvider, forwardRef, ɵlooseIdentical as looseIdentical} from '@angular/core'; +import {Directive, ElementRef, forwardRef, Host, Input, OnDestroy, Optional, Renderer2, StaticProvider, ɵlooseIdentical as looseIdentical} from '@angular/core'; import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor'; @@ -36,24 +36,24 @@ interface HTMLOption { /** Mock interface for HTMLCollection */ abstract class HTMLCollection { // TODO(issue/24571): remove '!'. - length !: number; + length!: number; abstract item(_: number): HTMLOption; } /** * @description - * The `ControlValueAccessor` for writing multi-select control values and listening to multi-select control - * changes. The value accessor is used by the `FormControlDirective`, `FormControlName`, and `NgModel` - * directives. - * + * The `ControlValueAccessor` for writing multi-select control values and listening to multi-select + * control changes. The value accessor is used by the `FormControlDirective`, `FormControlName`, and + * `NgModel` directives. + * * @see `SelectControlValueAccessor` * * @usageNotes - * + * * ### Using a multi-select control - * + * * The follow example shows you how to use a multi-select control with a reactive form. - * + * * ```ts * const countryControl = new FormControl(); * ``` @@ -65,9 +65,9 @@ abstract class HTMLCollection { * * * ``` - * + * * ### Customizing option selection - * + * * To customize the default option comparison algorithm, ` * ``` @@ -248,11 +253,12 @@ export const EMAIL_VALIDATOR: any = { * @see [Form Validation](guide/form-validation) * * @usageNotes - * + * * ### Adding an email validator * - * The following example shows how to add an email validator to an input attached to an ngModel binding. - * + * The following example shows how to add an email validator to an input attached to an ngModel + * binding. + * * ``` * * @@ -269,9 +275,9 @@ export const EMAIL_VALIDATOR: any = { }) export class EmailValidator implements Validator { // TODO(issue/24571): remove '!'. - private _enabled !: boolean; + private _enabled!: boolean; // TODO(issue/24571): remove '!'. - private _onChange !: () => void; + private _onChange!: () => void; /** * @description @@ -298,7 +304,9 @@ export class EmailValidator implements Validator { * * @param fn The callback function */ - registerOnValidatorChange(fn: () => void): void { this._onChange = fn; } + registerOnValidatorChange(fn: () => void): void { + this._onChange = fn; + } } /** @@ -308,7 +316,9 @@ export class EmailValidator implements Validator { * * @publicApi */ -export interface ValidatorFn { (control: AbstractControl): ValidationErrors|null; } +export interface ValidatorFn { + (control: AbstractControl): ValidationErrors|null; +} /** * @description @@ -334,7 +344,7 @@ export const MIN_LENGTH_VALIDATOR: any = { /** * A directive that adds minimum length validation to controls marked with the * `minlength` attribute. The directive is provided with the `NG_VALIDATORS` multi-provider list. - * + * * @see [Form Validation](guide/form-validation) * * @usageNotes @@ -357,19 +367,18 @@ export const MIN_LENGTH_VALIDATOR: any = { providers: [MIN_LENGTH_VALIDATOR], host: {'[attr.minlength]': 'minlength ? minlength : null'} }) -export class MinLengthValidator implements Validator, - OnChanges { +export class MinLengthValidator implements Validator, OnChanges { // TODO(issue/24571): remove '!'. - private _validator !: ValidatorFn; + private _validator!: ValidatorFn; // TODO(issue/24571): remove '!'. - private _onChange !: () => void; + private _onChange!: () => void; /** * @description * Tracks changes to the the minimum length bound to this directive. */ // TODO(issue/24571): remove '!'. - @Input() minlength !: string | number; + @Input() minlength!: string|number; /** * @description @@ -400,7 +409,9 @@ export class MinLengthValidator implements Validator, * * @param fn The callback function */ - registerOnValidatorChange(fn: () => void): void { this._onChange = fn; } + registerOnValidatorChange(fn: () => void): void { + this._onChange = fn; + } private _createValidator(): void { this._validator = Validators.minLength( @@ -421,7 +432,7 @@ export const MAX_LENGTH_VALIDATOR: any = { /** * A directive that adds max length validation to controls marked with the * `maxlength` attribute. The directive is provided with the `NG_VALIDATORS` multi-provider list. - * + * * @see [Form Validation](guide/form-validation) * * @usageNotes @@ -444,19 +455,18 @@ export const MAX_LENGTH_VALIDATOR: any = { providers: [MAX_LENGTH_VALIDATOR], host: {'[attr.maxlength]': 'maxlength ? maxlength : null'} }) -export class MaxLengthValidator implements Validator, - OnChanges { +export class MaxLengthValidator implements Validator, OnChanges { // TODO(issue/24571): remove '!'. - private _validator !: ValidatorFn; + private _validator!: ValidatorFn; // TODO(issue/24571): remove '!'. - private _onChange !: () => void; + private _onChange!: () => void; /** * @description * Tracks changes to the the maximum length bound to this directive. */ // TODO(issue/24571): remove '!'. - @Input() maxlength !: string | number; + @Input() maxlength!: string|number; /** * @description @@ -487,7 +497,9 @@ export class MaxLengthValidator implements Validator, * * @param fn The callback function */ - registerOnValidatorChange(fn: () => void): void { this._onChange = fn; } + registerOnValidatorChange(fn: () => void): void { + this._onChange = fn; + } private _createValidator(): void { this._validator = Validators.maxLength( @@ -511,7 +523,7 @@ export const PATTERN_VALIDATOR: any = { * A directive that adds regex pattern validation to controls marked with the * `pattern` attribute. The regex must match the entire control value. * The directive is provided with the `NG_VALIDATORS` multi-provider list. - * + * * @see [Form Validation](guide/form-validation) * * @usageNotes @@ -524,7 +536,7 @@ export const PATTERN_VALIDATOR: any = { * ```html * * ``` - * + * * @ngModule ReactiveFormsModule * @ngModule FormsModule * @publicApi @@ -534,19 +546,18 @@ export const PATTERN_VALIDATOR: any = { providers: [PATTERN_VALIDATOR], host: {'[attr.pattern]': 'pattern ? pattern : null'} }) -export class PatternValidator implements Validator, - OnChanges { +export class PatternValidator implements Validator, OnChanges { // TODO(issue/24571): remove '!'. - private _validator !: ValidatorFn; + private _validator!: ValidatorFn; // TODO(issue/24571): remove '!'. - private _onChange !: () => void; + private _onChange!: () => void; /** * @description * Tracks changes to the pattern bound to this directive. */ // TODO(issue/24571): remove '!'. - @Input() pattern !: string | RegExp; + @Input() pattern!: string|RegExp; /** * @description @@ -567,7 +578,9 @@ export class PatternValidator implements Validator, * Method that validates whether the value matches the * the pattern requirement. */ - validate(control: AbstractControl): ValidationErrors|null { return this._validator(control); } + validate(control: AbstractControl): ValidationErrors|null { + return this._validator(control); + } /** * @description @@ -575,7 +588,11 @@ export class PatternValidator implements Validator, * * @param fn The callback function */ - registerOnValidatorChange(fn: () => void): void { this._onChange = fn; } + registerOnValidatorChange(fn: () => void): void { + this._onChange = fn; + } - private _createValidator(): void { this._validator = Validators.pattern(this.pattern); } + private _createValidator(): void { + this._validator = Validators.pattern(this.pattern); + } } diff --git a/packages/forms/src/form_builder.ts b/packages/forms/src/form_builder.ts index aa6faa7c81..d59cbeffaa 100644 --- a/packages/forms/src/form_builder.ts +++ b/packages/forms/src/form_builder.ts @@ -11,8 +11,8 @@ import {Injectable} from '@angular/core'; import {AsyncValidatorFn, ValidatorFn} from './directives/validators'; import {AbstractControl, AbstractControlOptions, FormArray, FormControl, FormGroup, FormHooks} from './model'; -function isAbstractControlOptions(options: AbstractControlOptions | {[key: string]: any}): - options is AbstractControlOptions { +function isAbstractControlOptions(options: AbstractControlOptions| + {[key: string]: any}): options is AbstractControlOptions { return (options).asyncValidators !== undefined || (options).validators !== undefined || (options).updateOn !== undefined; diff --git a/packages/forms/src/form_providers.ts b/packages/forms/src/form_providers.ts index 84934e9327..2fdd72b476 100644 --- a/packages/forms/src/form_providers.ts +++ b/packages/forms/src/form_providers.ts @@ -52,14 +52,13 @@ export class ReactiveFormsModule { * binding is used with reactive form directives. */ static withConfig(opts: { - /** @deprecated as of v6 */ warnOnNgModelWithFormControl: 'never' | 'once' | 'always' + /** @deprecated as of v6 */ warnOnNgModelWithFormControl: 'never'|'once'|'always' }): ModuleWithProviders { return { ngModule: ReactiveFormsModule, - providers: [{ - provide: NG_MODEL_WITH_FORM_CONTROL_WARNING, - useValue: opts.warnOnNgModelWithFormControl - }] + providers: [ + {provide: NG_MODEL_WITH_FORM_CONTROL_WARNING, useValue: opts.warnOnNgModelWithFormControl} + ] }; } } diff --git a/packages/forms/src/model.ts b/packages/forms/src/model.ts index ddbcfd76fc..4dbe67666f 100644 --- a/packages/forms/src/model.ts +++ b/packages/forms/src/model.ts @@ -44,7 +44,7 @@ export const PENDING = 'PENDING'; */ export const DISABLED = 'DISABLED'; -function _find(control: AbstractControl, path: Array| string, delimiter: string) { +function _find(control: AbstractControl, path: Array|string, delimiter: string) { if (path == null) return null; if (!Array.isArray(path)) { @@ -55,7 +55,7 @@ function _find(control: AbstractControl, path: Array| string, del // Not using Array.reduce here due to a Chrome 80 bug // https://bugs.chromium.org/p/chromium/issues/detail?id=1049982 let controlToFind: AbstractControl|null = control; - path.forEach((name: string | number) => { + path.forEach((name: string|number) => { if (controlToFind instanceof FormGroup) { controlToFind = controlToFind.controls.hasOwnProperty(name as string) ? controlToFind.controls[name] : @@ -69,9 +69,8 @@ function _find(control: AbstractControl, path: Array| string, del return controlToFind; } -function coerceToValidator( - validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null): ValidatorFn| - null { +function coerceToValidator(validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions| + null): ValidatorFn|null { const validator = (isOptionsObj(validatorOrOpts) ? (validatorOrOpts as AbstractControlOptions).validators : validatorOrOpts) as ValidatorFn | @@ -81,8 +80,9 @@ function coerceToValidator( } function coerceToAsyncValidator( - asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null, validatorOrOpts?: ValidatorFn | - ValidatorFn[] | AbstractControlOptions | null): AsyncValidatorFn|null { + asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null, + validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null): AsyncValidatorFn| + null { const origAsyncValidator = (isOptionsObj(validatorOrOpts) ? (validatorOrOpts as AbstractControlOptions).asyncValidators : asyncValidator) as AsyncValidatorFn | @@ -92,7 +92,7 @@ function coerceToAsyncValidator( origAsyncValidator || null; } -export type FormHooks = 'change' | 'blur' | 'submit'; +export type FormHooks = 'change'|'blur'|'submit'; /** * Interface for options provided to an `AbstractControl`. @@ -118,8 +118,8 @@ export interface AbstractControlOptions { } -function isOptionsObj( - validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null): boolean { +function isOptionsObj(validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions| + null): boolean { return validatorOrOpts != null && !Array.isArray(validatorOrOpts) && typeof validatorOrOpts === 'object'; } @@ -142,21 +142,21 @@ function isOptionsObj( export abstract class AbstractControl { /** @internal */ // TODO(issue/24571): remove '!'. - _pendingDirty !: boolean; + _pendingDirty!: boolean; /** @internal */ // TODO(issue/24571): remove '!'. - _pendingTouched !: boolean; + _pendingTouched!: boolean; /** @internal */ _onCollectionChange = () => {}; /** @internal */ // TODO(issue/24571): remove '!'. - _updateOn !: FormHooks; + _updateOn!: FormHooks; // TODO(issue/24571): remove '!'. - private _parent !: FormGroup | FormArray; + private _parent!: FormGroup|FormArray; private _asyncValidationSubscription: any; /** @@ -184,7 +184,9 @@ export abstract class AbstractControl { /** * The parent control. */ - get parent(): FormGroup|FormArray { return this._parent; } + get parent(): FormGroup|FormArray { + return this._parent; + } /** * The validation status of the control. There are four possible @@ -199,7 +201,7 @@ export abstract class AbstractControl { * both valid AND invalid or invalid AND disabled. */ // TODO(issue/24571): remove '!'. - public readonly status !: string; + public readonly status!: string; /** * A control is `valid` when its `status` is `VALID`. @@ -209,7 +211,9 @@ export abstract class AbstractControl { * @returns True if the control has passed all of its validation tests, * false otherwise. */ - get valid(): boolean { return this.status === VALID; } + get valid(): boolean { + return this.status === VALID; + } /** * A control is `invalid` when its `status` is `INVALID`. @@ -219,7 +223,9 @@ export abstract class AbstractControl { * @returns True if this control has failed one or more of its validation checks, * false otherwise. */ - get invalid(): boolean { return this.status === INVALID; } + get invalid(): boolean { + return this.status === INVALID; + } /** * A control is `pending` when its `status` is `PENDING`. @@ -229,7 +235,9 @@ export abstract class AbstractControl { * @returns True if this control is in the process of conducting a validation check, * false otherwise. */ - get pending(): boolean { return this.status == PENDING; } + get pending(): boolean { + return this.status == PENDING; + } /** * A control is `disabled` when its `status` is `DISABLED`. @@ -242,7 +250,9 @@ export abstract class AbstractControl { * * @returns True if the control is disabled, false otherwise. */ - get disabled(): boolean { return this.status === DISABLED; } + get disabled(): boolean { + return this.status === DISABLED; + } /** * A control is `enabled` as long as its `status` is not `DISABLED`. @@ -253,14 +263,16 @@ export abstract class AbstractControl { * @see {@link AbstractControl.status} * */ - get enabled(): boolean { return this.status !== DISABLED; } + get enabled(): boolean { + return this.status !== DISABLED; + } /** * An object containing any errors generated by failing validation, * or null if there are no errors. */ // TODO(issue/24571): remove '!'. - public readonly errors !: ValidationErrors | null; + public readonly errors!: ValidationErrors|null; /** * A control is `pristine` if the user has not yet changed @@ -278,7 +290,9 @@ export abstract class AbstractControl { * @returns True if the user has changed the value of this control in the UI; compare `pristine`. * Programmatic changes to a control's value do not mark it dirty. */ - get dirty(): boolean { return !this.pristine; } + get dirty(): boolean { + return !this.pristine; + } /** * True if the control is marked as `touched`. @@ -294,7 +308,9 @@ export abstract class AbstractControl { * A control is `untouched` if the user has not yet triggered * a `blur` event on it. */ - get untouched(): boolean { return !this.touched; } + get untouched(): boolean { + return !this.touched; + } /** * A multicasting observable that emits an event every time the value of the control changes, in @@ -302,7 +318,7 @@ export abstract class AbstractControl { * without passing along {emitEvent: false} as a function argument. */ // TODO(issue/24571): remove '!'. - public readonly valueChanges !: Observable; + public readonly valueChanges!: Observable; /** * A multicasting observable that emits an event every time the validation `status` of the control @@ -312,7 +328,7 @@ export abstract class AbstractControl { * */ // TODO(issue/24571): remove '!'. - public readonly statusChanges !: Observable; + public readonly statusChanges!: Observable; /** * Reports the update strategy of the `AbstractControl` (meaning @@ -355,7 +371,9 @@ export abstract class AbstractControl { * `updateValueAndValidity()` for the new validation to take effect. * */ - clearValidators(): void { this.validator = null; } + clearValidators(): void { + this.validator = null; + } /** * Empties out the async validator list. @@ -364,7 +382,9 @@ export abstract class AbstractControl { * `updateValueAndValidity()` for the new validation to take effect. * */ - clearAsyncValidators(): void { this.asyncValidator = null; } + clearAsyncValidators(): void { + this.asyncValidator = null; + } /** * Marks the control as `touched`. A control is touched by focus and @@ -380,7 +400,7 @@ export abstract class AbstractControl { * marks all direct ancestors. Default is false. */ markAsTouched(opts: {onlySelf?: boolean} = {}): void { - (this as{touched: boolean}).touched = true; + (this as {touched: boolean}).touched = true; if (this._parent && !opts.onlySelf) { this._parent.markAsTouched(opts); @@ -413,11 +433,12 @@ export abstract class AbstractControl { * marks all direct ancestors. Default is false. */ markAsUntouched(opts: {onlySelf?: boolean} = {}): void { - (this as{touched: boolean}).touched = false; + (this as {touched: boolean}).touched = false; this._pendingTouched = false; - this._forEachChild( - (control: AbstractControl) => { control.markAsUntouched({onlySelf: true}); }); + this._forEachChild((control: AbstractControl) => { + control.markAsUntouched({onlySelf: true}); + }); if (this._parent && !opts.onlySelf) { this._parent._updateTouched(opts); @@ -438,7 +459,7 @@ export abstract class AbstractControl { * marks all direct ancestors. Default is false. */ markAsDirty(opts: {onlySelf?: boolean} = {}): void { - (this as{pristine: boolean}).pristine = false; + (this as {pristine: boolean}).pristine = false; if (this._parent && !opts.onlySelf) { this._parent.markAsDirty(opts); @@ -462,10 +483,12 @@ export abstract class AbstractControl { * marks all direct ancestors. Default is false.. */ markAsPristine(opts: {onlySelf?: boolean} = {}): void { - (this as{pristine: boolean}).pristine = true; + (this as {pristine: boolean}).pristine = true; this._pendingDirty = false; - this._forEachChild((control: AbstractControl) => { control.markAsPristine({onlySelf: true}); }); + this._forEachChild((control: AbstractControl) => { + control.markAsPristine({onlySelf: true}); + }); if (this._parent && !opts.onlySelf) { this._parent._updatePristine(opts); @@ -489,7 +512,7 @@ export abstract class AbstractControl { * */ markAsPending(opts: {onlySelf?: boolean, emitEvent?: boolean} = {}): void { - (this as{status: string}).status = PENDING; + (this as {status: string}).status = PENDING; if (opts.emitEvent !== false) { (this.statusChanges as EventEmitter).emit(this.status); @@ -522,10 +545,11 @@ export abstract class AbstractControl { // parent's dirtiness based on the children. const skipPristineCheck = this._parentMarkedDirty(opts.onlySelf); - (this as{status: string}).status = DISABLED; - (this as{errors: ValidationErrors | null}).errors = null; - this._forEachChild( - (control: AbstractControl) => { control.disable({...opts, onlySelf: true}); }); + (this as {status: string}).status = DISABLED; + (this as {errors: ValidationErrors | null}).errors = null; + this._forEachChild((control: AbstractControl) => { + control.disable({...opts, onlySelf: true}); + }); this._updateValue(); if (opts.emitEvent !== false) { @@ -560,9 +584,10 @@ export abstract class AbstractControl { // parent's dirtiness based on the children. const skipPristineCheck = this._parentMarkedDirty(opts.onlySelf); - (this as{status: string}).status = VALID; - this._forEachChild( - (control: AbstractControl) => { control.enable({...opts, onlySelf: true}); }); + (this as {status: string}).status = VALID; + this._forEachChild((control: AbstractControl) => { + control.enable({...opts, onlySelf: true}); + }); this.updateValueAndValidity({onlySelf: true, emitEvent: opts.emitEvent}); this._updateAncestors({...opts, skipPristineCheck}); @@ -583,7 +608,9 @@ export abstract class AbstractControl { /** * @param parent Sets the parent of the control */ - setParent(parent: FormGroup|FormArray): void { this._parent = parent; } + setParent(parent: FormGroup|FormArray): void { + this._parent = parent; + } /** * Sets the value of the control. Abstract method (implemented in sub-classes). @@ -620,8 +647,8 @@ export abstract class AbstractControl { if (this.enabled) { this._cancelExistingSubscription(); - (this as{errors: ValidationErrors | null}).errors = this._runValidator(); - (this as{status: string}).status = this._calculateStatus(); + (this as {errors: ValidationErrors | null}).errors = this._runValidator(); + (this as {status: string}).status = this._calculateStatus(); if (this.status === VALID || this.status === PENDING) { this._runAsyncValidator(opts.emitEvent); @@ -645,7 +672,7 @@ export abstract class AbstractControl { } private _setInitialStatus() { - (this as{status: string}).status = this._allControlsDisabled() ? DISABLED : VALID; + (this as {status: string}).status = this._allControlsDisabled() ? DISABLED : VALID; } private _runValidator(): ValidationErrors|null { @@ -654,10 +681,10 @@ export abstract class AbstractControl { private _runAsyncValidator(emitEvent?: boolean): void { if (this.asyncValidator) { - (this as{status: string}).status = PENDING; + (this as {status: string}).status = PENDING; const obs = toObservable(this.asyncValidator(this)); this._asyncValidationSubscription = - obs.subscribe((errors: ValidationErrors | null) => this.setErrors(errors, {emitEvent})); + obs.subscribe((errors: ValidationErrors|null) => this.setErrors(errors, {emitEvent})); } } @@ -690,7 +717,7 @@ export abstract class AbstractControl { * ``` */ setErrors(errors: ValidationErrors|null, opts: {emitEvent?: boolean} = {}): void { - (this as{errors: ValidationErrors | null}).errors = errors; + (this as {errors: ValidationErrors | null}).errors = errors; this._updateControlsErrors(opts.emitEvent !== false); } @@ -711,7 +738,9 @@ export abstract class AbstractControl { * * * `this.form.get(['person', 'name']);` */ - get(path: Array|string): AbstractControl|null { return _find(this, path, '.'); } + get(path: Array|string): AbstractControl|null { + return _find(this, path, '.'); + } /** * @description @@ -794,7 +823,7 @@ export abstract class AbstractControl { /** @internal */ _updateControlsErrors(emitEvent: boolean): void { - (this as{status: string}).status = this._calculateStatus(); + (this as {status: string}).status = this._calculateStatus(); if (emitEvent) { (this.statusChanges as EventEmitter).emit(this.status); @@ -807,8 +836,8 @@ export abstract class AbstractControl { /** @internal */ _initObservables() { - (this as{valueChanges: Observable}).valueChanges = new EventEmitter(); - (this as{statusChanges: Observable}).statusChanges = new EventEmitter(); + (this as {valueChanges: Observable}).valueChanges = new EventEmitter(); + (this as {statusChanges: Observable}).statusChanges = new EventEmitter(); } @@ -852,7 +881,7 @@ export abstract class AbstractControl { /** @internal */ _updatePristine(opts: {onlySelf?: boolean} = {}): void { - (this as{pristine: boolean}).pristine = !this._anyControlsDirty(); + (this as {pristine: boolean}).pristine = !this._anyControlsDirty(); if (this._parent && !opts.onlySelf) { this._parent._updatePristine(opts); @@ -861,7 +890,7 @@ export abstract class AbstractControl { /** @internal */ _updateTouched(opts: {onlySelf?: boolean} = {}): void { - (this as{touched: boolean}).touched = this._anyControlsTouched(); + (this as {touched: boolean}).touched = this._anyControlsTouched(); if (this._parent && !opts.onlySelf) { this._parent._updateTouched(opts); @@ -878,12 +907,14 @@ export abstract class AbstractControl { } /** @internal */ - _registerOnCollectionChange(fn: () => void): void { this._onCollectionChange = fn; } + _registerOnCollectionChange(fn: () => void): void { + this._onCollectionChange = fn; + } /** @internal */ _setUpdateStrategy(opts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null): void { if (isOptionsObj(opts) && (opts as AbstractControlOptions).updateOn != null) { - this._updateOn = (opts as AbstractControlOptions).updateOn !; + this._updateOn = (opts as AbstractControlOptions).updateOn!; } } @@ -1006,18 +1037,18 @@ export class FormControl extends AbstractControl { _pendingChange: any; /** - * Creates a new `FormControl` instance. - * - * @param formState Initializes the control with an initial value, - * or an object that defines the initial value and disabled state. - * - * @param validatorOrOpts A synchronous validator function, or an array of - * such functions, or an `AbstractControlOptions` object that contains validation functions - * and a validation trigger. - * - * @param asyncValidator A single async validator or array of async validator functions - * - */ + * Creates a new `FormControl` instance. + * + * @param formState Initializes the control with an initial value, + * or an object that defines the initial value and disabled state. + * + * @param validatorOrOpts A synchronous validator function, or an array of + * such functions, or an `AbstractControlOptions` object that contains validation functions + * and a validation trigger. + * + * @param asyncValidator A single async validator or array of async validator functions + * + */ constructor( formState: any = null, validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null, @@ -1060,7 +1091,7 @@ export class FormControl extends AbstractControl { emitModelToViewChange?: boolean, emitViewToModelChange?: boolean } = {}): void { - (this as{value: any}).value = this._pendingValue = value; + (this as {value: any}).value = this._pendingValue = value; if (this._onChange.length && options.emitModelToViewChange !== false) { this._onChange.forEach( (changeFn) => changeFn(this.value, options.emitViewToModelChange !== false)); @@ -1120,19 +1151,25 @@ export class FormControl extends AbstractControl { /** * @internal */ - _anyControls(condition: Function): boolean { return false; } + _anyControls(condition: Function): boolean { + return false; + } /** * @internal */ - _allControlsDisabled(): boolean { return this.disabled; } + _allControlsDisabled(): boolean { + return this.disabled; + } /** * Register a listener for change events. * * @param fn The method that is called when the value changes */ - registerOnChange(fn: Function): void { this._onChange.push(fn); } + registerOnChange(fn: Function): void { + this._onChange.push(fn); + } /** * @internal @@ -1172,11 +1209,11 @@ export class FormControl extends AbstractControl { private _applyFormState(formState: any) { if (this._isBoxedValue(formState)) { - (this as{value: any}).value = this._pendingValue = formState.value; + (this as {value: any}).value = this._pendingValue = formState.value; formState.disabled ? this.disable({onlySelf: true, emitEvent: false}) : this.enable({onlySelf: true, emitEvent: false}); } else { - (this as{value: any}).value = this._pendingValue = formState; + (this as {value: any}).value = this._pendingValue = formState; } } } @@ -1255,18 +1292,18 @@ export class FormControl extends AbstractControl { */ export class FormGroup extends AbstractControl { /** - * Creates a new `FormGroup` instance. - * - * @param controls A collection of child controls. The key for each child is the name - * under which it is registered. - * - * @param validatorOrOpts A synchronous validator function, or an array of - * such functions, or an `AbstractControlOptions` object that contains validation functions - * and a validation trigger. - * - * @param asyncValidator A single async validator or array of async validator functions - * - */ + * Creates a new `FormGroup` instance. + * + * @param controls A collection of child controls. The key for each child is the name + * under which it is registered. + * + * @param validatorOrOpts A synchronous validator function, or an array of + * such functions, or an `AbstractControlOptions` object that contains validation functions + * and a validation trigger. + * + * @param asyncValidator A single async validator or array of async validator functions + * + */ constructor( public controls: {[key: string]: AbstractControl}, validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null, @@ -1556,7 +1593,9 @@ export class FormGroup extends AbstractControl { } /** @internal */ - _updateValue(): void { (this as{value: any}).value = this._reduceValue(); } + _updateValue(): void { + (this as {value: any}).value = this._reduceValue(); + } /** @internal */ _anyControls(condition: Function): boolean { @@ -1581,8 +1620,9 @@ export class FormGroup extends AbstractControl { /** @internal */ _reduceChildren(initValue: any, fn: Function) { let res = initValue; - this._forEachChild( - (control: AbstractControl, name: string) => { res = fn(res, control, name); }); + this._forEachChild((control: AbstractControl, name: string) => { + res = fn(res, control, name); + }); return res; } @@ -1647,7 +1687,7 @@ export class FormGroup extends AbstractControl { * ], {validators: myValidator, asyncValidators: myAsyncValidator}); * ``` * - * ### Set the updateOn property for all controls in a form array + * ### Set the updateOn property for all controls in a form array * * The options object is used to set a default value for each child * control's `updateOn` property. If you set `updateOn` to `'blur'` at the @@ -1672,18 +1712,18 @@ export class FormGroup extends AbstractControl { */ export class FormArray extends AbstractControl { /** - * Creates a new `FormArray` instance. - * - * @param controls An array of child controls. Each child control is given an index - * where it is registered. - * - * @param validatorOrOpts A synchronous validator function, or an array of - * such functions, or an `AbstractControlOptions` object that contains validation functions - * and a validation trigger. - * - * @param asyncValidator A single async validator or array of async validator functions - * - */ + * Creates a new `FormArray` instance. + * + * @param controls An array of child controls. Each child control is given an index + * where it is registered. + * + * @param validatorOrOpts A synchronous validator function, or an array of + * such functions, or an `AbstractControlOptions` object that contains validation functions + * and a validation trigger. + * + * @param asyncValidator A single async validator or array of async validator functions + * + */ constructor( public controls: AbstractControl[], validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null, @@ -1702,7 +1742,9 @@ export class FormArray extends AbstractControl { * * @param index Index in the array to retrieve the control */ - at(index: number): AbstractControl { return this.controls[index]; } + at(index: number): AbstractControl { + return this.controls[index]; + } /** * Insert a new `AbstractControl` at the end of the array. @@ -1762,7 +1804,9 @@ export class FormArray extends AbstractControl { /** * Length of the control array. */ - get length(): number { return this.controls.length; } + get length(): number { + return this.controls.length; + } /** * Sets the value of the `FormArray`. It accepts an array that matches @@ -1979,12 +2023,14 @@ export class FormArray extends AbstractControl { /** @internal */ _forEachChild(cb: Function): void { - this.controls.forEach((control: AbstractControl, index: number) => { cb(control, index); }); + this.controls.forEach((control: AbstractControl, index: number) => { + cb(control, index); + }); } /** @internal */ _updateValue(): void { - (this as{value: any}).value = + (this as {value: any}).value = this.controls.filter((control) => control.enabled || this.disabled) .map((control) => control.value); } diff --git a/packages/forms/src/validators.ts b/packages/forms/src/validators.ts index 0b7a171bf3..51bc05f438 100644 --- a/packages/forms/src/validators.ts +++ b/packages/forms/src/validators.ts @@ -7,8 +7,9 @@ */ import {InjectionToken, ɵisObservable as isObservable, ɵisPromise as isPromise} from '@angular/core'; -import {Observable, forkJoin, from} from 'rxjs'; +import {forkJoin, from, Observable} from 'rxjs'; import {map} from 'rxjs/operators'; + import {AsyncValidatorFn, ValidationErrors, Validator, ValidatorFn} from './directives/validators'; import {AbstractControl, FormControl} from './model'; @@ -19,7 +20,8 @@ function isEmptyInputValue(value: any): boolean { /** * @description - * An `InjectionToken` for registering additional synchronous validators used with `AbstractControl`s. + * An `InjectionToken` for registering additional synchronous validators used with + * `AbstractControl`s. * * @see `NG_ASYNC_VALIDATORS` * @@ -48,7 +50,8 @@ export const NG_VALIDATORS = new InjectionToken>('NgVa /** * @description - * An `InjectionToken` for registering additional asynchronous validators used with `AbstractControl`s. + * An `InjectionToken` for registering additional asynchronous validators used with + * `AbstractControl`s. * * @see `NG_VALIDATORS` * @@ -124,7 +127,7 @@ export class Validators { * */ static min(min: number): ValidatorFn { - return (control: AbstractControl): ValidationErrors | null => { + return (control: AbstractControl): ValidationErrors|null => { if (isEmptyInputValue(control.value) || isEmptyInputValue(min)) { return null; // don't validate empty values to allow optional controls } @@ -157,7 +160,7 @@ export class Validators { * */ static max(max: number): ValidatorFn { - return (control: AbstractControl): ValidationErrors | null => { + return (control: AbstractControl): ValidationErrors|null => { if (isEmptyInputValue(control.value) || isEmptyInputValue(max)) { return null; // don't validate empty values to allow optional controls } @@ -221,11 +224,13 @@ export class Validators { * @description * Validator that requires the control's value pass an email validation test. * - * Tests the value using a [regular expression](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions) + * Tests the value using a [regular + * expression](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions) * pattern suitable for common usecases. The pattern is based on the definition of a valid email - * address in the [WHATWG HTML specification](https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address) - * with some enhancements to incorporate more RFC rules (such as rules related to domain names and - * the lengths of different parts of the address). + * address in the [WHATWG HTML + * specification](https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address) with + * some enhancements to incorporate more RFC rules (such as rules related to domain names and the + * lengths of different parts of the address). * * The differences from the WHATWG version include: * - Disallow `local-part` (the part before the `@` symbol) to begin or end with a period (`.`). @@ -285,7 +290,7 @@ export class Validators { * */ static minLength(minLength: number): ValidatorFn { - return (control: AbstractControl): ValidationErrors | null => { + return (control: AbstractControl): ValidationErrors|null => { if (isEmptyInputValue(control.value)) { return null; // don't validate empty values to allow optional controls } @@ -323,7 +328,7 @@ export class Validators { * */ static maxLength(maxLength: number): ValidatorFn { - return (control: AbstractControl): ValidationErrors | null => { + return (control: AbstractControl): ValidationErrors|null => { const length: number = control.value ? control.value.length : 0; return length > maxLength ? {'maxlength': {'requiredLength': maxLength, 'actualLength': length}} : @@ -379,7 +384,7 @@ export class Validators { regexStr = pattern.toString(); regex = pattern; } - return (control: AbstractControl): ValidationErrors | null => { + return (control: AbstractControl): ValidationErrors|null => { if (isEmptyInputValue(control.value)) { return null; // don't validate empty values to allow optional controls } @@ -396,7 +401,9 @@ export class Validators { * @see `updateValueAndValidity()` * */ - static nullValidator(control: AbstractControl): ValidationErrors|null { return null; } + static nullValidator(control: AbstractControl): ValidationErrors|null { + return null; + } /** * @description @@ -469,8 +476,8 @@ function _mergeErrors(arrayOfErrors: ValidationErrors[]): ValidationErrors|null // Not using Array.reduce here due to a Chrome 80 bug // https://bugs.chromium.org/p/chromium/issues/detail?id=1049982 - arrayOfErrors.forEach((errors: ValidationErrors | null) => { - res = errors != null ? {...res !, ...errors} : res !; + arrayOfErrors.forEach((errors: ValidationErrors|null) => { + res = errors != null ? {...res!, ...errors} : res!; }); return Object.keys(res).length === 0 ? null : res; diff --git a/packages/forms/test/directives_spec.ts b/packages/forms/test/directives_spec.ts index 0876818c2a..06e72afbe5 100644 --- a/packages/forms/test/directives_spec.ts +++ b/packages/forms/test/directives_spec.ts @@ -19,22 +19,30 @@ class DummyControlValueAccessor implements ControlValueAccessor { registerOnChange(fn: any) {} registerOnTouched(fn: any) {} - writeValue(obj: any): void { this.writtenValue = obj; } + writeValue(obj: any): void { + this.writtenValue = obj; + } } class CustomValidatorDirective implements Validator { - validate(c: FormControl): ValidationErrors { return {'custom': true}; } + validate(c: FormControl): ValidationErrors { + return {'custom': true}; + } } function asyncValidator(expected: any, timeout = 0) { return (c: AbstractControl): any => { - let resolve: (result: any) => void = undefined !; - const promise = new Promise(res => { resolve = res; }); + let resolve: (result: any) => void = undefined!; + const promise = new Promise(res => { + resolve = res; + }); const res = c.value != expected ? {'async': true} : null; if (timeout == 0) { resolve(res); } else { - setTimeout(() => { resolve(res); }, timeout); + setTimeout(() => { + resolve(res); + }, timeout); } return promise; }; @@ -44,16 +52,21 @@ function asyncValidator(expected: any, timeout = 0) { describe('Form Directives', () => { let defaultAccessor: DefaultValueAccessor; - beforeEach(() => { defaultAccessor = new DefaultValueAccessor(null !, null !, null !); }); + beforeEach(() => { + defaultAccessor = new DefaultValueAccessor(null!, null!, null!); + }); describe('shared', () => { describe('selectValueAccessor', () => { let dir: NgControl; - beforeEach(() => { dir = new SpyNgControl(); }); + beforeEach(() => { + dir = new SpyNgControl(); + }); - it('should throw when given an empty array', - () => { expect(() => selectValueAccessor(dir, [])).toThrowError(); }); + it('should throw when given an empty array', () => { + expect(() => selectValueAccessor(dir, [])).toThrowError(); + }); it('should throw when accessor is not provided as array', () => { expect(() => selectValueAccessor(dir, {} as any[])) @@ -61,49 +74,51 @@ function asyncValidator(expected: any, timeout = 0) { `Value accessor was not provided as an array for form control with unspecified name attribute`); }); - it('should return the default value accessor when no other provided', - () => { expect(selectValueAccessor(dir, [defaultAccessor])).toEqual(defaultAccessor); }); + it('should return the default value accessor when no other provided', () => { + expect(selectValueAccessor(dir, [defaultAccessor])).toEqual(defaultAccessor); + }); it('should return checkbox accessor when provided', () => { - const checkboxAccessor = new CheckboxControlValueAccessor(null !, null !); + const checkboxAccessor = new CheckboxControlValueAccessor(null!, null!); expect(selectValueAccessor(dir, [ defaultAccessor, checkboxAccessor ])).toEqual(checkboxAccessor); }); it('should return select accessor when provided', () => { - const selectAccessor = new SelectControlValueAccessor(null !, null !); + const selectAccessor = new SelectControlValueAccessor(null!, null!); expect(selectValueAccessor(dir, [ defaultAccessor, selectAccessor ])).toEqual(selectAccessor); }); it('should return select multiple accessor when provided', () => { - const selectMultipleAccessor = new SelectMultipleControlValueAccessor(null !, null !); + const selectMultipleAccessor = new SelectMultipleControlValueAccessor(null!, null!); expect(selectValueAccessor(dir, [ defaultAccessor, selectMultipleAccessor ])).toEqual(selectMultipleAccessor); }); it('should throw when more than one build-in accessor is provided', () => { - const checkboxAccessor = new CheckboxControlValueAccessor(null !, null !); - const selectAccessor = new SelectControlValueAccessor(null !, null !); + const checkboxAccessor = new CheckboxControlValueAccessor(null!, null!); + const selectAccessor = new SelectControlValueAccessor(null!, null!); expect(() => selectValueAccessor(dir, [checkboxAccessor, selectAccessor])).toThrowError(); }); it('should return custom accessor when provided', () => { const customAccessor: ControlValueAccessor = new SpyValueAccessor() as any; - const checkboxAccessor = new CheckboxControlValueAccessor(null !, null !); - expect(selectValueAccessor(dir, [defaultAccessor, customAccessor, checkboxAccessor])) - .toEqual(customAccessor); + const checkboxAccessor = new CheckboxControlValueAccessor(null!, null!); + expect(selectValueAccessor(dir, [ + defaultAccessor, customAccessor, checkboxAccessor + ])).toEqual(customAccessor); }); it('should return custom accessor when provided with select multiple', () => { const customAccessor: ControlValueAccessor = new SpyValueAccessor() as any; - const selectMultipleAccessor = new SelectMultipleControlValueAccessor(null !, null !); - expect(selectValueAccessor( - dir, [defaultAccessor, customAccessor, selectMultipleAccessor])) - .toEqual(customAccessor); + const selectMultipleAccessor = new SelectMultipleControlValueAccessor(null!, null!); + expect(selectValueAccessor(dir, [ + defaultAccessor, customAccessor, selectMultipleAccessor + ])).toEqual(customAccessor); }); it('should throw when more than one custom accessor is provided', () => { @@ -116,13 +131,13 @@ function asyncValidator(expected: any, timeout = 0) { it('should compose functions', () => { const dummy1 = (_: any /** TODO #9100 */) => ({'dummy1': true}); const dummy2 = (_: any /** TODO #9100 */) => ({'dummy2': true}); - const v = composeValidators([dummy1, dummy2]) !; + const v = composeValidators([dummy1, dummy2])!; expect(v(new FormControl(''))).toEqual({'dummy1': true, 'dummy2': true}); }); it('should compose validator directives', () => { const dummy1 = (_: any /** TODO #9100 */) => ({'dummy1': true}); - const v = composeValidators([dummy1, new CustomValidatorDirective()]) !; + const v = composeValidators([dummy1, new CustomValidatorDirective()])!; expect(v(new FormControl(''))).toEqual({'dummy1': true, 'custom': true}); }); }); @@ -137,8 +152,8 @@ function asyncValidator(expected: any, timeout = 0) { form = new FormGroupDirective([], []); formModel = new FormGroup({ 'login': new FormControl(), - 'passwords': new FormGroup( - {'password': new FormControl(), 'passwordConfirm': new FormControl()}) + 'passwords': + new FormGroup({'password': new FormControl(), 'passwordConfirm': new FormControl()}) }); form.form = formModel; @@ -174,7 +189,7 @@ function asyncValidator(expected: any, timeout = 0) { describe('addControl', () => { it('should throw when no control found', () => { - const dir = new FormControlName(form, null !, null !, [defaultAccessor], null); + const dir = new FormControlName(form, null!, null!, [defaultAccessor], null); dir.name = 'invalidName'; expect(() => form.addControl(dir)) @@ -182,7 +197,7 @@ function asyncValidator(expected: any, timeout = 0) { }); it('should throw for a named control when no value accessor', () => { - const dir = new FormControlName(form, null !, null !, null !, null); + const dir = new FormControlName(form, null!, null!, null!, null); dir.name = 'login'; expect(() => form.addControl(dir)) @@ -190,8 +205,8 @@ function asyncValidator(expected: any, timeout = 0) { }); it('should throw when no value accessor with path', () => { - const group = new FormGroupName(form, null !, null !); - const dir = new FormControlName(group, null !, null !, null !, null); + const group = new FormGroupName(form, null!, null!); + const dir = new FormControlName(group, null!, null!, null!, null); group.name = 'passwords'; dir.name = 'password'; @@ -321,7 +336,7 @@ function asyncValidator(expected: any, timeout = 0) { personControlGroupDir = new NgModelGroup(form, [], []); personControlGroupDir.name = 'person'; - loginControlDir = new NgModel(personControlGroupDir, null !, null !, [defaultAccessor]); + loginControlDir = new NgModel(personControlGroupDir, null!, null!, [defaultAccessor]); loginControlDir.name = 'login'; loginControlDir.valueAccessor = new DummyControlValueAccessor(); }); @@ -509,7 +524,9 @@ function asyncValidator(expected: any, timeout = 0) { controlDir.form = control; }); - it('should reexport control properties', () => { checkProperties(control); }); + it('should reexport control properties', () => { + checkProperties(control); + }); it('should reexport control methods', () => { expect(controlDir.hasError('required')).toBe(control.hasError('required')); @@ -544,7 +561,7 @@ function asyncValidator(expected: any, timeout = 0) { beforeEach(() => { ngModel = new NgModel( - null !, [Validators.required], [asyncValidator('expected')], [defaultAccessor]); + null!, [Validators.required], [asyncValidator('expected')], [defaultAccessor]); ngModel.valueAccessor = new DummyControlValueAccessor(); control = ngModel.control; }); @@ -577,7 +594,7 @@ function asyncValidator(expected: any, timeout = 0) { }); it('should throw when no value accessor with named control', () => { - const namedDir = new NgModel(null !, null !, null !, null !); + const namedDir = new NgModel(null!, null!, null!, null!); namedDir.name = 'one'; expect(() => namedDir.ngOnChanges({})) @@ -585,7 +602,7 @@ function asyncValidator(expected: any, timeout = 0) { }); it('should throw when no value accessor with unnamed control', () => { - const unnamedDir = new NgModel(null !, null !, null !, null !); + const unnamedDir = new NgModel(null!, null!, null!, null!); expect(() => unnamedDir.ngOnChanges({})) .toThrowError( @@ -641,7 +658,6 @@ function asyncValidator(expected: any, timeout = 0) { ngModel.ngOnChanges({isDisabled: new SimpleChange(null, 'anything else', false)}); tick(); expect(ngModel.control.disabled).toEqual(true); - })); }); @@ -656,7 +672,7 @@ function asyncValidator(expected: any, timeout = 0) { parent.form = new FormGroup({'name': formModel}); controlNameDir = new FormControlName(parent, [], [], [defaultAccessor], null); controlNameDir.name = 'name'; - (controlNameDir as{control: FormControl}).control = formModel; + (controlNameDir as {control: FormControl}).control = formModel; }); it('should reexport control properties', () => { diff --git a/packages/forms/test/form_array_spec.ts b/packages/forms/test/form_array_spec.ts index 6bac92e453..1bdc729233 100644 --- a/packages/forms/test/form_array_spec.ts +++ b/packages/forms/test/form_array_spec.ts @@ -10,1229 +10,1263 @@ import {fakeAsync, tick} from '@angular/core/testing'; import {AsyncTestCompleter, beforeEach, describe, inject, it} from '@angular/core/testing/src/testing_internal'; import {AbstractControl, FormArray, FormControl, FormGroup, ValidationErrors, ValidatorFn} from '@angular/forms'; import {Validators} from '@angular/forms/src/validators'; -import {of } from 'rxjs'; +import {of} from 'rxjs'; (function() { - function asyncValidator(expected: string, timeouts = {}) { - return (c: AbstractControl) => { - let resolve: (result: any) => void = undefined !; - const promise = new Promise(res => { resolve = res; }); - const t = (timeouts as any)[c.value] != null ? (timeouts as any)[c.value] : 0; - const res = c.value != expected ? {'async': true} : null; +function asyncValidator(expected: string, timeouts = {}) { + return (c: AbstractControl) => { + let resolve: (result: any) => void = undefined!; + const promise = new Promise(res => { + resolve = res; + }); + const t = (timeouts as any)[c.value] != null ? (timeouts as any)[c.value] : 0; + const res = c.value != expected ? {'async': true} : null; - if (t == 0) { + if (t == 0) { + resolve(res); + } else { + setTimeout(() => { resolve(res); - } else { - setTimeout(() => { resolve(res); }, t); - } + }, t); + } - return promise; - }; - } + return promise; + }; +} - describe('FormArray', () => { +describe('FormArray', () => { + describe('adding/removing', () => { + let a: FormArray; + let c1: FormControl, c2: FormControl, c3: FormControl; - describe('adding/removing', () => { - let a: FormArray; - let c1: FormControl, c2: FormControl, c3: FormControl; - - beforeEach(() => { - a = new FormArray([]); - c1 = new FormControl(1); - c2 = new FormControl(2); - c3 = new FormControl(3); - }); - - it('should support pushing', () => { - a.push(c1); - expect(a.length).toEqual(1); - expect(a.controls).toEqual([c1]); - }); - - it('should support removing', () => { - a.push(c1); - a.push(c2); - a.push(c3); - - a.removeAt(1); - - expect(a.controls).toEqual([c1, c3]); - }); - - it('should support clearing', () => { - a.push(c1); - a.push(c2); - a.push(c3); - - a.clear(); - - expect(a.controls).toEqual([]); - - a.clear(); - - expect(a.controls).toEqual([]); - }); - - it('should support inserting', () => { - a.push(c1); - a.push(c3); - - a.insert(1, c2); - - expect(a.controls).toEqual([c1, c2, c3]); - }); + beforeEach(() => { + a = new FormArray([]); + c1 = new FormControl(1); + c2 = new FormControl(2); + c3 = new FormControl(3); }); - describe('value', () => { - it('should be the reduced value of the child controls', () => { - const a = new FormArray([new FormControl(1), new FormControl(2)]); - expect(a.value).toEqual([1, 2]); - }); - - it('should be an empty array when there are no child controls', () => { - const a = new FormArray([]); - expect(a.value).toEqual([]); - }); + it('should support pushing', () => { + a.push(c1); + expect(a.length).toEqual(1); + expect(a.controls).toEqual([c1]); }); - describe('getRawValue()', () => { - let a: FormArray; + it('should support removing', () => { + a.push(c1); + a.push(c2); + a.push(c3); - it('should work with nested form groups/arrays', () => { - a = new FormArray([ - new FormGroup({'c2': new FormControl('v2'), 'c3': new FormControl('v3')}), - new FormArray([new FormControl('v4'), new FormControl('v5')]) - ]); - a.at(0).get('c3') !.disable(); - (a.at(1) as FormArray).at(1).disable(); + a.removeAt(1); - expect(a.getRawValue()).toEqual([{'c2': 'v2', 'c3': 'v3'}, ['v4', 'v5']]); - }); + expect(a.controls).toEqual([c1, c3]); }); - describe('markAllAsTouched', () => { - it('should mark all descendants as touched', () => { - const formArray: FormArray = new FormArray([ - new FormControl('v1'), new FormControl('v2'), - new FormGroup({'c1': new FormControl('v1')}), - new FormArray([new FormGroup({'c2': new FormControl('v2')})]) - ]); + it('should support clearing', () => { + a.push(c1); + a.push(c2); + a.push(c3); - expect(formArray.touched).toBe(false); + a.clear(); - const control1 = formArray.at(0) as FormControl; + expect(a.controls).toEqual([]); - expect(control1.touched).toBe(false); + a.clear(); - const group1 = formArray.at(2) as FormGroup; - - expect(group1.touched).toBe(false); - - const group1Control1 = group1.get('c1') as FormControl; - - expect(group1Control1.touched).toBe(false); - - const innerFormArray = formArray.at(3) as FormArray; - - expect(innerFormArray.touched).toBe(false); - - const innerFormArrayGroup = innerFormArray.at(0) as FormGroup; - - expect(innerFormArrayGroup.touched).toBe(false); - - const innerFormArrayGroupControl1 = innerFormArrayGroup.get('c2') as FormControl; - - expect(innerFormArrayGroupControl1.touched).toBe(false); - - formArray.markAllAsTouched(); - - expect(formArray.touched).toBe(true); - - expect(control1.touched).toBe(true); - - expect(group1.touched).toBe(true); - - expect(group1Control1.touched).toBe(true); - - expect(innerFormArray.touched).toBe(true); - - expect(innerFormArrayGroup.touched).toBe(true); - - expect(innerFormArrayGroupControl1.touched).toBe(true); - }); + expect(a.controls).toEqual([]); }); - describe('setValue', () => { - let c: FormControl, c2: FormControl, a: FormArray; + it('should support inserting', () => { + a.push(c1); + a.push(c3); - beforeEach(() => { - c = new FormControl(''); - c2 = new FormControl(''); - a = new FormArray([c, c2]); - }); - - it('should set its own value', () => { - a.setValue(['one', 'two']); - expect(a.value).toEqual(['one', 'two']); - }); - - it('should set child values', () => { - a.setValue(['one', 'two']); - expect(c.value).toEqual('one'); - expect(c2.value).toEqual('two'); - }); - - it('should set values for disabled child controls', () => { - c2.disable(); - a.setValue(['one', 'two']); - expect(c2.value).toEqual('two'); - expect(a.value).toEqual(['one']); - expect(a.getRawValue()).toEqual(['one', 'two']); - }); - - it('should set value for disabled arrays', () => { - a.disable(); - a.setValue(['one', 'two']); - expect(c.value).toEqual('one'); - expect(c2.value).toEqual('two'); - expect(a.value).toEqual(['one', 'two']); - }); - - it('should set parent values', () => { - const form = new FormGroup({'parent': a}); - a.setValue(['one', 'two']); - expect(form.value).toEqual({'parent': ['one', 'two']}); - }); - - it('should not update the parent explicitly specified', () => { - const form = new FormGroup({'parent': a}); - a.setValue(['one', 'two'], {onlySelf: true}); - - expect(form.value).toEqual({parent: ['', '']}); - }); - - it('should throw if fields are missing from supplied value (subset)', () => { - expect(() => a.setValue([, 'two'])) - .toThrowError(new RegExp(`Must supply a value for form control at index: 0`)); - }); - - it('should throw if a value is provided for a missing control (superset)', () => { - expect(() => a.setValue([ - 'one', 'two', 'three' - ])).toThrowError(new RegExp(`Cannot find form control at index 2`)); - }); - - it('should throw if a value is not provided for a disabled control', () => { - c2.disable(); - expect(() => a.setValue(['one'])) - .toThrowError(new RegExp(`Must supply a value for form control at index: 1`)); - }); - - it('should throw if no controls are set yet', () => { - const empty = new FormArray([]); - expect(() => empty.setValue(['one'])) - .toThrowError(new RegExp(`no form controls registered with this array`)); - }); - - describe('setValue() events', () => { - let form: FormGroup; - let logger: any[]; - - beforeEach(() => { - form = new FormGroup({'parent': a}); - logger = []; - }); - - it('should emit one valueChange event per control', () => { - form.valueChanges.subscribe(() => logger.push('form')); - a.valueChanges.subscribe(() => logger.push('array')); - c.valueChanges.subscribe(() => logger.push('control1')); - c2.valueChanges.subscribe(() => logger.push('control2')); - - a.setValue(['one', 'two']); - expect(logger).toEqual(['control1', 'control2', 'array', 'form']); - }); - - it('should not fire an event when explicitly specified', fakeAsync(() => { - form.valueChanges.subscribe((value) => { throw 'Should not happen'; }); - a.valueChanges.subscribe((value) => { throw 'Should not happen'; }); - c.valueChanges.subscribe((value) => { throw 'Should not happen'; }); - c2.valueChanges.subscribe((value) => { throw 'Should not happen'; }); - - a.setValue(['one', 'two'], {emitEvent: false}); - tick(); - })); - - it('should emit one statusChange event per control', () => { - form.statusChanges.subscribe(() => logger.push('form')); - a.statusChanges.subscribe(() => logger.push('array')); - c.statusChanges.subscribe(() => logger.push('control1')); - c2.statusChanges.subscribe(() => logger.push('control2')); - - a.setValue(['one', 'two']); - expect(logger).toEqual(['control1', 'control2', 'array', 'form']); - }); - }); - }); - - describe('patchValue', () => { - let c: FormControl, c2: FormControl, a: FormArray; - - beforeEach(() => { - c = new FormControl(''); - c2 = new FormControl(''); - a = new FormArray([c, c2]); - }); - - it('should set its own value', () => { - a.patchValue(['one', 'two']); - expect(a.value).toEqual(['one', 'two']); - }); - - it('should set child values', () => { - a.patchValue(['one', 'two']); - expect(c.value).toEqual('one'); - expect(c2.value).toEqual('two'); - }); - - it('should patch disabled control values', () => { - c2.disable(); - a.patchValue(['one', 'two']); - expect(c2.value).toEqual('two'); - expect(a.value).toEqual(['one']); - expect(a.getRawValue()).toEqual(['one', 'two']); - }); - - it('should patch disabled control arrays', () => { - a.disable(); - a.patchValue(['one', 'two']); - expect(c.value).toEqual('one'); - expect(c2.value).toEqual('two'); - expect(a.value).toEqual(['one', 'two']); - }); - - it('should set parent values', () => { - const form = new FormGroup({'parent': a}); - a.patchValue(['one', 'two']); - expect(form.value).toEqual({'parent': ['one', 'two']}); - }); - - it('should not update the parent explicitly specified', () => { - const form = new FormGroup({'parent': a}); - a.patchValue(['one', 'two'], {onlySelf: true}); - - expect(form.value).toEqual({parent: ['', '']}); - }); - - it('should ignore fields that are missing from supplied value (subset)', () => { - a.patchValue([, 'two']); - expect(a.value).toEqual(['', 'two']); - }); - - it('should not ignore fields that are null', () => { - a.patchValue([null]); - expect(a.value).toEqual([null, '']); - }); - - it('should ignore any value provided for a missing control (superset)', () => { - a.patchValue([, , 'three']); - expect(a.value).toEqual(['', '']); - }); - - describe('patchValue() events', () => { - let form: FormGroup; - let logger: any[]; - - beforeEach(() => { - form = new FormGroup({'parent': a}); - logger = []; - }); - - it('should emit one valueChange event per control', () => { - form.valueChanges.subscribe(() => logger.push('form')); - a.valueChanges.subscribe(() => logger.push('array')); - c.valueChanges.subscribe(() => logger.push('control1')); - c2.valueChanges.subscribe(() => logger.push('control2')); - - a.patchValue(['one', 'two']); - expect(logger).toEqual(['control1', 'control2', 'array', 'form']); - }); - - it('should not emit valueChange events for skipped controls', () => { - form.valueChanges.subscribe(() => logger.push('form')); - a.valueChanges.subscribe(() => logger.push('array')); - c.valueChanges.subscribe(() => logger.push('control1')); - c2.valueChanges.subscribe(() => logger.push('control2')); - - a.patchValue(['one']); - expect(logger).toEqual(['control1', 'array', 'form']); - }); - - it('should not fire an event when explicitly specified', fakeAsync(() => { - form.valueChanges.subscribe((value) => { throw 'Should not happen'; }); - a.valueChanges.subscribe((value) => { throw 'Should not happen'; }); - c.valueChanges.subscribe((value) => { throw 'Should not happen'; }); - c2.valueChanges.subscribe((value) => { throw 'Should not happen'; }); - - a.patchValue(['one', 'two'], {emitEvent: false}); - tick(); - })); - - it('should emit one statusChange event per control', () => { - form.statusChanges.subscribe(() => logger.push('form')); - a.statusChanges.subscribe(() => logger.push('array')); - c.statusChanges.subscribe(() => logger.push('control1')); - c2.statusChanges.subscribe(() => logger.push('control2')); - - a.patchValue(['one', 'two']); - expect(logger).toEqual(['control1', 'control2', 'array', 'form']); - }); - }); - }); - - describe('reset()', () => { - let c: FormControl, c2: FormControl, a: FormArray; - - beforeEach(() => { - c = new FormControl('initial value'); - c2 = new FormControl(''); - a = new FormArray([c, c2]); - }); - - it('should set its own value if value passed', () => { - a.setValue(['new value', 'new value']); - - a.reset(['initial value', '']); - expect(a.value).toEqual(['initial value', '']); - }); - - it('should not update the parent when explicitly specified', () => { - const form = new FormGroup({'a': a}); - a.reset(['one', 'two'], {onlySelf: true}); - - expect(form.value).toEqual({a: ['initial value', '']}); - }); - - it('should set its own value if boxed value passed', () => { - a.setValue(['new value', 'new value']); - - a.reset([{value: 'initial value', disabled: false}, '']); - expect(a.value).toEqual(['initial value', '']); - }); - - it('should clear its own value if no value passed', () => { - a.setValue(['new value', 'new value']); - - a.reset(); - expect(a.value).toEqual([null, null]); - }); - - it('should set the value of each of its child controls if value passed', () => { - a.setValue(['new value', 'new value']); - - a.reset(['initial value', '']); - expect(c.value).toBe('initial value'); - expect(c2.value).toBe(''); - }); - - it('should clear the value of each of its child controls if no value', () => { - a.setValue(['new value', 'new value']); - - a.reset(); - expect(c.value).toBe(null); - expect(c2.value).toBe(null); - }); - - it('should set the value of its parent if value passed', () => { - const form = new FormGroup({'a': a}); - a.setValue(['new value', 'new value']); - - a.reset(['initial value', '']); - expect(form.value).toEqual({'a': ['initial value', '']}); - }); - - it('should clear the value of its parent if no value passed', () => { - const form = new FormGroup({'a': a}); - a.setValue(['new value', 'new value']); - - a.reset(); - expect(form.value).toEqual({'a': [null, null]}); - }); - - it('should mark itself as pristine', () => { - a.markAsDirty(); - expect(a.pristine).toBe(false); - - a.reset(); - expect(a.pristine).toBe(true); - }); - - it('should mark all child controls as pristine', () => { - c.markAsDirty(); - c2.markAsDirty(); - expect(c.pristine).toBe(false); - expect(c2.pristine).toBe(false); - - a.reset(); - expect(c.pristine).toBe(true); - expect(c2.pristine).toBe(true); - }); - - it('should mark the parent as pristine if all siblings pristine', () => { - const c3 = new FormControl(''); - const form = new FormGroup({'a': a, 'c3': c3}); - - a.markAsDirty(); - expect(form.pristine).toBe(false); - - a.reset(); - expect(form.pristine).toBe(true); - }); - - it('should not mark the parent pristine if any dirty siblings', () => { - const c3 = new FormControl(''); - const form = new FormGroup({'a': a, 'c3': c3}); - - a.markAsDirty(); - c3.markAsDirty(); - expect(form.pristine).toBe(false); - - a.reset(); - expect(form.pristine).toBe(false); - }); - - it('should mark itself as untouched', () => { - a.markAsTouched(); - expect(a.untouched).toBe(false); - - a.reset(); - expect(a.untouched).toBe(true); - }); - - it('should mark all child controls as untouched', () => { - c.markAsTouched(); - c2.markAsTouched(); - expect(c.untouched).toBe(false); - expect(c2.untouched).toBe(false); - - a.reset(); - expect(c.untouched).toBe(true); - expect(c2.untouched).toBe(true); - }); - - it('should mark the parent untouched if all siblings untouched', () => { - const c3 = new FormControl(''); - const form = new FormGroup({'a': a, 'c3': c3}); - - a.markAsTouched(); - expect(form.untouched).toBe(false); - - a.reset(); - expect(form.untouched).toBe(true); - }); - - it('should not mark the parent untouched if any touched siblings', () => { - const c3 = new FormControl(''); - const form = new FormGroup({'a': a, 'c3': c3}); - - a.markAsTouched(); - c3.markAsTouched(); - expect(form.untouched).toBe(false); - - a.reset(); - expect(form.untouched).toBe(false); - }); - - it('should retain previous disabled state', () => { - a.disable(); - a.reset(); - - expect(a.disabled).toBe(true); - }); - - it('should set child disabled state if boxed value passed', () => { - a.disable(); - a.reset([{value: '', disabled: false}, '']); - - expect(c.disabled).toBe(false); - expect(a.disabled).toBe(false); - }); - - - describe('reset() events', () => { - let form: FormGroup, c3: FormControl, logger: any[]; - - beforeEach(() => { - c3 = new FormControl(''); - form = new FormGroup({'a': a, 'c3': c3}); - logger = []; - }); - - it('should emit one valueChange event per reset control', () => { - form.valueChanges.subscribe(() => logger.push('form')); - a.valueChanges.subscribe(() => logger.push('array')); - c.valueChanges.subscribe(() => logger.push('control1')); - c2.valueChanges.subscribe(() => logger.push('control2')); - c3.valueChanges.subscribe(() => logger.push('control3')); - - a.reset(); - expect(logger).toEqual(['control1', 'control2', 'array', 'form']); - }); - - it('should not fire an event when explicitly specified', fakeAsync(() => { - form.valueChanges.subscribe((value) => { throw 'Should not happen'; }); - a.valueChanges.subscribe((value) => { throw 'Should not happen'; }); - c.valueChanges.subscribe((value) => { throw 'Should not happen'; }); - c2.valueChanges.subscribe((value) => { throw 'Should not happen'; }); - c3.valueChanges.subscribe((value) => { throw 'Should not happen'; }); - - a.reset([], {emitEvent: false}); - tick(); - })); - - it('should emit one statusChange event per reset control', () => { - form.statusChanges.subscribe(() => logger.push('form')); - a.statusChanges.subscribe(() => logger.push('array')); - c.statusChanges.subscribe(() => logger.push('control1')); - c2.statusChanges.subscribe(() => logger.push('control2')); - c3.statusChanges.subscribe(() => logger.push('control3')); - - a.reset(); - expect(logger).toEqual(['control1', 'control2', 'array', 'form']); - }); - - it('should mark as pristine and not dirty before emitting valueChange and statusChange events when resetting', - () => { - const pristineAndNotDirty = () => { - expect(a.pristine).toBe(true); - expect(a.dirty).toBe(false); - }; - - c2.markAsDirty(); - expect(a.pristine).toBe(false); - expect(a.dirty).toBe(true); - - a.valueChanges.subscribe(pristineAndNotDirty); - a.statusChanges.subscribe(pristineAndNotDirty); - - a.reset(); - }); - }); - }); - - describe('errors', () => { - it('should run the validator when the value changes', () => { - const simpleValidator = (c: FormArray) => - c.controls[0].value != 'correct' ? {'broken': true} : null; - - const c = new FormControl(null); - const g = new FormArray([c], simpleValidator as ValidatorFn); - - c.setValue('correct'); - - expect(g.valid).toEqual(true); - expect(g.errors).toEqual(null); - - c.setValue('incorrect'); - - expect(g.valid).toEqual(false); - expect(g.errors).toEqual({'broken': true}); - }); - }); - - - describe('dirty', () => { - let c: FormControl; - let a: FormArray; - - beforeEach(() => { - c = new FormControl('value'); - a = new FormArray([c]); - }); - - it('should be false after creating a control', () => { expect(a.dirty).toEqual(false); }); - - it('should be true after changing the value of the control', () => { - c.markAsDirty(); - - expect(a.dirty).toEqual(true); - }); - }); - - describe('touched', () => { - let c: FormControl; - let a: FormArray; - - beforeEach(() => { - c = new FormControl('value'); - a = new FormArray([c]); - }); - - it('should be false after creating a control', () => { expect(a.touched).toEqual(false); }); - - it('should be true after child control is marked as touched', () => { - c.markAsTouched(); - - expect(a.touched).toEqual(true); - }); - }); - - - describe('pending', () => { - let c: FormControl; - let a: FormArray; - - beforeEach(() => { - c = new FormControl('value'); - a = new FormArray([c]); - }); - - it('should be false after creating a control', () => { - expect(c.pending).toEqual(false); - expect(a.pending).toEqual(false); - }); - - it('should be true after changing the value of the control', () => { - c.markAsPending(); - - expect(c.pending).toEqual(true); - expect(a.pending).toEqual(true); - }); - - it('should not update the parent when onlySelf = true', () => { - c.markAsPending({onlySelf: true}); - - expect(c.pending).toEqual(true); - expect(a.pending).toEqual(false); - }); - - describe('status change events', () => { - let logger: string[]; - - beforeEach(() => { - logger = []; - a.statusChanges.subscribe((status) => logger.push(status)); - }); - - it('should emit event after marking control as pending', () => { - c.markAsPending(); - expect(logger).toEqual(['PENDING']); - }); - - it('should not emit event from parent when onlySelf is true', () => { - c.markAsPending({onlySelf: true}); - expect(logger).toEqual([]); - }); - - it('should not emit event when emitEvent = false', () => { - c.markAsPending({emitEvent: false}); - expect(logger).toEqual([]); - }); - - it('should emit event when parent is markedAsPending', () => { - a.markAsPending(); - expect(logger).toEqual(['PENDING']); - }); - }); - - }); - - describe('valueChanges', () => { - let a: FormArray; - let c1: any /** TODO #9100 */, c2: any /** TODO #9100 */; - - beforeEach(() => { - c1 = new FormControl('old1'); - c2 = new FormControl('old2'); - a = new FormArray([c1, c2]); - }); - - it('should fire an event after the value has been updated', - inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { - a.valueChanges.subscribe({ - next: (value: any) => { - expect(a.value).toEqual(['new1', 'old2']); - expect(value).toEqual(['new1', 'old2']); - async.done(); - } - }); - c1.setValue('new1'); - })); - - it('should fire an event after the control\'s observable fired an event', - inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { - let controlCallbackIsCalled = false; - - - c1.valueChanges.subscribe({next: (value: any) => { controlCallbackIsCalled = true; }}); - - a.valueChanges.subscribe({ - next: (value: any) => { - expect(controlCallbackIsCalled).toBe(true); - async.done(); - } - }); - - c1.setValue('new1'); - })); - - it('should fire an event when a control is removed', - inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { - a.valueChanges.subscribe({ - next: (value: any) => { - expect(value).toEqual(['old1']); - async.done(); - } - }); - - a.removeAt(1); - })); - - it('should fire an event when a control is added', - inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { - a.removeAt(1); - - a.valueChanges.subscribe({ - next: (value: any) => { - expect(value).toEqual(['old1', 'old2']); - async.done(); - } - }); - - a.push(c2); - })); - }); - - describe('get', () => { - it('should return null when path is null', () => { - const g = new FormGroup({}); - expect(g.get(null !)).toEqual(null); - }); - - it('should return null when path is empty', () => { - const g = new FormGroup({}); - expect(g.get([])).toEqual(null); - }); - - it('should return null when path is invalid', () => { - const g = new FormGroup({}); - expect(g.get('invalid')).toEqual(null); - }); - - it('should return a child of a control group', () => { - const g = new FormGroup({ - 'one': new FormControl('111'), - 'nested': new FormGroup({'two': new FormControl('222')}) - }); - - expect(g.get(['one']) !.value).toEqual('111'); - expect(g.get('one') !.value).toEqual('111'); - expect(g.get(['nested', 'two']) !.value).toEqual('222'); - expect(g.get('nested.two') !.value).toEqual('222'); - }); - - it('should return an element of an array', () => { - const g = new FormGroup({'array': new FormArray([new FormControl('111')])}); - - expect(g.get(['array', 0]) !.value).toEqual('111'); - }); - }); - - describe('validator', () => { - function simpleValidator(c: AbstractControl): ValidationErrors|null { - return c.get([0]) !.value === 'correct' ? null : {'broken': true}; - } - - function arrayRequiredValidator(c: AbstractControl): ValidationErrors|null { - return Validators.required(c.get([0]) as AbstractControl); - } - - it('should set a single validator', () => { - const a = new FormArray([new FormControl()], simpleValidator); - expect(a.valid).toBe(false); - expect(a.errors).toEqual({'broken': true}); - - a.setValue(['correct']); - expect(a.valid).toBe(true); - }); - - it('should set a single validator from options obj', () => { - const a = new FormArray([new FormControl()], {validators: simpleValidator}); - expect(a.valid).toBe(false); - expect(a.errors).toEqual({'broken': true}); - - a.setValue(['correct']); - expect(a.valid).toBe(true); - }); - - it('should set multiple validators from an array', () => { - const a = new FormArray([new FormControl()], [simpleValidator, arrayRequiredValidator]); - expect(a.valid).toBe(false); - expect(a.errors).toEqual({'required': true, 'broken': true}); - - a.setValue(['c']); - expect(a.valid).toBe(false); - expect(a.errors).toEqual({'broken': true}); - - a.setValue(['correct']); - expect(a.valid).toBe(true); - }); - - it('should set multiple validators from options obj', () => { - const a = new FormArray( - [new FormControl()], {validators: [simpleValidator, arrayRequiredValidator]}); - expect(a.valid).toBe(false); - expect(a.errors).toEqual({'required': true, 'broken': true}); - - a.setValue(['c']); - expect(a.valid).toBe(false); - expect(a.errors).toEqual({'broken': true}); - - a.setValue(['correct']); - expect(a.valid).toBe(true); - }); - }); - - describe('asyncValidator', () => { - function otherObservableValidator() { return of ({'other': true}); } - - it('should run the async validator', fakeAsync(() => { - const c = new FormControl('value'); - const g = new FormArray([c], null !, asyncValidator('expected')); - - expect(g.pending).toEqual(true); - - tick(); - - expect(g.errors).toEqual({'async': true}); - expect(g.pending).toEqual(false); - })); - - it('should set a single async validator from options obj', fakeAsync(() => { - const g = new FormArray( - [new FormControl('value')], {asyncValidators: asyncValidator('expected')}); - - expect(g.pending).toEqual(true); - - tick(); - - expect(g.errors).toEqual({'async': true}); - expect(g.pending).toEqual(false); - })); - - it('should set multiple async validators from an array', fakeAsync(() => { - const g = new FormArray( - [new FormControl('value')], null !, - [asyncValidator('expected'), otherObservableValidator]); - - expect(g.pending).toEqual(true); - - tick(); - - expect(g.errors).toEqual({'async': true, 'other': true}); - expect(g.pending).toEqual(false); - })); - - it('should set multiple async validators from options obj', fakeAsync(() => { - const g = new FormArray( - [new FormControl('value')], - {asyncValidators: [asyncValidator('expected'), otherObservableValidator]}); - - expect(g.pending).toEqual(true); - - tick(); - - expect(g.errors).toEqual({'async': true, 'other': true}); - expect(g.pending).toEqual(false); - })); - }); - - describe('disable() & enable()', () => { - let a: FormArray; - let c: FormControl; - let c2: FormControl; - - beforeEach(() => { - c = new FormControl(null); - c2 = new FormControl(null); - a = new FormArray([c, c2]); - }); - - it('should mark the array as disabled', () => { - expect(a.disabled).toBe(false); - expect(a.valid).toBe(true); - - a.disable(); - expect(a.disabled).toBe(true); - expect(a.valid).toBe(false); - - a.enable(); - expect(a.disabled).toBe(false); - expect(a.valid).toBe(true); - }); - - it('should set the array status as disabled', () => { - expect(a.status).toBe('VALID'); - - a.disable(); - expect(a.status).toBe('DISABLED'); - - a.enable(); - expect(a.status).toBe('VALID'); - }); - - it('should mark children of the array as disabled', () => { - expect(c.disabled).toBe(false); - expect(c2.disabled).toBe(false); - - a.disable(); - expect(c.disabled).toBe(true); - expect(c2.disabled).toBe(true); - - a.enable(); - expect(c.disabled).toBe(false); - expect(c2.disabled).toBe(false); - }); - - it('should ignore disabled controls in validation', () => { - const g = new FormGroup({ - nested: new FormArray([new FormControl(null, Validators.required)]), - two: new FormControl('two') - }); - expect(g.valid).toBe(false); - - g.get('nested') !.disable(); - expect(g.valid).toBe(true); - - g.get('nested') !.enable(); - expect(g.valid).toBe(false); - }); - - it('should ignore disabled controls when serializing value', () => { - const g = new FormGroup( - {nested: new FormArray([new FormControl('one')]), two: new FormControl('two')}); - expect(g.value).toEqual({'nested': ['one'], 'two': 'two'}); - - g.get('nested') !.disable(); - expect(g.value).toEqual({'two': 'two'}); - - g.get('nested') !.enable(); - expect(g.value).toEqual({'nested': ['one'], 'two': 'two'}); - }); - - it('should ignore disabled controls when determining dirtiness', () => { - const g = new FormGroup({nested: a, two: new FormControl('two')}); - g.get(['nested', 0]) !.markAsDirty(); - expect(g.dirty).toBe(true); - - g.get('nested') !.disable(); - expect(g.get('nested') !.dirty).toBe(true); - expect(g.dirty).toEqual(false); - - g.get('nested') !.enable(); - expect(g.dirty).toEqual(true); - }); - - it('should ignore disabled controls when determining touched state', () => { - const g = new FormGroup({nested: a, two: new FormControl('two')}); - g.get(['nested', 0]) !.markAsTouched(); - expect(g.touched).toBe(true); - - g.get('nested') !.disable(); - expect(g.get('nested') !.touched).toBe(true); - expect(g.touched).toEqual(false); - - g.get('nested') !.enable(); - expect(g.touched).toEqual(true); - }); - - it('should keep empty, disabled arrays disabled when updating validity', () => { - const arr = new FormArray([]); - expect(arr.status).toEqual('VALID'); - - arr.disable(); - expect(arr.status).toEqual('DISABLED'); - - arr.updateValueAndValidity(); - expect(arr.status).toEqual('DISABLED'); - - arr.push(new FormControl({value: '', disabled: true})); - expect(arr.status).toEqual('DISABLED'); - - arr.push(new FormControl()); - expect(arr.status).toEqual('VALID'); - }); - - it('should re-enable empty, disabled arrays', () => { - const arr = new FormArray([]); - arr.disable(); - expect(arr.status).toEqual('DISABLED'); - - arr.enable(); - expect(arr.status).toEqual('VALID'); - }); - - it('should not run validators on disabled controls', () => { - const validator = jasmine.createSpy('validator'); - const arr = new FormArray([new FormControl()], validator); - expect(validator.calls.count()).toEqual(1); - - arr.disable(); - expect(validator.calls.count()).toEqual(1); - - arr.setValue(['value']); - expect(validator.calls.count()).toEqual(1); - - arr.enable(); - expect(validator.calls.count()).toEqual(2); - }); - - describe('disabled errors', () => { - it('should clear out array errors when disabled', () => { - const arr = new FormArray([new FormControl()], () => ({'expected': true})); - expect(arr.errors).toEqual({'expected': true}); - - arr.disable(); - expect(arr.errors).toEqual(null); - - arr.enable(); - expect(arr.errors).toEqual({'expected': true}); - }); - - it('should re-populate array errors when enabled from a child', () => { - const arr = new FormArray([new FormControl()], () => ({'expected': true})); - arr.disable(); - expect(arr.errors).toEqual(null); - - arr.push(new FormControl()); - expect(arr.errors).toEqual({'expected': true}); - }); - - it('should clear out async array errors when disabled', fakeAsync(() => { - const arr = new FormArray([new FormControl()], null !, asyncValidator('expected')); - tick(); - expect(arr.errors).toEqual({'async': true}); - - arr.disable(); - expect(arr.errors).toEqual(null); - - arr.enable(); - tick(); - expect(arr.errors).toEqual({'async': true}); - })); - - it('should re-populate async array errors when enabled from a child', fakeAsync(() => { - const arr = new FormArray([new FormControl()], null !, asyncValidator('expected')); - tick(); - expect(arr.errors).toEqual({'async': true}); - - arr.disable(); - expect(arr.errors).toEqual(null); - - arr.push(new FormControl()); - tick(); - expect(arr.errors).toEqual({'async': true}); - })); - }); - - describe('disabled events', () => { - let logger: string[]; - let c: FormControl; - let a: FormArray; - let form: FormGroup; - - beforeEach(() => { - logger = []; - c = new FormControl('', Validators.required); - a = new FormArray([c]); - form = new FormGroup({a: a}); - }); - - it('should emit value change events in the right order', () => { - c.valueChanges.subscribe(() => logger.push('control')); - a.valueChanges.subscribe(() => logger.push('array')); - form.valueChanges.subscribe(() => logger.push('form')); - - a.disable(); - expect(logger).toEqual(['control', 'array', 'form']); - }); - - it('should emit status change events in the right order', () => { - c.statusChanges.subscribe(() => logger.push('control')); - a.statusChanges.subscribe(() => logger.push('array')); - form.statusChanges.subscribe(() => logger.push('form')); - - a.disable(); - expect(logger).toEqual(['control', 'array', 'form']); - }); - - it('should not emit value change events when emitEvent = false', () => { - c.valueChanges.subscribe(() => logger.push('control')); - a.valueChanges.subscribe(() => logger.push('array')); - form.valueChanges.subscribe(() => logger.push('form')); - - a.disable({emitEvent: false}); - expect(logger).toEqual([]); - a.enable({emitEvent: false}); - expect(logger).toEqual([]); - }); - - it('should not emit status change events when emitEvent = false', () => { - c.statusChanges.subscribe(() => logger.push('control')); - a.statusChanges.subscribe(() => logger.push('array')); - form.statusChanges.subscribe(() => logger.push('form')); - - a.disable({emitEvent: false}); - expect(logger).toEqual([]); - a.enable({emitEvent: false}); - expect(logger).toEqual([]); - }); - - }); - - describe('setControl()', () => { - let c: FormControl; - let a: FormArray; - - beforeEach(() => { - c = new FormControl('one'); - a = new FormArray([c]); - }); - - it('should replace existing control with new control', () => { - const c2 = new FormControl('new!', Validators.minLength(10)); - a.setControl(0, c2); - - expect(a.controls[0]).toEqual(c2); - expect(a.value).toEqual(['new!']); - expect(a.valid).toBe(false); - }); - - it('should add control if control did not exist before', () => { - const c2 = new FormControl('new!', Validators.minLength(10)); - a.setControl(1, c2); - - expect(a.controls[1]).toEqual(c2); - expect(a.value).toEqual(['one', 'new!']); - expect(a.valid).toBe(false); - }); - - it('should remove control if new control is null', () => { - a.setControl(0, null !); - expect(a.controls[0]).not.toBeDefined(); - expect(a.value).toEqual([]); - }); - - it('should only emit value change event once', () => { - const logger: string[] = []; - const c2 = new FormControl('new!'); - a.valueChanges.subscribe(() => logger.push('change!')); - a.setControl(0, c2); - expect(logger).toEqual(['change!']); - }); - - }); + a.insert(1, c2); + expect(a.controls).toEqual([c1, c2, c3]); }); }); + + describe('value', () => { + it('should be the reduced value of the child controls', () => { + const a = new FormArray([new FormControl(1), new FormControl(2)]); + expect(a.value).toEqual([1, 2]); + }); + + it('should be an empty array when there are no child controls', () => { + const a = new FormArray([]); + expect(a.value).toEqual([]); + }); + }); + + describe('getRawValue()', () => { + let a: FormArray; + + it('should work with nested form groups/arrays', () => { + a = new FormArray([ + new FormGroup({'c2': new FormControl('v2'), 'c3': new FormControl('v3')}), + new FormArray([new FormControl('v4'), new FormControl('v5')]) + ]); + a.at(0).get('c3')!.disable(); + (a.at(1) as FormArray).at(1).disable(); + + expect(a.getRawValue()).toEqual([{'c2': 'v2', 'c3': 'v3'}, ['v4', 'v5']]); + }); + }); + + describe('markAllAsTouched', () => { + it('should mark all descendants as touched', () => { + const formArray: FormArray = new FormArray([ + new FormControl('v1'), new FormControl('v2'), new FormGroup({'c1': new FormControl('v1')}), + new FormArray([new FormGroup({'c2': new FormControl('v2')})]) + ]); + + expect(formArray.touched).toBe(false); + + const control1 = formArray.at(0) as FormControl; + + expect(control1.touched).toBe(false); + + const group1 = formArray.at(2) as FormGroup; + + expect(group1.touched).toBe(false); + + const group1Control1 = group1.get('c1') as FormControl; + + expect(group1Control1.touched).toBe(false); + + const innerFormArray = formArray.at(3) as FormArray; + + expect(innerFormArray.touched).toBe(false); + + const innerFormArrayGroup = innerFormArray.at(0) as FormGroup; + + expect(innerFormArrayGroup.touched).toBe(false); + + const innerFormArrayGroupControl1 = innerFormArrayGroup.get('c2') as FormControl; + + expect(innerFormArrayGroupControl1.touched).toBe(false); + + formArray.markAllAsTouched(); + + expect(formArray.touched).toBe(true); + + expect(control1.touched).toBe(true); + + expect(group1.touched).toBe(true); + + expect(group1Control1.touched).toBe(true); + + expect(innerFormArray.touched).toBe(true); + + expect(innerFormArrayGroup.touched).toBe(true); + + expect(innerFormArrayGroupControl1.touched).toBe(true); + }); + }); + + describe('setValue', () => { + let c: FormControl, c2: FormControl, a: FormArray; + + beforeEach(() => { + c = new FormControl(''); + c2 = new FormControl(''); + a = new FormArray([c, c2]); + }); + + it('should set its own value', () => { + a.setValue(['one', 'two']); + expect(a.value).toEqual(['one', 'two']); + }); + + it('should set child values', () => { + a.setValue(['one', 'two']); + expect(c.value).toEqual('one'); + expect(c2.value).toEqual('two'); + }); + + it('should set values for disabled child controls', () => { + c2.disable(); + a.setValue(['one', 'two']); + expect(c2.value).toEqual('two'); + expect(a.value).toEqual(['one']); + expect(a.getRawValue()).toEqual(['one', 'two']); + }); + + it('should set value for disabled arrays', () => { + a.disable(); + a.setValue(['one', 'two']); + expect(c.value).toEqual('one'); + expect(c2.value).toEqual('two'); + expect(a.value).toEqual(['one', 'two']); + }); + + it('should set parent values', () => { + const form = new FormGroup({'parent': a}); + a.setValue(['one', 'two']); + expect(form.value).toEqual({'parent': ['one', 'two']}); + }); + + it('should not update the parent explicitly specified', () => { + const form = new FormGroup({'parent': a}); + a.setValue(['one', 'two'], {onlySelf: true}); + + expect(form.value).toEqual({parent: ['', '']}); + }); + + it('should throw if fields are missing from supplied value (subset)', () => { + expect(() => a.setValue([, 'two'])) + .toThrowError(new RegExp(`Must supply a value for form control at index: 0`)); + }); + + it('should throw if a value is provided for a missing control (superset)', () => { + expect(() => a.setValue([ + 'one', 'two', 'three' + ])).toThrowError(new RegExp(`Cannot find form control at index 2`)); + }); + + it('should throw if a value is not provided for a disabled control', () => { + c2.disable(); + expect(() => a.setValue(['one'])) + .toThrowError(new RegExp(`Must supply a value for form control at index: 1`)); + }); + + it('should throw if no controls are set yet', () => { + const empty = new FormArray([]); + expect(() => empty.setValue(['one'])) + .toThrowError(new RegExp(`no form controls registered with this array`)); + }); + + describe('setValue() events', () => { + let form: FormGroup; + let logger: any[]; + + beforeEach(() => { + form = new FormGroup({'parent': a}); + logger = []; + }); + + it('should emit one valueChange event per control', () => { + form.valueChanges.subscribe(() => logger.push('form')); + a.valueChanges.subscribe(() => logger.push('array')); + c.valueChanges.subscribe(() => logger.push('control1')); + c2.valueChanges.subscribe(() => logger.push('control2')); + + a.setValue(['one', 'two']); + expect(logger).toEqual(['control1', 'control2', 'array', 'form']); + }); + + it('should not fire an event when explicitly specified', fakeAsync(() => { + form.valueChanges.subscribe((value) => { + throw 'Should not happen'; + }); + a.valueChanges.subscribe((value) => { + throw 'Should not happen'; + }); + c.valueChanges.subscribe((value) => { + throw 'Should not happen'; + }); + c2.valueChanges.subscribe((value) => { + throw 'Should not happen'; + }); + + a.setValue(['one', 'two'], {emitEvent: false}); + tick(); + })); + + it('should emit one statusChange event per control', () => { + form.statusChanges.subscribe(() => logger.push('form')); + a.statusChanges.subscribe(() => logger.push('array')); + c.statusChanges.subscribe(() => logger.push('control1')); + c2.statusChanges.subscribe(() => logger.push('control2')); + + a.setValue(['one', 'two']); + expect(logger).toEqual(['control1', 'control2', 'array', 'form']); + }); + }); + }); + + describe('patchValue', () => { + let c: FormControl, c2: FormControl, a: FormArray; + + beforeEach(() => { + c = new FormControl(''); + c2 = new FormControl(''); + a = new FormArray([c, c2]); + }); + + it('should set its own value', () => { + a.patchValue(['one', 'two']); + expect(a.value).toEqual(['one', 'two']); + }); + + it('should set child values', () => { + a.patchValue(['one', 'two']); + expect(c.value).toEqual('one'); + expect(c2.value).toEqual('two'); + }); + + it('should patch disabled control values', () => { + c2.disable(); + a.patchValue(['one', 'two']); + expect(c2.value).toEqual('two'); + expect(a.value).toEqual(['one']); + expect(a.getRawValue()).toEqual(['one', 'two']); + }); + + it('should patch disabled control arrays', () => { + a.disable(); + a.patchValue(['one', 'two']); + expect(c.value).toEqual('one'); + expect(c2.value).toEqual('two'); + expect(a.value).toEqual(['one', 'two']); + }); + + it('should set parent values', () => { + const form = new FormGroup({'parent': a}); + a.patchValue(['one', 'two']); + expect(form.value).toEqual({'parent': ['one', 'two']}); + }); + + it('should not update the parent explicitly specified', () => { + const form = new FormGroup({'parent': a}); + a.patchValue(['one', 'two'], {onlySelf: true}); + + expect(form.value).toEqual({parent: ['', '']}); + }); + + it('should ignore fields that are missing from supplied value (subset)', () => { + a.patchValue([, 'two']); + expect(a.value).toEqual(['', 'two']); + }); + + it('should not ignore fields that are null', () => { + a.patchValue([null]); + expect(a.value).toEqual([null, '']); + }); + + it('should ignore any value provided for a missing control (superset)', () => { + a.patchValue([, , 'three']); + expect(a.value).toEqual(['', '']); + }); + + describe('patchValue() events', () => { + let form: FormGroup; + let logger: any[]; + + beforeEach(() => { + form = new FormGroup({'parent': a}); + logger = []; + }); + + it('should emit one valueChange event per control', () => { + form.valueChanges.subscribe(() => logger.push('form')); + a.valueChanges.subscribe(() => logger.push('array')); + c.valueChanges.subscribe(() => logger.push('control1')); + c2.valueChanges.subscribe(() => logger.push('control2')); + + a.patchValue(['one', 'two']); + expect(logger).toEqual(['control1', 'control2', 'array', 'form']); + }); + + it('should not emit valueChange events for skipped controls', () => { + form.valueChanges.subscribe(() => logger.push('form')); + a.valueChanges.subscribe(() => logger.push('array')); + c.valueChanges.subscribe(() => logger.push('control1')); + c2.valueChanges.subscribe(() => logger.push('control2')); + + a.patchValue(['one']); + expect(logger).toEqual(['control1', 'array', 'form']); + }); + + it('should not fire an event when explicitly specified', fakeAsync(() => { + form.valueChanges.subscribe((value) => { + throw 'Should not happen'; + }); + a.valueChanges.subscribe((value) => { + throw 'Should not happen'; + }); + c.valueChanges.subscribe((value) => { + throw 'Should not happen'; + }); + c2.valueChanges.subscribe((value) => { + throw 'Should not happen'; + }); + + a.patchValue(['one', 'two'], {emitEvent: false}); + tick(); + })); + + it('should emit one statusChange event per control', () => { + form.statusChanges.subscribe(() => logger.push('form')); + a.statusChanges.subscribe(() => logger.push('array')); + c.statusChanges.subscribe(() => logger.push('control1')); + c2.statusChanges.subscribe(() => logger.push('control2')); + + a.patchValue(['one', 'two']); + expect(logger).toEqual(['control1', 'control2', 'array', 'form']); + }); + }); + }); + + describe('reset()', () => { + let c: FormControl, c2: FormControl, a: FormArray; + + beforeEach(() => { + c = new FormControl('initial value'); + c2 = new FormControl(''); + a = new FormArray([c, c2]); + }); + + it('should set its own value if value passed', () => { + a.setValue(['new value', 'new value']); + + a.reset(['initial value', '']); + expect(a.value).toEqual(['initial value', '']); + }); + + it('should not update the parent when explicitly specified', () => { + const form = new FormGroup({'a': a}); + a.reset(['one', 'two'], {onlySelf: true}); + + expect(form.value).toEqual({a: ['initial value', '']}); + }); + + it('should set its own value if boxed value passed', () => { + a.setValue(['new value', 'new value']); + + a.reset([{value: 'initial value', disabled: false}, '']); + expect(a.value).toEqual(['initial value', '']); + }); + + it('should clear its own value if no value passed', () => { + a.setValue(['new value', 'new value']); + + a.reset(); + expect(a.value).toEqual([null, null]); + }); + + it('should set the value of each of its child controls if value passed', () => { + a.setValue(['new value', 'new value']); + + a.reset(['initial value', '']); + expect(c.value).toBe('initial value'); + expect(c2.value).toBe(''); + }); + + it('should clear the value of each of its child controls if no value', () => { + a.setValue(['new value', 'new value']); + + a.reset(); + expect(c.value).toBe(null); + expect(c2.value).toBe(null); + }); + + it('should set the value of its parent if value passed', () => { + const form = new FormGroup({'a': a}); + a.setValue(['new value', 'new value']); + + a.reset(['initial value', '']); + expect(form.value).toEqual({'a': ['initial value', '']}); + }); + + it('should clear the value of its parent if no value passed', () => { + const form = new FormGroup({'a': a}); + a.setValue(['new value', 'new value']); + + a.reset(); + expect(form.value).toEqual({'a': [null, null]}); + }); + + it('should mark itself as pristine', () => { + a.markAsDirty(); + expect(a.pristine).toBe(false); + + a.reset(); + expect(a.pristine).toBe(true); + }); + + it('should mark all child controls as pristine', () => { + c.markAsDirty(); + c2.markAsDirty(); + expect(c.pristine).toBe(false); + expect(c2.pristine).toBe(false); + + a.reset(); + expect(c.pristine).toBe(true); + expect(c2.pristine).toBe(true); + }); + + it('should mark the parent as pristine if all siblings pristine', () => { + const c3 = new FormControl(''); + const form = new FormGroup({'a': a, 'c3': c3}); + + a.markAsDirty(); + expect(form.pristine).toBe(false); + + a.reset(); + expect(form.pristine).toBe(true); + }); + + it('should not mark the parent pristine if any dirty siblings', () => { + const c3 = new FormControl(''); + const form = new FormGroup({'a': a, 'c3': c3}); + + a.markAsDirty(); + c3.markAsDirty(); + expect(form.pristine).toBe(false); + + a.reset(); + expect(form.pristine).toBe(false); + }); + + it('should mark itself as untouched', () => { + a.markAsTouched(); + expect(a.untouched).toBe(false); + + a.reset(); + expect(a.untouched).toBe(true); + }); + + it('should mark all child controls as untouched', () => { + c.markAsTouched(); + c2.markAsTouched(); + expect(c.untouched).toBe(false); + expect(c2.untouched).toBe(false); + + a.reset(); + expect(c.untouched).toBe(true); + expect(c2.untouched).toBe(true); + }); + + it('should mark the parent untouched if all siblings untouched', () => { + const c3 = new FormControl(''); + const form = new FormGroup({'a': a, 'c3': c3}); + + a.markAsTouched(); + expect(form.untouched).toBe(false); + + a.reset(); + expect(form.untouched).toBe(true); + }); + + it('should not mark the parent untouched if any touched siblings', () => { + const c3 = new FormControl(''); + const form = new FormGroup({'a': a, 'c3': c3}); + + a.markAsTouched(); + c3.markAsTouched(); + expect(form.untouched).toBe(false); + + a.reset(); + expect(form.untouched).toBe(false); + }); + + it('should retain previous disabled state', () => { + a.disable(); + a.reset(); + + expect(a.disabled).toBe(true); + }); + + it('should set child disabled state if boxed value passed', () => { + a.disable(); + a.reset([{value: '', disabled: false}, '']); + + expect(c.disabled).toBe(false); + expect(a.disabled).toBe(false); + }); + + + describe('reset() events', () => { + let form: FormGroup, c3: FormControl, logger: any[]; + + beforeEach(() => { + c3 = new FormControl(''); + form = new FormGroup({'a': a, 'c3': c3}); + logger = []; + }); + + it('should emit one valueChange event per reset control', () => { + form.valueChanges.subscribe(() => logger.push('form')); + a.valueChanges.subscribe(() => logger.push('array')); + c.valueChanges.subscribe(() => logger.push('control1')); + c2.valueChanges.subscribe(() => logger.push('control2')); + c3.valueChanges.subscribe(() => logger.push('control3')); + + a.reset(); + expect(logger).toEqual(['control1', 'control2', 'array', 'form']); + }); + + it('should not fire an event when explicitly specified', fakeAsync(() => { + form.valueChanges.subscribe((value) => { + throw 'Should not happen'; + }); + a.valueChanges.subscribe((value) => { + throw 'Should not happen'; + }); + c.valueChanges.subscribe((value) => { + throw 'Should not happen'; + }); + c2.valueChanges.subscribe((value) => { + throw 'Should not happen'; + }); + c3.valueChanges.subscribe((value) => { + throw 'Should not happen'; + }); + + a.reset([], {emitEvent: false}); + tick(); + })); + + it('should emit one statusChange event per reset control', () => { + form.statusChanges.subscribe(() => logger.push('form')); + a.statusChanges.subscribe(() => logger.push('array')); + c.statusChanges.subscribe(() => logger.push('control1')); + c2.statusChanges.subscribe(() => logger.push('control2')); + c3.statusChanges.subscribe(() => logger.push('control3')); + + a.reset(); + expect(logger).toEqual(['control1', 'control2', 'array', 'form']); + }); + + it('should mark as pristine and not dirty before emitting valueChange and statusChange events when resetting', + () => { + const pristineAndNotDirty = () => { + expect(a.pristine).toBe(true); + expect(a.dirty).toBe(false); + }; + + c2.markAsDirty(); + expect(a.pristine).toBe(false); + expect(a.dirty).toBe(true); + + a.valueChanges.subscribe(pristineAndNotDirty); + a.statusChanges.subscribe(pristineAndNotDirty); + + a.reset(); + }); + }); + }); + + describe('errors', () => { + it('should run the validator when the value changes', () => { + const simpleValidator = (c: FormArray) => + c.controls[0].value != 'correct' ? {'broken': true} : null; + + const c = new FormControl(null); + const g = new FormArray([c], simpleValidator as ValidatorFn); + + c.setValue('correct'); + + expect(g.valid).toEqual(true); + expect(g.errors).toEqual(null); + + c.setValue('incorrect'); + + expect(g.valid).toEqual(false); + expect(g.errors).toEqual({'broken': true}); + }); + }); + + + describe('dirty', () => { + let c: FormControl; + let a: FormArray; + + beforeEach(() => { + c = new FormControl('value'); + a = new FormArray([c]); + }); + + it('should be false after creating a control', () => { + expect(a.dirty).toEqual(false); + }); + + it('should be true after changing the value of the control', () => { + c.markAsDirty(); + + expect(a.dirty).toEqual(true); + }); + }); + + describe('touched', () => { + let c: FormControl; + let a: FormArray; + + beforeEach(() => { + c = new FormControl('value'); + a = new FormArray([c]); + }); + + it('should be false after creating a control', () => { + expect(a.touched).toEqual(false); + }); + + it('should be true after child control is marked as touched', () => { + c.markAsTouched(); + + expect(a.touched).toEqual(true); + }); + }); + + + describe('pending', () => { + let c: FormControl; + let a: FormArray; + + beforeEach(() => { + c = new FormControl('value'); + a = new FormArray([c]); + }); + + it('should be false after creating a control', () => { + expect(c.pending).toEqual(false); + expect(a.pending).toEqual(false); + }); + + it('should be true after changing the value of the control', () => { + c.markAsPending(); + + expect(c.pending).toEqual(true); + expect(a.pending).toEqual(true); + }); + + it('should not update the parent when onlySelf = true', () => { + c.markAsPending({onlySelf: true}); + + expect(c.pending).toEqual(true); + expect(a.pending).toEqual(false); + }); + + describe('status change events', () => { + let logger: string[]; + + beforeEach(() => { + logger = []; + a.statusChanges.subscribe((status) => logger.push(status)); + }); + + it('should emit event after marking control as pending', () => { + c.markAsPending(); + expect(logger).toEqual(['PENDING']); + }); + + it('should not emit event from parent when onlySelf is true', () => { + c.markAsPending({onlySelf: true}); + expect(logger).toEqual([]); + }); + + it('should not emit event when emitEvent = false', () => { + c.markAsPending({emitEvent: false}); + expect(logger).toEqual([]); + }); + + it('should emit event when parent is markedAsPending', () => { + a.markAsPending(); + expect(logger).toEqual(['PENDING']); + }); + }); + }); + + describe('valueChanges', () => { + let a: FormArray; + let c1: any /** TODO #9100 */, c2: any /** TODO #9100 */; + + beforeEach(() => { + c1 = new FormControl('old1'); + c2 = new FormControl('old2'); + a = new FormArray([c1, c2]); + }); + + it('should fire an event after the value has been updated', + inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { + a.valueChanges.subscribe({ + next: (value: any) => { + expect(a.value).toEqual(['new1', 'old2']); + expect(value).toEqual(['new1', 'old2']); + async.done(); + } + }); + c1.setValue('new1'); + })); + + it('should fire an event after the control\'s observable fired an event', + inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { + let controlCallbackIsCalled = false; + + + c1.valueChanges.subscribe({ + next: (value: any) => { + controlCallbackIsCalled = true; + } + }); + + a.valueChanges.subscribe({ + next: (value: any) => { + expect(controlCallbackIsCalled).toBe(true); + async.done(); + } + }); + + c1.setValue('new1'); + })); + + it('should fire an event when a control is removed', + inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { + a.valueChanges.subscribe({ + next: (value: any) => { + expect(value).toEqual(['old1']); + async.done(); + } + }); + + a.removeAt(1); + })); + + it('should fire an event when a control is added', + inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { + a.removeAt(1); + + a.valueChanges.subscribe({ + next: (value: any) => { + expect(value).toEqual(['old1', 'old2']); + async.done(); + } + }); + + a.push(c2); + })); + }); + + describe('get', () => { + it('should return null when path is null', () => { + const g = new FormGroup({}); + expect(g.get(null!)).toEqual(null); + }); + + it('should return null when path is empty', () => { + const g = new FormGroup({}); + expect(g.get([])).toEqual(null); + }); + + it('should return null when path is invalid', () => { + const g = new FormGroup({}); + expect(g.get('invalid')).toEqual(null); + }); + + it('should return a child of a control group', () => { + const g = new FormGroup({ + 'one': new FormControl('111'), + 'nested': new FormGroup({'two': new FormControl('222')}) + }); + + expect(g.get(['one'])!.value).toEqual('111'); + expect(g.get('one')!.value).toEqual('111'); + expect(g.get(['nested', 'two'])!.value).toEqual('222'); + expect(g.get('nested.two')!.value).toEqual('222'); + }); + + it('should return an element of an array', () => { + const g = new FormGroup({'array': new FormArray([new FormControl('111')])}); + + expect(g.get(['array', 0])!.value).toEqual('111'); + }); + }); + + describe('validator', () => { + function simpleValidator(c: AbstractControl): ValidationErrors|null { + return c.get([0])!.value === 'correct' ? null : {'broken': true}; + } + + function arrayRequiredValidator(c: AbstractControl): ValidationErrors|null { + return Validators.required(c.get([0]) as AbstractControl); + } + + it('should set a single validator', () => { + const a = new FormArray([new FormControl()], simpleValidator); + expect(a.valid).toBe(false); + expect(a.errors).toEqual({'broken': true}); + + a.setValue(['correct']); + expect(a.valid).toBe(true); + }); + + it('should set a single validator from options obj', () => { + const a = new FormArray([new FormControl()], {validators: simpleValidator}); + expect(a.valid).toBe(false); + expect(a.errors).toEqual({'broken': true}); + + a.setValue(['correct']); + expect(a.valid).toBe(true); + }); + + it('should set multiple validators from an array', () => { + const a = new FormArray([new FormControl()], [simpleValidator, arrayRequiredValidator]); + expect(a.valid).toBe(false); + expect(a.errors).toEqual({'required': true, 'broken': true}); + + a.setValue(['c']); + expect(a.valid).toBe(false); + expect(a.errors).toEqual({'broken': true}); + + a.setValue(['correct']); + expect(a.valid).toBe(true); + }); + + it('should set multiple validators from options obj', () => { + const a = new FormArray( + [new FormControl()], {validators: [simpleValidator, arrayRequiredValidator]}); + expect(a.valid).toBe(false); + expect(a.errors).toEqual({'required': true, 'broken': true}); + + a.setValue(['c']); + expect(a.valid).toBe(false); + expect(a.errors).toEqual({'broken': true}); + + a.setValue(['correct']); + expect(a.valid).toBe(true); + }); + }); + + describe('asyncValidator', () => { + function otherObservableValidator() { + return of({'other': true}); + } + + it('should run the async validator', fakeAsync(() => { + const c = new FormControl('value'); + const g = new FormArray([c], null!, asyncValidator('expected')); + + expect(g.pending).toEqual(true); + + tick(); + + expect(g.errors).toEqual({'async': true}); + expect(g.pending).toEqual(false); + })); + + it('should set a single async validator from options obj', fakeAsync(() => { + const g = new FormArray( + [new FormControl('value')], {asyncValidators: asyncValidator('expected')}); + + expect(g.pending).toEqual(true); + + tick(); + + expect(g.errors).toEqual({'async': true}); + expect(g.pending).toEqual(false); + })); + + it('should set multiple async validators from an array', fakeAsync(() => { + const g = new FormArray( + [new FormControl('value')], null!, + [asyncValidator('expected'), otherObservableValidator]); + + expect(g.pending).toEqual(true); + + tick(); + + expect(g.errors).toEqual({'async': true, 'other': true}); + expect(g.pending).toEqual(false); + })); + + it('should set multiple async validators from options obj', fakeAsync(() => { + const g = new FormArray( + [new FormControl('value')], + {asyncValidators: [asyncValidator('expected'), otherObservableValidator]}); + + expect(g.pending).toEqual(true); + + tick(); + + expect(g.errors).toEqual({'async': true, 'other': true}); + expect(g.pending).toEqual(false); + })); + }); + + describe('disable() & enable()', () => { + let a: FormArray; + let c: FormControl; + let c2: FormControl; + + beforeEach(() => { + c = new FormControl(null); + c2 = new FormControl(null); + a = new FormArray([c, c2]); + }); + + it('should mark the array as disabled', () => { + expect(a.disabled).toBe(false); + expect(a.valid).toBe(true); + + a.disable(); + expect(a.disabled).toBe(true); + expect(a.valid).toBe(false); + + a.enable(); + expect(a.disabled).toBe(false); + expect(a.valid).toBe(true); + }); + + it('should set the array status as disabled', () => { + expect(a.status).toBe('VALID'); + + a.disable(); + expect(a.status).toBe('DISABLED'); + + a.enable(); + expect(a.status).toBe('VALID'); + }); + + it('should mark children of the array as disabled', () => { + expect(c.disabled).toBe(false); + expect(c2.disabled).toBe(false); + + a.disable(); + expect(c.disabled).toBe(true); + expect(c2.disabled).toBe(true); + + a.enable(); + expect(c.disabled).toBe(false); + expect(c2.disabled).toBe(false); + }); + + it('should ignore disabled controls in validation', () => { + const g = new FormGroup({ + nested: new FormArray([new FormControl(null, Validators.required)]), + two: new FormControl('two') + }); + expect(g.valid).toBe(false); + + g.get('nested')!.disable(); + expect(g.valid).toBe(true); + + g.get('nested')!.enable(); + expect(g.valid).toBe(false); + }); + + it('should ignore disabled controls when serializing value', () => { + const g = new FormGroup( + {nested: new FormArray([new FormControl('one')]), two: new FormControl('two')}); + expect(g.value).toEqual({'nested': ['one'], 'two': 'two'}); + + g.get('nested')!.disable(); + expect(g.value).toEqual({'two': 'two'}); + + g.get('nested')!.enable(); + expect(g.value).toEqual({'nested': ['one'], 'two': 'two'}); + }); + + it('should ignore disabled controls when determining dirtiness', () => { + const g = new FormGroup({nested: a, two: new FormControl('two')}); + g.get(['nested', 0])!.markAsDirty(); + expect(g.dirty).toBe(true); + + g.get('nested')!.disable(); + expect(g.get('nested')!.dirty).toBe(true); + expect(g.dirty).toEqual(false); + + g.get('nested')!.enable(); + expect(g.dirty).toEqual(true); + }); + + it('should ignore disabled controls when determining touched state', () => { + const g = new FormGroup({nested: a, two: new FormControl('two')}); + g.get(['nested', 0])!.markAsTouched(); + expect(g.touched).toBe(true); + + g.get('nested')!.disable(); + expect(g.get('nested')!.touched).toBe(true); + expect(g.touched).toEqual(false); + + g.get('nested')!.enable(); + expect(g.touched).toEqual(true); + }); + + it('should keep empty, disabled arrays disabled when updating validity', () => { + const arr = new FormArray([]); + expect(arr.status).toEqual('VALID'); + + arr.disable(); + expect(arr.status).toEqual('DISABLED'); + + arr.updateValueAndValidity(); + expect(arr.status).toEqual('DISABLED'); + + arr.push(new FormControl({value: '', disabled: true})); + expect(arr.status).toEqual('DISABLED'); + + arr.push(new FormControl()); + expect(arr.status).toEqual('VALID'); + }); + + it('should re-enable empty, disabled arrays', () => { + const arr = new FormArray([]); + arr.disable(); + expect(arr.status).toEqual('DISABLED'); + + arr.enable(); + expect(arr.status).toEqual('VALID'); + }); + + it('should not run validators on disabled controls', () => { + const validator = jasmine.createSpy('validator'); + const arr = new FormArray([new FormControl()], validator); + expect(validator.calls.count()).toEqual(1); + + arr.disable(); + expect(validator.calls.count()).toEqual(1); + + arr.setValue(['value']); + expect(validator.calls.count()).toEqual(1); + + arr.enable(); + expect(validator.calls.count()).toEqual(2); + }); + + describe('disabled errors', () => { + it('should clear out array errors when disabled', () => { + const arr = new FormArray([new FormControl()], () => ({'expected': true})); + expect(arr.errors).toEqual({'expected': true}); + + arr.disable(); + expect(arr.errors).toEqual(null); + + arr.enable(); + expect(arr.errors).toEqual({'expected': true}); + }); + + it('should re-populate array errors when enabled from a child', () => { + const arr = new FormArray([new FormControl()], () => ({'expected': true})); + arr.disable(); + expect(arr.errors).toEqual(null); + + arr.push(new FormControl()); + expect(arr.errors).toEqual({'expected': true}); + }); + + it('should clear out async array errors when disabled', fakeAsync(() => { + const arr = new FormArray([new FormControl()], null!, asyncValidator('expected')); + tick(); + expect(arr.errors).toEqual({'async': true}); + + arr.disable(); + expect(arr.errors).toEqual(null); + + arr.enable(); + tick(); + expect(arr.errors).toEqual({'async': true}); + })); + + it('should re-populate async array errors when enabled from a child', fakeAsync(() => { + const arr = new FormArray([new FormControl()], null!, asyncValidator('expected')); + tick(); + expect(arr.errors).toEqual({'async': true}); + + arr.disable(); + expect(arr.errors).toEqual(null); + + arr.push(new FormControl()); + tick(); + expect(arr.errors).toEqual({'async': true}); + })); + }); + + describe('disabled events', () => { + let logger: string[]; + let c: FormControl; + let a: FormArray; + let form: FormGroup; + + beforeEach(() => { + logger = []; + c = new FormControl('', Validators.required); + a = new FormArray([c]); + form = new FormGroup({a: a}); + }); + + it('should emit value change events in the right order', () => { + c.valueChanges.subscribe(() => logger.push('control')); + a.valueChanges.subscribe(() => logger.push('array')); + form.valueChanges.subscribe(() => logger.push('form')); + + a.disable(); + expect(logger).toEqual(['control', 'array', 'form']); + }); + + it('should emit status change events in the right order', () => { + c.statusChanges.subscribe(() => logger.push('control')); + a.statusChanges.subscribe(() => logger.push('array')); + form.statusChanges.subscribe(() => logger.push('form')); + + a.disable(); + expect(logger).toEqual(['control', 'array', 'form']); + }); + + it('should not emit value change events when emitEvent = false', () => { + c.valueChanges.subscribe(() => logger.push('control')); + a.valueChanges.subscribe(() => logger.push('array')); + form.valueChanges.subscribe(() => logger.push('form')); + + a.disable({emitEvent: false}); + expect(logger).toEqual([]); + a.enable({emitEvent: false}); + expect(logger).toEqual([]); + }); + + it('should not emit status change events when emitEvent = false', () => { + c.statusChanges.subscribe(() => logger.push('control')); + a.statusChanges.subscribe(() => logger.push('array')); + form.statusChanges.subscribe(() => logger.push('form')); + + a.disable({emitEvent: false}); + expect(logger).toEqual([]); + a.enable({emitEvent: false}); + expect(logger).toEqual([]); + }); + }); + + describe('setControl()', () => { + let c: FormControl; + let a: FormArray; + + beforeEach(() => { + c = new FormControl('one'); + a = new FormArray([c]); + }); + + it('should replace existing control with new control', () => { + const c2 = new FormControl('new!', Validators.minLength(10)); + a.setControl(0, c2); + + expect(a.controls[0]).toEqual(c2); + expect(a.value).toEqual(['new!']); + expect(a.valid).toBe(false); + }); + + it('should add control if control did not exist before', () => { + const c2 = new FormControl('new!', Validators.minLength(10)); + a.setControl(1, c2); + + expect(a.controls[1]).toEqual(c2); + expect(a.value).toEqual(['one', 'new!']); + expect(a.valid).toBe(false); + }); + + it('should remove control if new control is null', () => { + a.setControl(0, null!); + expect(a.controls[0]).not.toBeDefined(); + expect(a.value).toEqual([]); + }); + + it('should only emit value change event once', () => { + const logger: string[] = []; + const c2 = new FormControl('new!'); + a.valueChanges.subscribe(() => logger.push('change!')); + a.setControl(0, c2); + expect(logger).toEqual(['change!']); + }); + }); + }); +}); })(); diff --git a/packages/forms/test/form_builder_spec.ts b/packages/forms/test/form_builder_spec.ts index e04cd74677..05e2a735ce 100644 --- a/packages/forms/test/form_builder_spec.ts +++ b/packages/forms/test/form_builder_spec.ts @@ -8,195 +8,208 @@ import {fakeAsync, tick} from '@angular/core/testing'; import {beforeEach, describe, expect, it} from '@angular/core/testing/src/testing_internal'; import {FormBuilder, Validators} from '@angular/forms'; -import {of } from 'rxjs'; +import {of} from 'rxjs'; (function() { - function syncValidator(_: any /** TODO #9100 */): any /** TODO #9100 */ { return null; } - function asyncValidator(_: any /** TODO #9100 */) { return Promise.resolve(null); } +function syncValidator(_: any /** TODO #9100 */): any /** TODO #9100 */ { + return null; +} +function asyncValidator(_: any /** TODO #9100 */) { + return Promise.resolve(null); +} - describe('Form Builder', () => { - let b: FormBuilder; +describe('Form Builder', () => { + let b: FormBuilder; - beforeEach(() => { b = new FormBuilder(); }); + beforeEach(() => { + b = new FormBuilder(); + }); - it('should create controls from a value', () => { - const g = b.group({'login': 'some value'}); + it('should create controls from a value', () => { + const g = b.group({'login': 'some value'}); - expect(g.controls['login'].value).toEqual('some value'); + expect(g.controls['login'].value).toEqual('some value'); + }); + + it('should create controls from a boxed value', () => { + const g = b.group({'login': {value: 'some value', disabled: true}}); + + expect(g.controls['login'].value).toEqual('some value'); + expect(g.controls['login'].disabled).toEqual(true); + }); + + it('should create controls from an array', () => { + const g = b.group( + {'login': ['some value'], 'password': ['some value', syncValidator, asyncValidator]}); + + expect(g.controls['login'].value).toEqual('some value'); + expect(g.controls['password'].value).toEqual('some value'); + expect(g.controls['password'].validator).toEqual(syncValidator); + expect(g.controls['password'].asyncValidator).toEqual(asyncValidator); + }); + + it('should use controls whose form state is a primitive value', () => { + const g = b.group({'login': b.control('some value', syncValidator, asyncValidator)}); + + expect(g.controls['login'].value).toEqual('some value'); + expect(g.controls['login'].validator).toBe(syncValidator); + expect(g.controls['login'].asyncValidator).toBe(asyncValidator); + }); + + it('should support controls with no validators and whose form state is null', () => { + const g = b.group({'login': b.control(null)}); + expect(g.controls['login'].value).toBeNull(); + expect(g.controls['login'].validator).toBeNull(); + expect(g.controls['login'].asyncValidator).toBeNull(); + }); + + it('should support controls with validators and whose form state is null', () => { + const g = b.group({'login': b.control(null, syncValidator, asyncValidator)}); + expect(g.controls['login'].value).toBeNull(); + expect(g.controls['login'].validator).toBe(syncValidator); + expect(g.controls['login'].asyncValidator).toBe(asyncValidator); + }); + + it('should support controls with no validators and whose form state is undefined', () => { + const g = b.group({'login': b.control(undefined)}); + expect(g.controls['login'].value).toBeNull(); + expect(g.controls['login'].validator).toBeNull(); + expect(g.controls['login'].asyncValidator).toBeNull(); + }); + + it('should support controls with validators and whose form state is undefined', () => { + const g = b.group({'login': b.control(undefined, syncValidator, asyncValidator)}); + expect(g.controls['login'].value).toBeNull(); + expect(g.controls['login'].validator).toBe(syncValidator); + expect(g.controls['login'].asyncValidator).toBe(asyncValidator); + }); + + it('should create groups with a custom validator', () => { + const g = b.group( + {'login': 'some value'}, {'validator': syncValidator, 'asyncValidator': asyncValidator}); + + expect(g.validator).toBe(syncValidator); + expect(g.asyncValidator).toBe(asyncValidator); + }); + + it('should create control arrays', () => { + const c = b.control('three'); + const e = b.control(null); + const f = b.control(undefined); + const a = b.array( + ['one', ['two', syncValidator], c, b.array(['four']), e, f], syncValidator, asyncValidator); + + expect(a.value).toEqual(['one', 'two', 'three', ['four'], null, null]); + expect(a.validator).toBe(syncValidator); + expect(a.asyncValidator).toBe(asyncValidator); + }); + + it('should create control arrays with multiple async validators', fakeAsync(() => { + function asyncValidator1() { + return of({'async1': true}); + } + function asyncValidator2() { + return of({'async2': true}); + } + + const a = b.array(['one', 'two'], null, [asyncValidator1, asyncValidator2]); + expect(a.value).toEqual(['one', 'two']); + + tick(); + + expect(a.errors).toEqual({'async1': true, 'async2': true}); + })); + + it('should create control arrays with multiple sync validators', () => { + function syncValidator1() { + return {'sync1': true}; + } + function syncValidator2() { + return {'sync2': true}; + } + + const a = b.array(['one', 'two'], [syncValidator1, syncValidator2]); + expect(a.value).toEqual(['one', 'two']); + expect(a.errors).toEqual({'sync1': true, 'sync2': true}); + }); + + describe('updateOn', () => { + it('should default to on change', () => { + const c = b.control(''); + expect(c.updateOn).toEqual('change'); }); - it('should create controls from a boxed value', () => { - const g = b.group({'login': {value: 'some value', disabled: true}}); - - expect(g.controls['login'].value).toEqual('some value'); - expect(g.controls['login'].disabled).toEqual(true); + it('should default to on change with an options obj', () => { + const c = b.control('', {validators: Validators.required}); + expect(c.updateOn).toEqual('change'); }); - it('should create controls from an array', () => { - const g = b.group( - {'login': ['some value'], 'password': ['some value', syncValidator, asyncValidator]}); - - expect(g.controls['login'].value).toEqual('some value'); - expect(g.controls['password'].value).toEqual('some value'); - expect(g.controls['password'].validator).toEqual(syncValidator); - expect(g.controls['password'].asyncValidator).toEqual(asyncValidator); + it('should set updateOn when updating on blur', () => { + const c = b.control('', {updateOn: 'blur'}); + expect(c.updateOn).toEqual('blur'); }); - it('should use controls whose form state is a primitive value', () => { - const g = b.group({'login': b.control('some value', syncValidator, asyncValidator)}); + describe('in groups and arrays', () => { + it('should default to group updateOn when not set in control', () => { + const g = b.group({one: b.control(''), two: b.control('')}, {updateOn: 'blur'}); - expect(g.controls['login'].value).toEqual('some value'); - expect(g.controls['login'].validator).toBe(syncValidator); - expect(g.controls['login'].asyncValidator).toBe(asyncValidator); - }); - - it('should support controls with no validators and whose form state is null', () => { - const g = b.group({'login': b.control(null)}); - expect(g.controls['login'].value).toBeNull(); - expect(g.controls['login'].validator).toBeNull(); - expect(g.controls['login'].asyncValidator).toBeNull(); - }); - - it('should support controls with validators and whose form state is null', () => { - const g = b.group({'login': b.control(null, syncValidator, asyncValidator)}); - expect(g.controls['login'].value).toBeNull(); - expect(g.controls['login'].validator).toBe(syncValidator); - expect(g.controls['login'].asyncValidator).toBe(asyncValidator); - }); - - it('should support controls with no validators and whose form state is undefined', () => { - const g = b.group({'login': b.control(undefined)}); - expect(g.controls['login'].value).toBeNull(); - expect(g.controls['login'].validator).toBeNull(); - expect(g.controls['login'].asyncValidator).toBeNull(); - }); - - it('should support controls with validators and whose form state is undefined', () => { - const g = b.group({'login': b.control(undefined, syncValidator, asyncValidator)}); - expect(g.controls['login'].value).toBeNull(); - expect(g.controls['login'].validator).toBe(syncValidator); - expect(g.controls['login'].asyncValidator).toBe(asyncValidator); - }); - - it('should create groups with a custom validator', () => { - const g = b.group( - {'login': 'some value'}, {'validator': syncValidator, 'asyncValidator': asyncValidator}); - - expect(g.validator).toBe(syncValidator); - expect(g.asyncValidator).toBe(asyncValidator); - }); - - it('should create control arrays', () => { - const c = b.control('three'); - const e = b.control(null); - const f = b.control(undefined); - const a = b.array( - ['one', ['two', syncValidator], c, b.array(['four']), e, f], syncValidator, - asyncValidator); - - expect(a.value).toEqual(['one', 'two', 'three', ['four'], null, null]); - expect(a.validator).toBe(syncValidator); - expect(a.asyncValidator).toBe(asyncValidator); - }); - - it('should create control arrays with multiple async validators', fakeAsync(() => { - function asyncValidator1() { return of ({'async1': true}); } - function asyncValidator2() { return of ({'async2': true}); } - - const a = b.array(['one', 'two'], null, [asyncValidator1, asyncValidator2]); - expect(a.value).toEqual(['one', 'two']); - - tick(); - - expect(a.errors).toEqual({'async1': true, 'async2': true}); - })); - - it('should create control arrays with multiple sync validators', () => { - function syncValidator1() { return {'sync1': true}; } - function syncValidator2() { return {'sync2': true}; } - - const a = b.array(['one', 'two'], [syncValidator1, syncValidator2]); - expect(a.value).toEqual(['one', 'two']); - expect(a.errors).toEqual({'sync1': true, 'sync2': true}); - }); - - describe('updateOn', () => { - it('should default to on change', () => { - const c = b.control(''); - expect(c.updateOn).toEqual('change'); + expect(g.get('one')!.updateOn).toEqual('blur'); + expect(g.get('two')!.updateOn).toEqual('blur'); }); - it('should default to on change with an options obj', () => { - const c = b.control('', {validators: Validators.required}); - expect(c.updateOn).toEqual('change'); + it('should default to array updateOn when not set in control', () => { + const a = b.array([b.control(''), b.control('')], {updateOn: 'blur'}); + + expect(a.get([0])!.updateOn).toEqual('blur'); + expect(a.get([1])!.updateOn).toEqual('blur'); }); - it('should set updateOn when updating on blur', () => { - const c = b.control('', {updateOn: 'blur'}); - expect(c.updateOn).toEqual('blur'); + it('should set updateOn with nested groups', () => { + const g = b.group( + { + group: b.group({one: b.control(''), two: b.control('')}), + }, + {updateOn: 'blur'}); + + expect(g.get('group.one')!.updateOn).toEqual('blur'); + expect(g.get('group.two')!.updateOn).toEqual('blur'); + expect(g.get('group')!.updateOn).toEqual('blur'); }); - describe('in groups and arrays', () => { - it('should default to group updateOn when not set in control', () => { - const g = b.group({one: b.control(''), two: b.control('')}, {updateOn: 'blur'}); + it('should set updateOn with nested arrays', () => { + const g = b.group( + { + arr: b.array([b.control(''), b.control('')]), + }, + {updateOn: 'blur'}); - expect(g.get('one') !.updateOn).toEqual('blur'); - expect(g.get('two') !.updateOn).toEqual('blur'); + expect(g.get(['arr', 0])!.updateOn).toEqual('blur'); + expect(g.get(['arr', 1])!.updateOn).toEqual('blur'); + expect(g.get('arr')!.updateOn).toEqual('blur'); + }); + + it('should allow control updateOn to override group updateOn', () => { + const g = b.group( + {one: b.control('', {updateOn: 'change'}), two: b.control('')}, {updateOn: 'blur'}); + + expect(g.get('one')!.updateOn).toEqual('change'); + expect(g.get('two')!.updateOn).toEqual('blur'); + }); + + it('should set updateOn with complex setup', () => { + const g = b.group({ + group: b.group( + {one: b.control('', {updateOn: 'change'}), two: b.control('')}, {updateOn: 'blur'}), + groupTwo: b.group({one: b.control('')}, {updateOn: 'submit'}), + three: b.control('') }); - it('should default to array updateOn when not set in control', () => { - const a = b.array([b.control(''), b.control('')], {updateOn: 'blur'}); - - expect(a.get([0]) !.updateOn).toEqual('blur'); - expect(a.get([1]) !.updateOn).toEqual('blur'); - }); - - it('should set updateOn with nested groups', () => { - const g = b.group( - { - group: b.group({one: b.control(''), two: b.control('')}), - }, - {updateOn: 'blur'}); - - expect(g.get('group.one') !.updateOn).toEqual('blur'); - expect(g.get('group.two') !.updateOn).toEqual('blur'); - expect(g.get('group') !.updateOn).toEqual('blur'); - }); - - it('should set updateOn with nested arrays', () => { - const g = b.group( - { - arr: b.array([b.control(''), b.control('')]), - }, - {updateOn: 'blur'}); - - expect(g.get(['arr', 0]) !.updateOn).toEqual('blur'); - expect(g.get(['arr', 1]) !.updateOn).toEqual('blur'); - expect(g.get('arr') !.updateOn).toEqual('blur'); - }); - - it('should allow control updateOn to override group updateOn', () => { - const g = b.group( - {one: b.control('', {updateOn: 'change'}), two: b.control('')}, {updateOn: 'blur'}); - - expect(g.get('one') !.updateOn).toEqual('change'); - expect(g.get('two') !.updateOn).toEqual('blur'); - }); - - it('should set updateOn with complex setup', () => { - const g = b.group({ - group: b.group( - {one: b.control('', {updateOn: 'change'}), two: b.control('')}, {updateOn: 'blur'}), - groupTwo: b.group({one: b.control('')}, {updateOn: 'submit'}), - three: b.control('') - }); - - expect(g.get('group.one') !.updateOn).toEqual('change'); - expect(g.get('group.two') !.updateOn).toEqual('blur'); - expect(g.get('groupTwo.one') !.updateOn).toEqual('submit'); - expect(g.get('three') !.updateOn).toEqual('change'); - }); + expect(g.get('group.one')!.updateOn).toEqual('change'); + expect(g.get('group.two')!.updateOn).toEqual('blur'); + expect(g.get('groupTwo.one')!.updateOn).toEqual('submit'); + expect(g.get('three')!.updateOn).toEqual('change'); }); }); }); +}); })(); diff --git a/packages/forms/test/form_control_spec.ts b/packages/forms/test/form_control_spec.ts index 0cf27f38cb..2f40bc982e 100644 --- a/packages/forms/test/form_control_spec.ts +++ b/packages/forms/test/form_control_spec.ts @@ -14,1240 +14,1260 @@ import {AbstractControl, AsyncValidatorFn, FormControl, FormGroup, ValidationErr import {FormArray} from '@angular/forms/src/model'; (function() { - function asyncValidator(expected: string, timeouts = {}): AsyncValidatorFn { - return (c: AbstractControl) => { - let resolve: (result: any) => void = undefined !; - const promise = new Promise(res => { resolve = res; }); - const t = (timeouts as any)[c.value] != null ? (timeouts as any)[c.value] : 0; - const res = c.value != expected ? {'async': true} : null; +function asyncValidator(expected: string, timeouts = {}): AsyncValidatorFn { + return (c: AbstractControl) => { + let resolve: (result: any) => void = undefined!; + const promise = new Promise(res => { + resolve = res; + }); + const t = (timeouts as any)[c.value] != null ? (timeouts as any)[c.value] : 0; + const res = c.value != expected ? {'async': true} : null; - if (t == 0) { + if (t == 0) { + resolve(res); + } else { + setTimeout(() => { resolve(res); - } else { - setTimeout(() => { resolve(res); }, t); - } + }, t); + } - return promise; - }; - } + return promise; + }; +} - function asyncValidatorReturningObservable(c: AbstractControl) { - const e = new EventEmitter(); - Promise.resolve(null).then(() => { e.emit({'async': true}); }); - return e; - } +function asyncValidatorReturningObservable(c: AbstractControl) { + const e = new EventEmitter(); + Promise.resolve(null).then(() => { + e.emit({'async': true}); + }); + return e; +} - function otherAsyncValidator() { return Promise.resolve({'other': true}); } +function otherAsyncValidator() { + return Promise.resolve({'other': true}); +} - function syncValidator(_: any /** TODO #9100 */): any /** TODO #9100 */ { return null; } +function syncValidator(_: any /** TODO #9100 */): any /** TODO #9100 */ { + return null; +} - describe('FormControl', () => { - it('should default the value to null', () => { - const c = new FormControl(); - expect(c.value).toBe(null); +describe('FormControl', () => { + it('should default the value to null', () => { + const c = new FormControl(); + expect(c.value).toBe(null); + }); + + describe('markAllAsTouched', () => { + it('should mark only the control itself as touched', () => { + const control = new FormControl(''); + expect(control.touched).toBe(false); + control.markAllAsTouched(); + expect(control.touched).toBe(true); + }); + }); + + describe('boxed values', () => { + it('should support valid boxed values on creation', () => { + const c = new FormControl({value: 'some val', disabled: true}, null!, null!); + expect(c.disabled).toBe(true); + expect(c.value).toBe('some val'); + expect(c.status).toBe('DISABLED'); }); - describe('markAllAsTouched', () => { - it('should mark only the control itself as touched', () => { - const control = new FormControl(''); - expect(control.touched).toBe(false); - control.markAllAsTouched(); - expect(control.touched).toBe(true); - }); + it('should honor boxed value with disabled control when validating', () => { + const c = new FormControl({value: '', disabled: true}, Validators.required); + expect(c.disabled).toBe(true); + expect(c.valid).toBe(false); + expect(c.status).toBe('DISABLED'); }); - describe('boxed values', () => { - it('should support valid boxed values on creation', () => { - const c = new FormControl({value: 'some val', disabled: true}, null !, null !); - expect(c.disabled).toBe(true); - expect(c.value).toBe('some val'); - expect(c.status).toBe('DISABLED'); - }); - - it('should honor boxed value with disabled control when validating', () => { - const c = new FormControl({value: '', disabled: true}, Validators.required); - expect(c.disabled).toBe(true); - expect(c.valid).toBe(false); - expect(c.status).toBe('DISABLED'); - }); - - it('should not treat objects as boxed values if they have more than two props', () => { - const c = new FormControl({value: '', disabled: true, test: 'test'}, null !, null !); - expect(c.value).toEqual({value: '', disabled: true, test: 'test'}); - expect(c.disabled).toBe(false); - }); - - it('should not treat objects as boxed values if disabled is missing', () => { - const c = new FormControl({value: '', test: 'test'}, null !, null !); - expect(c.value).toEqual({value: '', test: 'test'}); - expect(c.disabled).toBe(false); - }); - + it('should not treat objects as boxed values if they have more than two props', () => { + const c = new FormControl({value: '', disabled: true, test: 'test'}, null!, null!); + expect(c.value).toEqual({value: '', disabled: true, test: 'test'}); + expect(c.disabled).toBe(false); }); - describe('updateOn', () => { + it('should not treat objects as boxed values if disabled is missing', () => { + const c = new FormControl({value: '', test: 'test'}, null!, null!); + expect(c.value).toEqual({value: '', test: 'test'}); + expect(c.disabled).toBe(false); + }); + }); - it('should default to on change', () => { - const c = new FormControl(''); - expect(c.updateOn).toEqual('change'); + describe('updateOn', () => { + it('should default to on change', () => { + const c = new FormControl(''); + expect(c.updateOn).toEqual('change'); + }); + + it('should default to on change with an options obj', () => { + const c = new FormControl('', {validators: Validators.required}); + expect(c.updateOn).toEqual('change'); + }); + + it('should set updateOn when updating on blur', () => { + const c = new FormControl('', {updateOn: 'blur'}); + expect(c.updateOn).toEqual('blur'); + }); + + describe('in groups and arrays', () => { + it('should default to group updateOn when not set in control', () => { + const g = + new FormGroup({one: new FormControl(), two: new FormControl()}, {updateOn: 'blur'}); + + expect(g.get('one')!.updateOn).toEqual('blur'); + expect(g.get('two')!.updateOn).toEqual('blur'); }); - it('should default to on change with an options obj', () => { - const c = new FormControl('', {validators: Validators.required}); - expect(c.updateOn).toEqual('change'); + it('should default to array updateOn when not set in control', () => { + const a = new FormArray([new FormControl(), new FormControl()], {updateOn: 'blur'}); + + expect(a.get([0])!.updateOn).toEqual('blur'); + expect(a.get([1])!.updateOn).toEqual('blur'); }); - it('should set updateOn when updating on blur', () => { - const c = new FormControl('', {updateOn: 'blur'}); - expect(c.updateOn).toEqual('blur'); + it('should set updateOn with nested groups', () => { + const g = new FormGroup( + { + group: new FormGroup({one: new FormControl(), two: new FormControl()}), + }, + {updateOn: 'blur'}); + + expect(g.get('group.one')!.updateOn).toEqual('blur'); + expect(g.get('group.two')!.updateOn).toEqual('blur'); + expect(g.get('group')!.updateOn).toEqual('blur'); }); - describe('in groups and arrays', () => { - it('should default to group updateOn when not set in control', () => { - const g = - new FormGroup({one: new FormControl(), two: new FormControl()}, {updateOn: 'blur'}); + it('should set updateOn with nested arrays', () => { + const g = new FormGroup( + { + arr: new FormArray([new FormControl(), new FormControl()]), + }, + {updateOn: 'blur'}); - expect(g.get('one') !.updateOn).toEqual('blur'); - expect(g.get('two') !.updateOn).toEqual('blur'); - }); + expect(g.get(['arr', 0])!.updateOn).toEqual('blur'); + expect(g.get(['arr', 1])!.updateOn).toEqual('blur'); + expect(g.get('arr')!.updateOn).toEqual('blur'); + }); - it('should default to array updateOn when not set in control', () => { - const a = new FormArray([new FormControl(), new FormControl()], {updateOn: 'blur'}); + it('should allow control updateOn to override group updateOn', () => { + const g = new FormGroup( + {one: new FormControl('', {updateOn: 'change'}), two: new FormControl()}, + {updateOn: 'blur'}); - expect(a.get([0]) !.updateOn).toEqual('blur'); - expect(a.get([1]) !.updateOn).toEqual('blur'); - }); + expect(g.get('one')!.updateOn).toEqual('change'); + expect(g.get('two')!.updateOn).toEqual('blur'); + }); - it('should set updateOn with nested groups', () => { - const g = new FormGroup( - { - group: new FormGroup({one: new FormControl(), two: new FormControl()}), - }, - {updateOn: 'blur'}); - - expect(g.get('group.one') !.updateOn).toEqual('blur'); - expect(g.get('group.two') !.updateOn).toEqual('blur'); - expect(g.get('group') !.updateOn).toEqual('blur'); - }); - - it('should set updateOn with nested arrays', () => { - const g = new FormGroup( - { - arr: new FormArray([new FormControl(), new FormControl()]), - }, - {updateOn: 'blur'}); - - expect(g.get(['arr', 0]) !.updateOn).toEqual('blur'); - expect(g.get(['arr', 1]) !.updateOn).toEqual('blur'); - expect(g.get('arr') !.updateOn).toEqual('blur'); - }); - - it('should allow control updateOn to override group updateOn', () => { - const g = new FormGroup( + it('should set updateOn with complex setup', () => { + const g = new FormGroup({ + group: new FormGroup( {one: new FormControl('', {updateOn: 'change'}), two: new FormControl()}, - {updateOn: 'blur'}); - - expect(g.get('one') !.updateOn).toEqual('change'); - expect(g.get('two') !.updateOn).toEqual('blur'); - }); - - it('should set updateOn with complex setup', () => { - const g = new FormGroup({ - group: new FormGroup( - {one: new FormControl('', {updateOn: 'change'}), two: new FormControl()}, - {updateOn: 'blur'}), - groupTwo: new FormGroup({one: new FormControl()}, {updateOn: 'submit'}), - three: new FormControl() - }); - - expect(g.get('group.one') !.updateOn).toEqual('change'); - expect(g.get('group.two') !.updateOn).toEqual('blur'); - expect(g.get('groupTwo.one') !.updateOn).toEqual('submit'); - expect(g.get('three') !.updateOn).toEqual('change'); - }); - - - }); - - }); - - describe('validator', () => { - - it('should run validator with the initial value', () => { - const c = new FormControl('value', Validators.required); - expect(c.valid).toEqual(true); - }); - - it('should rerun the validator when the value changes', () => { - const c = new FormControl('value', Validators.required); - c.setValue(null); - expect(c.valid).toEqual(false); - }); - - it('should support arrays of validator functions if passed', () => { - const c = new FormControl('value', [Validators.required, Validators.minLength(3)]); - c.setValue('a'); - expect(c.valid).toEqual(false); - - c.setValue('aaa'); - expect(c.valid).toEqual(true); - }); - - it('should support single validator from options obj', () => { - const c = new FormControl(null, {validators: Validators.required}); - expect(c.valid).toEqual(false); - expect(c.errors).toEqual({required: true}); - - c.setValue('value'); - expect(c.valid).toEqual(true); - }); - - it('should support multiple validators from options obj', () => { - const c = - new FormControl(null, {validators: [Validators.required, Validators.minLength(3)]}); - expect(c.valid).toEqual(false); - expect(c.errors).toEqual({required: true}); - - c.setValue('aa'); - expect(c.valid).toEqual(false); - expect(c.errors).toEqual({minlength: {requiredLength: 3, actualLength: 2}}); - - c.setValue('aaa'); - expect(c.valid).toEqual(true); - }); - - it('should support a null validators value', () => { - const c = new FormControl(null, {validators: null}); - expect(c.valid).toEqual(true); - }); - - it('should support an empty options obj', () => { - const c = new FormControl(null, {}); - expect(c.valid).toEqual(true); - }); - - it('should return errors', () => { - const c = new FormControl(null, Validators.required); - expect(c.errors).toEqual({'required': true}); - }); - - it('should set single validator', () => { - const c = new FormControl(null); - expect(c.valid).toEqual(true); - - c.setValidators(Validators.required); - - c.setValue(null); - expect(c.valid).toEqual(false); - - c.setValue('abc'); - expect(c.valid).toEqual(true); - }); - - it('should set multiple validators from array', () => { - const c = new FormControl(''); - expect(c.valid).toEqual(true); - - c.setValidators([Validators.minLength(5), Validators.required]); - - c.setValue(''); - expect(c.valid).toEqual(false); - - c.setValue('abc'); - expect(c.valid).toEqual(false); - - c.setValue('abcde'); - expect(c.valid).toEqual(true); - }); - - it('should clear validators', () => { - const c = new FormControl('', Validators.required); - expect(c.valid).toEqual(false); - - c.clearValidators(); - expect(c.validator).toEqual(null); - - c.setValue(''); - expect(c.valid).toEqual(true); - }); - - it('should add after clearing', () => { - const c = new FormControl('', Validators.required); - expect(c.valid).toEqual(false); - - c.clearValidators(); - expect(c.validator).toEqual(null); - - c.setValidators([Validators.required]); - expect(c.validator).not.toBe(null); - }); - }); - - describe('asyncValidator', () => { - it('should run validator with the initial value', fakeAsync(() => { - const c = new FormControl('value', null !, asyncValidator('expected')); - tick(); - - expect(c.valid).toEqual(false); - expect(c.errors).toEqual({'async': true}); - })); - - it('should support validators returning observables', fakeAsync(() => { - const c = new FormControl('value', null !, asyncValidatorReturningObservable); - tick(); - - expect(c.valid).toEqual(false); - expect(c.errors).toEqual({'async': true}); - })); - - it('should rerun the validator when the value changes', fakeAsync(() => { - const c = new FormControl('value', null !, asyncValidator('expected')); - - c.setValue('expected'); - tick(); - - expect(c.valid).toEqual(true); - })); - - it('should run the async validator only when the sync validator passes', fakeAsync(() => { - const c = new FormControl('', Validators.required, asyncValidator('expected')); - tick(); - - expect(c.errors).toEqual({'required': true}); - - c.setValue('some value'); - tick(); - - expect(c.errors).toEqual({'async': true}); - })); - - it('should mark the control as pending while running the async validation', fakeAsync(() => { - const c = new FormControl('', null !, asyncValidator('expected')); - - expect(c.pending).toEqual(true); - - tick(); - - expect(c.pending).toEqual(false); - })); - - it('should only use the latest async validation run', fakeAsync(() => { - const c = new FormControl( - '', null !, asyncValidator('expected', {'long': 200, 'expected': 100})); - - c.setValue('long'); - c.setValue('expected'); - - tick(300); - - expect(c.valid).toEqual(true); - })); - - it('should support arrays of async validator functions if passed', fakeAsync(() => { - const c = - new FormControl('value', null !, [asyncValidator('expected'), otherAsyncValidator]); - tick(); - - expect(c.errors).toEqual({'async': true, 'other': true}); - })); - - - it('should support a single async validator from options obj', fakeAsync(() => { - const c = new FormControl('value', {asyncValidators: asyncValidator('expected')}); - expect(c.pending).toEqual(true); - tick(); - - expect(c.valid).toEqual(false); - expect(c.errors).toEqual({'async': true}); - })); - - it('should support multiple async validators from options obj', fakeAsync(() => { - const c = new FormControl( - 'value', {asyncValidators: [asyncValidator('expected'), otherAsyncValidator]}); - expect(c.pending).toEqual(true); - tick(); - - expect(c.valid).toEqual(false); - expect(c.errors).toEqual({'async': true, 'other': true}); - })); - - it('should support a mix of validators from options obj', fakeAsync(() => { - const c = new FormControl( - '', {validators: Validators.required, asyncValidators: asyncValidator('expected')}); - tick(); - expect(c.errors).toEqual({required: true}); - - c.setValue('value'); - expect(c.pending).toBe(true); - - tick(); - expect(c.valid).toEqual(false); - expect(c.errors).toEqual({'async': true}); - })); - - it('should add single async validator', fakeAsync(() => { - const c = new FormControl('value', null !); - - c.setAsyncValidators(asyncValidator('expected')); - expect(c.asyncValidator).not.toEqual(null); - - c.setValue('expected'); - tick(); - - expect(c.valid).toEqual(true); - })); - - it('should add async validator from array', fakeAsync(() => { - const c = new FormControl('value', null !); - - c.setAsyncValidators([asyncValidator('expected')]); - expect(c.asyncValidator).not.toEqual(null); - - c.setValue('expected'); - tick(); - - expect(c.valid).toEqual(true); - })); - - it('should clear async validators', fakeAsync(() => { - const c = new FormControl('value', [asyncValidator('expected'), otherAsyncValidator]); - - c.clearValidators(); - - expect(c.asyncValidator).toEqual(null); - })); - - it('should not change validity state if control is disabled while async validating', - fakeAsync(() => { - const c = new FormControl('value', [asyncValidator('expected')]); - c.disable(); - tick(); - expect(c.status).toEqual('DISABLED'); - })); - }); - - describe('dirty', () => { - it('should be false after creating a control', () => { - const c = new FormControl('value'); - expect(c.dirty).toEqual(false); - }); - - it('should be true after changing the value of the control', () => { - const c = new FormControl('value'); - c.markAsDirty(); - expect(c.dirty).toEqual(true); - }); - }); - - describe('touched', () => { - it('should be false after creating a control', () => { - const c = new FormControl('value'); - expect(c.touched).toEqual(false); - }); - - it('should be true after markAsTouched runs', () => { - const c = new FormControl('value'); - c.markAsTouched(); - expect(c.touched).toEqual(true); - }); - }); - - describe('setValue', () => { - let g: FormGroup, c: FormControl; - beforeEach(() => { - c = new FormControl('oldValue'); - g = new FormGroup({'one': c}); - }); - - it('should set the value of the control', () => { - c.setValue('newValue'); - expect(c.value).toEqual('newValue'); - }); - - it('should invoke ngOnChanges if it is present', () => { - let ngOnChanges: any; - c.registerOnChange((v: any) => ngOnChanges = ['invoked', v]); - - c.setValue('newValue'); - - expect(ngOnChanges).toEqual(['invoked', 'newValue']); - }); - - it('should not invoke on change when explicitly specified', () => { - let onChange: any = null; - c.registerOnChange((v: any) => onChange = ['invoked', v]); - - c.setValue('newValue', {emitModelToViewChange: false}); - - expect(onChange).toBeNull(); - }); - - it('should set the parent', () => { - c.setValue('newValue'); - expect(g.value).toEqual({'one': 'newValue'}); - }); - - it('should not set the parent when explicitly specified', () => { - c.setValue('newValue', {onlySelf: true}); - expect(g.value).toEqual({'one': 'oldValue'}); - }); - - it('should fire an event', fakeAsync(() => { - c.valueChanges.subscribe((value) => { expect(value).toEqual('newValue'); }); - - c.setValue('newValue'); - tick(); - })); - - it('should not fire an event when explicitly specified', fakeAsync(() => { - c.valueChanges.subscribe((value) => { throw 'Should not happen'; }); - - c.setValue('newValue', {emitEvent: false}); - tick(); - })); - - it('should work on a disabled control', () => { - g.addControl('two', new FormControl('two')); - c.disable(); - c.setValue('new value'); - expect(c.value).toEqual('new value'); - expect(g.value).toEqual({'two': 'two'}); - }); - }); - - describe('patchValue', () => { - let g: FormGroup, c: FormControl; - beforeEach(() => { - c = new FormControl('oldValue'); - g = new FormGroup({'one': c}); - }); - - it('should set the value of the control', () => { - c.patchValue('newValue'); - expect(c.value).toEqual('newValue'); - }); - - it('should invoke ngOnChanges if it is present', () => { - let ngOnChanges: any; - c.registerOnChange((v: any) => ngOnChanges = ['invoked', v]); - - c.patchValue('newValue'); - - expect(ngOnChanges).toEqual(['invoked', 'newValue']); - }); - - it('should not invoke on change when explicitly specified', () => { - let onChange: any = null; - c.registerOnChange((v: any) => onChange = ['invoked', v]); - - c.patchValue('newValue', {emitModelToViewChange: false}); - - expect(onChange).toBeNull(); - }); - - it('should set the parent', () => { - c.patchValue('newValue'); - expect(g.value).toEqual({'one': 'newValue'}); - }); - - it('should not set the parent when explicitly specified', () => { - c.patchValue('newValue', {onlySelf: true}); - expect(g.value).toEqual({'one': 'oldValue'}); - }); - - it('should fire an event', fakeAsync(() => { - c.valueChanges.subscribe((value) => { expect(value).toEqual('newValue'); }); - - c.patchValue('newValue'); - tick(); - })); - - it('should not fire an event when explicitly specified', fakeAsync(() => { - c.valueChanges.subscribe((value) => { throw 'Should not happen'; }); - - c.patchValue('newValue', {emitEvent: false}); - - tick(); - })); - - it('should patch value on a disabled control', () => { - g.addControl('two', new FormControl('two')); - c.disable(); - - c.patchValue('new value'); - expect(c.value).toEqual('new value'); - expect(g.value).toEqual({'two': 'two'}); - }); - }); - - describe('reset()', () => { - let c: FormControl; - - beforeEach(() => { c = new FormControl('initial value'); }); - - it('should reset to a specific value if passed', () => { - c.setValue('new value'); - expect(c.value).toBe('new value'); - - c.reset('initial value'); - expect(c.value).toBe('initial value'); - }); - - it('should not set the parent when explicitly specified', () => { - const g = new FormGroup({'one': c}); - c.patchValue('newValue', {onlySelf: true}); - expect(g.value).toEqual({'one': 'initial value'}); - }); - - it('should reset to a specific value if passed with boxed value', () => { - c.setValue('new value'); - expect(c.value).toBe('new value'); - - c.reset({value: 'initial value', disabled: false}); - expect(c.value).toBe('initial value'); - }); - - it('should clear the control value if no value is passed', () => { - c.setValue('new value'); - expect(c.value).toBe('new value'); - - c.reset(); - expect(c.value).toBe(null); - }); - - it('should update the value of any parent controls with passed value', () => { - const g = new FormGroup({'one': c}); - c.setValue('new value'); - expect(g.value).toEqual({'one': 'new value'}); - - c.reset('initial value'); - expect(g.value).toEqual({'one': 'initial value'}); - }); - - it('should update the value of any parent controls with null value', () => { - const g = new FormGroup({'one': c}); - c.setValue('new value'); - expect(g.value).toEqual({'one': 'new value'}); - - c.reset(); - expect(g.value).toEqual({'one': null}); - }); - - it('should mark the control as pristine', () => { - c.markAsDirty(); - expect(c.pristine).toBe(false); - - c.reset(); - expect(c.pristine).toBe(true); - }); - - it('should set the parent pristine state if all pristine', () => { - const g = new FormGroup({'one': c}); - c.markAsDirty(); - expect(g.pristine).toBe(false); - - c.reset(); - expect(g.pristine).toBe(true); - }); - - it('should not set the parent pristine state if it has other dirty controls', () => { - const c2 = new FormControl('two'); - const g = new FormGroup({'one': c, 'two': c2}); - c.markAsDirty(); - c2.markAsDirty(); - - c.reset(); - expect(g.pristine).toBe(false); - }); - - it('should mark the control as untouched', () => { - c.markAsTouched(); - expect(c.untouched).toBe(false); - - c.reset(); - expect(c.untouched).toBe(true); - }); - - it('should set the parent untouched state if all untouched', () => { - const g = new FormGroup({'one': c}); - c.markAsTouched(); - expect(g.untouched).toBe(false); - - c.reset(); - expect(g.untouched).toBe(true); - }); - - it('should not set the parent untouched state if other touched controls', () => { - const c2 = new FormControl('two'); - const g = new FormGroup({'one': c, 'two': c2}); - c.markAsTouched(); - c2.markAsTouched(); - - c.reset(); - expect(g.untouched).toBe(false); - }); - - it('should retain the disabled state of the control', () => { - c.disable(); - c.reset(); - - expect(c.disabled).toBe(true); - }); - - it('should set disabled state based on boxed value if passed', () => { - c.disable(); - c.reset({value: null, disabled: false}); - - expect(c.disabled).toBe(false); - }); - - describe('reset() events', () => { - let g: FormGroup, c2: FormControl, logger: any[]; - - beforeEach(() => { - c2 = new FormControl('two'); - g = new FormGroup({'one': c, 'two': c2}); - logger = []; - }); - - it('should emit one valueChange event per reset control', () => { - g.valueChanges.subscribe(() => logger.push('group')); - c.valueChanges.subscribe(() => logger.push('control1')); - c2.valueChanges.subscribe(() => logger.push('control2')); - - c.reset(); - expect(logger).toEqual(['control1', 'group']); - }); - - it('should not fire an event when explicitly specified', fakeAsync(() => { - g.valueChanges.subscribe((value) => { throw 'Should not happen'; }); - c.valueChanges.subscribe((value) => { throw 'Should not happen'; }); - c2.valueChanges.subscribe((value) => { throw 'Should not happen'; }); - - c.reset(null, {emitEvent: false}); - - tick(); - })); - - it('should emit one statusChange event per reset control', () => { - g.statusChanges.subscribe(() => logger.push('group')); - c.statusChanges.subscribe(() => logger.push('control1')); - c2.statusChanges.subscribe(() => logger.push('control2')); - - c.reset(); - expect(logger).toEqual(['control1', 'group']); - }); - - it('should emit one statusChange event per disabled control', () => { - g.statusChanges.subscribe(() => logger.push('group')); - c.statusChanges.subscribe(() => logger.push('control1')); - c2.statusChanges.subscribe(() => logger.push('control2')); - - c.reset({value: null, disabled: true}); - expect(logger).toEqual(['control1', 'group']); - }); - }); - - }); - - describe('valueChanges & statusChanges', () => { - let c: FormControl; - - beforeEach(() => { c = new FormControl('old', Validators.required); }); - - it('should fire an event after the value has been updated', - inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { - c.valueChanges.subscribe({ - next: (value: any) => { - expect(c.value).toEqual('new'); - expect(value).toEqual('new'); - async.done(); - } - }); - c.setValue('new'); - })); - - it('should fire an event after the status has been updated to invalid', fakeAsync(() => { - c.statusChanges.subscribe({ - next: (status: any) => { - expect(c.status).toEqual('INVALID'); - expect(status).toEqual('INVALID'); - } - }); - - c.setValue(''); - tick(); - })); - - it('should fire an event after the status has been updated to pending', fakeAsync(() => { - const c = new FormControl('old', Validators.required, asyncValidator('expected')); - - const log: any[] /** TODO #9100 */ = []; - c.valueChanges.subscribe({next: (value: any) => log.push(`value: '${value}'`)}); - - c.statusChanges.subscribe({next: (status: any) => log.push(`status: '${status}'`)}); - - c.setValue(''); - tick(); - - c.setValue('nonEmpty'); - tick(); - - c.setValue('expected'); - tick(); - - expect(log).toEqual([ - 'value: \'\'', - 'status: \'INVALID\'', - 'value: \'nonEmpty\'', - 'status: \'PENDING\'', - 'status: \'INVALID\'', - 'value: \'expected\'', - 'status: \'PENDING\'', - 'status: \'VALID\'', - ]); - })); - - // TODO: remove the if statement after making observable delivery sync - it('should update set errors and status before emitting an event', - inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { - c.valueChanges.subscribe((value: any /** TODO #9100 */) => { - expect(c.valid).toEqual(false); - expect(c.errors).toEqual({'required': true}); - async.done(); - }); - c.setValue(''); - })); - - it('should return a cold observable', - inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { - c.setValue('will be ignored'); - c.valueChanges.subscribe({ - next: (value: any) => { - expect(value).toEqual('new'); - async.done(); - } - }); - c.setValue('new'); - })); - }); - - describe('setErrors', () => { - it('should set errors on a control', () => { - const c = new FormControl('someValue'); - - c.setErrors({'someError': true}); - - expect(c.valid).toEqual(false); - expect(c.errors).toEqual({'someError': true}); - }); - - it('should reset the errors and validity when the value changes', () => { - const c = new FormControl('someValue', Validators.required); - - c.setErrors({'someError': true}); - c.setValue(''); - - expect(c.errors).toEqual({'required': true}); - }); - - it('should update the parent group\'s validity', () => { - const c = new FormControl('someValue'); - const g = new FormGroup({'one': c}); - - expect(g.valid).toEqual(true); - - c.setErrors({'someError': true}); - - expect(g.valid).toEqual(false); - }); - - it('should not reset parent\'s errors', () => { - const c = new FormControl('someValue'); - const g = new FormGroup({'one': c}); - - g.setErrors({'someGroupError': true}); - c.setErrors({'someError': true}); - - expect(g.errors).toEqual({'someGroupError': true}); - }); - - it('should reset errors when updating a value', () => { - const c = new FormControl('oldValue'); - const g = new FormGroup({'one': c}); - - g.setErrors({'someGroupError': true}); - c.setErrors({'someError': true}); - - c.setValue('newValue'); - - expect(c.errors).toEqual(null); - expect(g.errors).toEqual(null); - }); - }); - - describe('disable() & enable()', () => { - - it('should mark the control as disabled', () => { - const c = new FormControl(null); - expect(c.disabled).toBe(false); - expect(c.valid).toBe(true); - - c.disable(); - expect(c.disabled).toBe(true); - expect(c.valid).toBe(false); - - c.enable(); - expect(c.disabled).toBe(false); - expect(c.valid).toBe(true); - }); - - it('should set the control status as disabled', () => { - const c = new FormControl(null); - expect(c.status).toEqual('VALID'); - - c.disable(); - expect(c.status).toEqual('DISABLED'); - - c.enable(); - expect(c.status).toEqual('VALID'); - }); - - it('should retain the original value when disabled', () => { - const c = new FormControl('some value'); - expect(c.value).toEqual('some value'); - - c.disable(); - expect(c.value).toEqual('some value'); - - c.enable(); - expect(c.value).toEqual('some value'); - }); - - it('should keep the disabled control in the group, but return false for contains()', () => { - const c = new FormControl(''); - const g = new FormGroup({'one': c}); - - expect(g.get('one')).toBeDefined(); - expect(g.contains('one')).toBe(true); - - c.disable(); - expect(g.get('one')).toBeDefined(); - expect(g.contains('one')).toBe(false); - }); - - it('should mark the parent group disabled if all controls are disabled', () => { - const c = new FormControl(); - const c2 = new FormControl(); - const g = new FormGroup({'one': c, 'two': c2}); - expect(g.enabled).toBe(true); - - c.disable(); - expect(g.enabled).toBe(true); - - c2.disable(); - expect(g.enabled).toBe(false); - - c.enable(); - expect(g.enabled).toBe(true); - }); - - it('should update the parent group value when child control status changes', () => { - const c = new FormControl('one'); - const c2 = new FormControl('two'); - const g = new FormGroup({'one': c, 'two': c2}); - expect(g.value).toEqual({'one': 'one', 'two': 'two'}); - - c.disable(); - expect(g.value).toEqual({'two': 'two'}); - - c2.disable(); - expect(g.value).toEqual({'one': 'one', 'two': 'two'}); - - c.enable(); - expect(g.value).toEqual({'one': 'one'}); - }); - - it('should mark the parent array disabled if all controls are disabled', () => { - const c = new FormControl(); - const c2 = new FormControl(); - const a = new FormArray([c, c2]); - expect(a.enabled).toBe(true); - - c.disable(); - expect(a.enabled).toBe(true); - - c2.disable(); - expect(a.enabled).toBe(false); - - c.enable(); - expect(a.enabled).toBe(true); - }); - - it('should update the parent array value when child control status changes', () => { - const c = new FormControl('one'); - const c2 = new FormControl('two'); - const a = new FormArray([c, c2]); - expect(a.value).toEqual(['one', 'two']); - - c.disable(); - expect(a.value).toEqual(['two']); - - c2.disable(); - expect(a.value).toEqual(['one', 'two']); - - c.enable(); - expect(a.value).toEqual(['one']); - }); - - it('should ignore disabled array controls when determining dirtiness', () => { - const c = new FormControl('one'); - const c2 = new FormControl('two'); - const a = new FormArray([c, c2]); - c.markAsDirty(); - expect(a.dirty).toBe(true); - - c.disable(); - expect(c.dirty).toBe(true); - expect(a.dirty).toBe(false); - - c.enable(); - expect(a.dirty).toBe(true); - }); - - it('should not make a dirty array not dirty when disabling controls', () => { - const c = new FormControl('one'); - const c2 = new FormControl('two'); - const a = new FormArray([c, c2]); - - a.markAsDirty(); - expect(a.dirty).toBe(true); - expect(c.dirty).toBe(false); - - c.disable(); - expect(a.dirty).toBe(true); - - c.enable(); - expect(a.dirty).toBe(true); - }); - - it('should ignore disabled controls in validation', () => { - const c = new FormControl(null, Validators.required); - const c2 = new FormControl(null); - const g = new FormGroup({one: c, two: c2}); - expect(g.valid).toBe(false); - - c.disable(); - expect(g.valid).toBe(true); - - c.enable(); - expect(g.valid).toBe(false); - }); - - it('should ignore disabled controls when serializing value in a group', () => { - const c = new FormControl('one'); - const c2 = new FormControl('two'); - const g = new FormGroup({one: c, two: c2}); - expect(g.value).toEqual({one: 'one', two: 'two'}); - - c.disable(); - expect(g.value).toEqual({two: 'two'}); - - c.enable(); - expect(g.value).toEqual({one: 'one', two: 'two'}); - }); - - it('should ignore disabled controls when serializing value in an array', () => { - const c = new FormControl('one'); - const c2 = new FormControl('two'); - const a = new FormArray([c, c2]); - expect(a.value).toEqual(['one', 'two']); - - c.disable(); - expect(a.value).toEqual(['two']); - - c.enable(); - expect(a.value).toEqual(['one', 'two']); - }); - - it('should ignore disabled controls when determining dirtiness', () => { - const c = new FormControl('one'); - const c2 = new FormControl('two'); - const g = new FormGroup({one: c, two: c2}); - c.markAsDirty(); - expect(g.dirty).toBe(true); - - c.disable(); - expect(c.dirty).toBe(true); - expect(g.dirty).toBe(false); - - c.enable(); - expect(g.dirty).toBe(true); - }); - - it('should not make a dirty group not dirty when disabling controls', () => { - const c = new FormControl('one'); - const c2 = new FormControl('two'); - const g = new FormGroup({one: c, two: c2}); - - g.markAsDirty(); - expect(g.dirty).toBe(true); - expect(c.dirty).toBe(false); - - c.disable(); - expect(g.dirty).toBe(true); - - c.enable(); - expect(g.dirty).toBe(true); - }); - - it('should ignore disabled controls when determining touched state', () => { - const c = new FormControl('one'); - const c2 = new FormControl('two'); - const g = new FormGroup({one: c, two: c2}); - c.markAsTouched(); - expect(g.touched).toBe(true); - - c.disable(); - expect(c.touched).toBe(true); - expect(g.touched).toBe(false); - - c.enable(); - expect(g.touched).toBe(true); - }); - - it('should not run validators on disabled controls', () => { - const validator = jasmine.createSpy('validator'); - const c = new FormControl('', validator); - expect(validator.calls.count()).toEqual(1); - - c.disable(); - expect(validator.calls.count()).toEqual(1); - - c.setValue('value'); - expect(validator.calls.count()).toEqual(1); - - c.enable(); - expect(validator.calls.count()).toEqual(2); - }); - - describe('disabled errors', () => { - it('should clear out the errors when disabled', () => { - const c = new FormControl('', Validators.required); - expect(c.errors).toEqual({required: true}); - - c.disable(); - expect(c.errors).toEqual(null); - - c.enable(); - expect(c.errors).toEqual({required: true}); - }); - - it('should clear out async errors when disabled', fakeAsync(() => { - const c = new FormControl('', null !, asyncValidator('expected')); - tick(); - expect(c.errors).toEqual({'async': true}); - - c.disable(); - expect(c.errors).toEqual(null); - - c.enable(); - tick(); - expect(c.errors).toEqual({'async': true}); - })); - }); - - describe('disabled events', () => { - let logger: string[]; - let c: FormControl; - let g: FormGroup; - - beforeEach(() => { - logger = []; - c = new FormControl('', Validators.required); - g = new FormGroup({one: c}); - }); - - it('should emit a statusChange event when disabled status changes', () => { - c.statusChanges.subscribe((status: string) => logger.push(status)); - - c.disable(); - expect(logger).toEqual(['DISABLED']); - - c.enable(); - expect(logger).toEqual(['DISABLED', 'INVALID']); - - }); - - it('should emit status change events in correct order', () => { - c.statusChanges.subscribe(() => logger.push('control')); - g.statusChanges.subscribe(() => logger.push('group')); - - c.disable(); - expect(logger).toEqual(['control', 'group']); - }); - - it('should throw when sync validator passed into async validator param', () => { - const fn = () => new FormControl('', syncValidator, syncValidator); - // test for the specific error since without the error check it would still throw an error - // but - // not a meaningful one - expect(fn).toThrowError(`Expected validator to return Promise or Observable.`); - }); - - it('should not emit value change events when emitEvent = false', () => { - c.valueChanges.subscribe(() => logger.push('control')); - g.valueChanges.subscribe(() => logger.push('group')); - - c.disable({emitEvent: false}); - expect(logger).toEqual([]); - c.enable({emitEvent: false}); - expect(logger).toEqual([]); - }); - - it('should not emit status change events when emitEvent = false', () => { - c.statusChanges.subscribe(() => logger.push('control')); - g.statusChanges.subscribe(() => logger.push('form')); - - c.disable({emitEvent: false}); - expect(logger).toEqual([]); - c.enable({emitEvent: false}); - expect(logger).toEqual([]); - }); - - }); - }); - describe('pending', () => { - let c: FormControl; - - beforeEach(() => { c = new FormControl('value'); }); - - it('should be false after creating a control', () => { expect(c.pending).toEqual(false); }); - - it('should be true after changing the value of the control', () => { - c.markAsPending(); - expect(c.pending).toEqual(true); - }); - - describe('status change events', () => { - let logger: string[]; - - beforeEach(() => { - logger = []; - c.statusChanges.subscribe((status) => logger.push(status)); - }); - - it('should emit event after marking control as pending', () => { - c.markAsPending(); - expect(logger).toEqual(['PENDING']); - }); - - it('should not emit event when emitEvent = false', () => { - c.markAsPending({emitEvent: false}); - expect(logger).toEqual([]); + {updateOn: 'blur'}), + groupTwo: new FormGroup({one: new FormControl()}, {updateOn: 'submit'}), + three: new FormControl() }); + expect(g.get('group.one')!.updateOn).toEqual('change'); + expect(g.get('group.two')!.updateOn).toEqual('blur'); + expect(g.get('groupTwo.one')!.updateOn).toEqual('submit'); + expect(g.get('three')!.updateOn).toEqual('change'); }); }); }); + + describe('validator', () => { + it('should run validator with the initial value', () => { + const c = new FormControl('value', Validators.required); + expect(c.valid).toEqual(true); + }); + + it('should rerun the validator when the value changes', () => { + const c = new FormControl('value', Validators.required); + c.setValue(null); + expect(c.valid).toEqual(false); + }); + + it('should support arrays of validator functions if passed', () => { + const c = new FormControl('value', [Validators.required, Validators.minLength(3)]); + c.setValue('a'); + expect(c.valid).toEqual(false); + + c.setValue('aaa'); + expect(c.valid).toEqual(true); + }); + + it('should support single validator from options obj', () => { + const c = new FormControl(null, {validators: Validators.required}); + expect(c.valid).toEqual(false); + expect(c.errors).toEqual({required: true}); + + c.setValue('value'); + expect(c.valid).toEqual(true); + }); + + it('should support multiple validators from options obj', () => { + const c = new FormControl(null, {validators: [Validators.required, Validators.minLength(3)]}); + expect(c.valid).toEqual(false); + expect(c.errors).toEqual({required: true}); + + c.setValue('aa'); + expect(c.valid).toEqual(false); + expect(c.errors).toEqual({minlength: {requiredLength: 3, actualLength: 2}}); + + c.setValue('aaa'); + expect(c.valid).toEqual(true); + }); + + it('should support a null validators value', () => { + const c = new FormControl(null, {validators: null}); + expect(c.valid).toEqual(true); + }); + + it('should support an empty options obj', () => { + const c = new FormControl(null, {}); + expect(c.valid).toEqual(true); + }); + + it('should return errors', () => { + const c = new FormControl(null, Validators.required); + expect(c.errors).toEqual({'required': true}); + }); + + it('should set single validator', () => { + const c = new FormControl(null); + expect(c.valid).toEqual(true); + + c.setValidators(Validators.required); + + c.setValue(null); + expect(c.valid).toEqual(false); + + c.setValue('abc'); + expect(c.valid).toEqual(true); + }); + + it('should set multiple validators from array', () => { + const c = new FormControl(''); + expect(c.valid).toEqual(true); + + c.setValidators([Validators.minLength(5), Validators.required]); + + c.setValue(''); + expect(c.valid).toEqual(false); + + c.setValue('abc'); + expect(c.valid).toEqual(false); + + c.setValue('abcde'); + expect(c.valid).toEqual(true); + }); + + it('should clear validators', () => { + const c = new FormControl('', Validators.required); + expect(c.valid).toEqual(false); + + c.clearValidators(); + expect(c.validator).toEqual(null); + + c.setValue(''); + expect(c.valid).toEqual(true); + }); + + it('should add after clearing', () => { + const c = new FormControl('', Validators.required); + expect(c.valid).toEqual(false); + + c.clearValidators(); + expect(c.validator).toEqual(null); + + c.setValidators([Validators.required]); + expect(c.validator).not.toBe(null); + }); + }); + + describe('asyncValidator', () => { + it('should run validator with the initial value', fakeAsync(() => { + const c = new FormControl('value', null!, asyncValidator('expected')); + tick(); + + expect(c.valid).toEqual(false); + expect(c.errors).toEqual({'async': true}); + })); + + it('should support validators returning observables', fakeAsync(() => { + const c = new FormControl('value', null!, asyncValidatorReturningObservable); + tick(); + + expect(c.valid).toEqual(false); + expect(c.errors).toEqual({'async': true}); + })); + + it('should rerun the validator when the value changes', fakeAsync(() => { + const c = new FormControl('value', null!, asyncValidator('expected')); + + c.setValue('expected'); + tick(); + + expect(c.valid).toEqual(true); + })); + + it('should run the async validator only when the sync validator passes', fakeAsync(() => { + const c = new FormControl('', Validators.required, asyncValidator('expected')); + tick(); + + expect(c.errors).toEqual({'required': true}); + + c.setValue('some value'); + tick(); + + expect(c.errors).toEqual({'async': true}); + })); + + it('should mark the control as pending while running the async validation', fakeAsync(() => { + const c = new FormControl('', null!, asyncValidator('expected')); + + expect(c.pending).toEqual(true); + + tick(); + + expect(c.pending).toEqual(false); + })); + + it('should only use the latest async validation run', fakeAsync(() => { + const c = + new FormControl('', null!, asyncValidator('expected', {'long': 200, 'expected': 100})); + + c.setValue('long'); + c.setValue('expected'); + + tick(300); + + expect(c.valid).toEqual(true); + })); + + it('should support arrays of async validator functions if passed', fakeAsync(() => { + const c = + new FormControl('value', null!, [asyncValidator('expected'), otherAsyncValidator]); + tick(); + + expect(c.errors).toEqual({'async': true, 'other': true}); + })); + + + it('should support a single async validator from options obj', fakeAsync(() => { + const c = new FormControl('value', {asyncValidators: asyncValidator('expected')}); + expect(c.pending).toEqual(true); + tick(); + + expect(c.valid).toEqual(false); + expect(c.errors).toEqual({'async': true}); + })); + + it('should support multiple async validators from options obj', fakeAsync(() => { + const c = new FormControl( + 'value', {asyncValidators: [asyncValidator('expected'), otherAsyncValidator]}); + expect(c.pending).toEqual(true); + tick(); + + expect(c.valid).toEqual(false); + expect(c.errors).toEqual({'async': true, 'other': true}); + })); + + it('should support a mix of validators from options obj', fakeAsync(() => { + const c = new FormControl( + '', {validators: Validators.required, asyncValidators: asyncValidator('expected')}); + tick(); + expect(c.errors).toEqual({required: true}); + + c.setValue('value'); + expect(c.pending).toBe(true); + + tick(); + expect(c.valid).toEqual(false); + expect(c.errors).toEqual({'async': true}); + })); + + it('should add single async validator', fakeAsync(() => { + const c = new FormControl('value', null!); + + c.setAsyncValidators(asyncValidator('expected')); + expect(c.asyncValidator).not.toEqual(null); + + c.setValue('expected'); + tick(); + + expect(c.valid).toEqual(true); + })); + + it('should add async validator from array', fakeAsync(() => { + const c = new FormControl('value', null!); + + c.setAsyncValidators([asyncValidator('expected')]); + expect(c.asyncValidator).not.toEqual(null); + + c.setValue('expected'); + tick(); + + expect(c.valid).toEqual(true); + })); + + it('should clear async validators', fakeAsync(() => { + const c = new FormControl('value', [asyncValidator('expected'), otherAsyncValidator]); + + c.clearValidators(); + + expect(c.asyncValidator).toEqual(null); + })); + + it('should not change validity state if control is disabled while async validating', + fakeAsync(() => { + const c = new FormControl('value', [asyncValidator('expected')]); + c.disable(); + tick(); + expect(c.status).toEqual('DISABLED'); + })); + }); + + describe('dirty', () => { + it('should be false after creating a control', () => { + const c = new FormControl('value'); + expect(c.dirty).toEqual(false); + }); + + it('should be true after changing the value of the control', () => { + const c = new FormControl('value'); + c.markAsDirty(); + expect(c.dirty).toEqual(true); + }); + }); + + describe('touched', () => { + it('should be false after creating a control', () => { + const c = new FormControl('value'); + expect(c.touched).toEqual(false); + }); + + it('should be true after markAsTouched runs', () => { + const c = new FormControl('value'); + c.markAsTouched(); + expect(c.touched).toEqual(true); + }); + }); + + describe('setValue', () => { + let g: FormGroup, c: FormControl; + beforeEach(() => { + c = new FormControl('oldValue'); + g = new FormGroup({'one': c}); + }); + + it('should set the value of the control', () => { + c.setValue('newValue'); + expect(c.value).toEqual('newValue'); + }); + + it('should invoke ngOnChanges if it is present', () => { + let ngOnChanges: any; + c.registerOnChange((v: any) => ngOnChanges = ['invoked', v]); + + c.setValue('newValue'); + + expect(ngOnChanges).toEqual(['invoked', 'newValue']); + }); + + it('should not invoke on change when explicitly specified', () => { + let onChange: any = null; + c.registerOnChange((v: any) => onChange = ['invoked', v]); + + c.setValue('newValue', {emitModelToViewChange: false}); + + expect(onChange).toBeNull(); + }); + + it('should set the parent', () => { + c.setValue('newValue'); + expect(g.value).toEqual({'one': 'newValue'}); + }); + + it('should not set the parent when explicitly specified', () => { + c.setValue('newValue', {onlySelf: true}); + expect(g.value).toEqual({'one': 'oldValue'}); + }); + + it('should fire an event', fakeAsync(() => { + c.valueChanges.subscribe((value) => { + expect(value).toEqual('newValue'); + }); + + c.setValue('newValue'); + tick(); + })); + + it('should not fire an event when explicitly specified', fakeAsync(() => { + c.valueChanges.subscribe((value) => { + throw 'Should not happen'; + }); + + c.setValue('newValue', {emitEvent: false}); + tick(); + })); + + it('should work on a disabled control', () => { + g.addControl('two', new FormControl('two')); + c.disable(); + c.setValue('new value'); + expect(c.value).toEqual('new value'); + expect(g.value).toEqual({'two': 'two'}); + }); + }); + + describe('patchValue', () => { + let g: FormGroup, c: FormControl; + beforeEach(() => { + c = new FormControl('oldValue'); + g = new FormGroup({'one': c}); + }); + + it('should set the value of the control', () => { + c.patchValue('newValue'); + expect(c.value).toEqual('newValue'); + }); + + it('should invoke ngOnChanges if it is present', () => { + let ngOnChanges: any; + c.registerOnChange((v: any) => ngOnChanges = ['invoked', v]); + + c.patchValue('newValue'); + + expect(ngOnChanges).toEqual(['invoked', 'newValue']); + }); + + it('should not invoke on change when explicitly specified', () => { + let onChange: any = null; + c.registerOnChange((v: any) => onChange = ['invoked', v]); + + c.patchValue('newValue', {emitModelToViewChange: false}); + + expect(onChange).toBeNull(); + }); + + it('should set the parent', () => { + c.patchValue('newValue'); + expect(g.value).toEqual({'one': 'newValue'}); + }); + + it('should not set the parent when explicitly specified', () => { + c.patchValue('newValue', {onlySelf: true}); + expect(g.value).toEqual({'one': 'oldValue'}); + }); + + it('should fire an event', fakeAsync(() => { + c.valueChanges.subscribe((value) => { + expect(value).toEqual('newValue'); + }); + + c.patchValue('newValue'); + tick(); + })); + + it('should not fire an event when explicitly specified', fakeAsync(() => { + c.valueChanges.subscribe((value) => { + throw 'Should not happen'; + }); + + c.patchValue('newValue', {emitEvent: false}); + + tick(); + })); + + it('should patch value on a disabled control', () => { + g.addControl('two', new FormControl('two')); + c.disable(); + + c.patchValue('new value'); + expect(c.value).toEqual('new value'); + expect(g.value).toEqual({'two': 'two'}); + }); + }); + + describe('reset()', () => { + let c: FormControl; + + beforeEach(() => { + c = new FormControl('initial value'); + }); + + it('should reset to a specific value if passed', () => { + c.setValue('new value'); + expect(c.value).toBe('new value'); + + c.reset('initial value'); + expect(c.value).toBe('initial value'); + }); + + it('should not set the parent when explicitly specified', () => { + const g = new FormGroup({'one': c}); + c.patchValue('newValue', {onlySelf: true}); + expect(g.value).toEqual({'one': 'initial value'}); + }); + + it('should reset to a specific value if passed with boxed value', () => { + c.setValue('new value'); + expect(c.value).toBe('new value'); + + c.reset({value: 'initial value', disabled: false}); + expect(c.value).toBe('initial value'); + }); + + it('should clear the control value if no value is passed', () => { + c.setValue('new value'); + expect(c.value).toBe('new value'); + + c.reset(); + expect(c.value).toBe(null); + }); + + it('should update the value of any parent controls with passed value', () => { + const g = new FormGroup({'one': c}); + c.setValue('new value'); + expect(g.value).toEqual({'one': 'new value'}); + + c.reset('initial value'); + expect(g.value).toEqual({'one': 'initial value'}); + }); + + it('should update the value of any parent controls with null value', () => { + const g = new FormGroup({'one': c}); + c.setValue('new value'); + expect(g.value).toEqual({'one': 'new value'}); + + c.reset(); + expect(g.value).toEqual({'one': null}); + }); + + it('should mark the control as pristine', () => { + c.markAsDirty(); + expect(c.pristine).toBe(false); + + c.reset(); + expect(c.pristine).toBe(true); + }); + + it('should set the parent pristine state if all pristine', () => { + const g = new FormGroup({'one': c}); + c.markAsDirty(); + expect(g.pristine).toBe(false); + + c.reset(); + expect(g.pristine).toBe(true); + }); + + it('should not set the parent pristine state if it has other dirty controls', () => { + const c2 = new FormControl('two'); + const g = new FormGroup({'one': c, 'two': c2}); + c.markAsDirty(); + c2.markAsDirty(); + + c.reset(); + expect(g.pristine).toBe(false); + }); + + it('should mark the control as untouched', () => { + c.markAsTouched(); + expect(c.untouched).toBe(false); + + c.reset(); + expect(c.untouched).toBe(true); + }); + + it('should set the parent untouched state if all untouched', () => { + const g = new FormGroup({'one': c}); + c.markAsTouched(); + expect(g.untouched).toBe(false); + + c.reset(); + expect(g.untouched).toBe(true); + }); + + it('should not set the parent untouched state if other touched controls', () => { + const c2 = new FormControl('two'); + const g = new FormGroup({'one': c, 'two': c2}); + c.markAsTouched(); + c2.markAsTouched(); + + c.reset(); + expect(g.untouched).toBe(false); + }); + + it('should retain the disabled state of the control', () => { + c.disable(); + c.reset(); + + expect(c.disabled).toBe(true); + }); + + it('should set disabled state based on boxed value if passed', () => { + c.disable(); + c.reset({value: null, disabled: false}); + + expect(c.disabled).toBe(false); + }); + + describe('reset() events', () => { + let g: FormGroup, c2: FormControl, logger: any[]; + + beforeEach(() => { + c2 = new FormControl('two'); + g = new FormGroup({'one': c, 'two': c2}); + logger = []; + }); + + it('should emit one valueChange event per reset control', () => { + g.valueChanges.subscribe(() => logger.push('group')); + c.valueChanges.subscribe(() => logger.push('control1')); + c2.valueChanges.subscribe(() => logger.push('control2')); + + c.reset(); + expect(logger).toEqual(['control1', 'group']); + }); + + it('should not fire an event when explicitly specified', fakeAsync(() => { + g.valueChanges.subscribe((value) => { + throw 'Should not happen'; + }); + c.valueChanges.subscribe((value) => { + throw 'Should not happen'; + }); + c2.valueChanges.subscribe((value) => { + throw 'Should not happen'; + }); + + c.reset(null, {emitEvent: false}); + + tick(); + })); + + it('should emit one statusChange event per reset control', () => { + g.statusChanges.subscribe(() => logger.push('group')); + c.statusChanges.subscribe(() => logger.push('control1')); + c2.statusChanges.subscribe(() => logger.push('control2')); + + c.reset(); + expect(logger).toEqual(['control1', 'group']); + }); + + it('should emit one statusChange event per disabled control', () => { + g.statusChanges.subscribe(() => logger.push('group')); + c.statusChanges.subscribe(() => logger.push('control1')); + c2.statusChanges.subscribe(() => logger.push('control2')); + + c.reset({value: null, disabled: true}); + expect(logger).toEqual(['control1', 'group']); + }); + }); + }); + + describe('valueChanges & statusChanges', () => { + let c: FormControl; + + beforeEach(() => { + c = new FormControl('old', Validators.required); + }); + + it('should fire an event after the value has been updated', + inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { + c.valueChanges.subscribe({ + next: (value: any) => { + expect(c.value).toEqual('new'); + expect(value).toEqual('new'); + async.done(); + } + }); + c.setValue('new'); + })); + + it('should fire an event after the status has been updated to invalid', fakeAsync(() => { + c.statusChanges.subscribe({ + next: (status: any) => { + expect(c.status).toEqual('INVALID'); + expect(status).toEqual('INVALID'); + } + }); + + c.setValue(''); + tick(); + })); + + it('should fire an event after the status has been updated to pending', fakeAsync(() => { + const c = new FormControl('old', Validators.required, asyncValidator('expected')); + + const log: any[] /** TODO #9100 */ = []; + c.valueChanges.subscribe({next: (value: any) => log.push(`value: '${value}'`)}); + + c.statusChanges.subscribe({next: (status: any) => log.push(`status: '${status}'`)}); + + c.setValue(''); + tick(); + + c.setValue('nonEmpty'); + tick(); + + c.setValue('expected'); + tick(); + + expect(log).toEqual([ + 'value: \'\'', + 'status: \'INVALID\'', + 'value: \'nonEmpty\'', + 'status: \'PENDING\'', + 'status: \'INVALID\'', + 'value: \'expected\'', + 'status: \'PENDING\'', + 'status: \'VALID\'', + ]); + })); + + // TODO: remove the if statement after making observable delivery sync + it('should update set errors and status before emitting an event', + inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { + c.valueChanges.subscribe((value: any /** TODO #9100 */) => { + expect(c.valid).toEqual(false); + expect(c.errors).toEqual({'required': true}); + async.done(); + }); + c.setValue(''); + })); + + it('should return a cold observable', + inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { + c.setValue('will be ignored'); + c.valueChanges.subscribe({ + next: (value: any) => { + expect(value).toEqual('new'); + async.done(); + } + }); + c.setValue('new'); + })); + }); + + describe('setErrors', () => { + it('should set errors on a control', () => { + const c = new FormControl('someValue'); + + c.setErrors({'someError': true}); + + expect(c.valid).toEqual(false); + expect(c.errors).toEqual({'someError': true}); + }); + + it('should reset the errors and validity when the value changes', () => { + const c = new FormControl('someValue', Validators.required); + + c.setErrors({'someError': true}); + c.setValue(''); + + expect(c.errors).toEqual({'required': true}); + }); + + it('should update the parent group\'s validity', () => { + const c = new FormControl('someValue'); + const g = new FormGroup({'one': c}); + + expect(g.valid).toEqual(true); + + c.setErrors({'someError': true}); + + expect(g.valid).toEqual(false); + }); + + it('should not reset parent\'s errors', () => { + const c = new FormControl('someValue'); + const g = new FormGroup({'one': c}); + + g.setErrors({'someGroupError': true}); + c.setErrors({'someError': true}); + + expect(g.errors).toEqual({'someGroupError': true}); + }); + + it('should reset errors when updating a value', () => { + const c = new FormControl('oldValue'); + const g = new FormGroup({'one': c}); + + g.setErrors({'someGroupError': true}); + c.setErrors({'someError': true}); + + c.setValue('newValue'); + + expect(c.errors).toEqual(null); + expect(g.errors).toEqual(null); + }); + }); + + describe('disable() & enable()', () => { + it('should mark the control as disabled', () => { + const c = new FormControl(null); + expect(c.disabled).toBe(false); + expect(c.valid).toBe(true); + + c.disable(); + expect(c.disabled).toBe(true); + expect(c.valid).toBe(false); + + c.enable(); + expect(c.disabled).toBe(false); + expect(c.valid).toBe(true); + }); + + it('should set the control status as disabled', () => { + const c = new FormControl(null); + expect(c.status).toEqual('VALID'); + + c.disable(); + expect(c.status).toEqual('DISABLED'); + + c.enable(); + expect(c.status).toEqual('VALID'); + }); + + it('should retain the original value when disabled', () => { + const c = new FormControl('some value'); + expect(c.value).toEqual('some value'); + + c.disable(); + expect(c.value).toEqual('some value'); + + c.enable(); + expect(c.value).toEqual('some value'); + }); + + it('should keep the disabled control in the group, but return false for contains()', () => { + const c = new FormControl(''); + const g = new FormGroup({'one': c}); + + expect(g.get('one')).toBeDefined(); + expect(g.contains('one')).toBe(true); + + c.disable(); + expect(g.get('one')).toBeDefined(); + expect(g.contains('one')).toBe(false); + }); + + it('should mark the parent group disabled if all controls are disabled', () => { + const c = new FormControl(); + const c2 = new FormControl(); + const g = new FormGroup({'one': c, 'two': c2}); + expect(g.enabled).toBe(true); + + c.disable(); + expect(g.enabled).toBe(true); + + c2.disable(); + expect(g.enabled).toBe(false); + + c.enable(); + expect(g.enabled).toBe(true); + }); + + it('should update the parent group value when child control status changes', () => { + const c = new FormControl('one'); + const c2 = new FormControl('two'); + const g = new FormGroup({'one': c, 'two': c2}); + expect(g.value).toEqual({'one': 'one', 'two': 'two'}); + + c.disable(); + expect(g.value).toEqual({'two': 'two'}); + + c2.disable(); + expect(g.value).toEqual({'one': 'one', 'two': 'two'}); + + c.enable(); + expect(g.value).toEqual({'one': 'one'}); + }); + + it('should mark the parent array disabled if all controls are disabled', () => { + const c = new FormControl(); + const c2 = new FormControl(); + const a = new FormArray([c, c2]); + expect(a.enabled).toBe(true); + + c.disable(); + expect(a.enabled).toBe(true); + + c2.disable(); + expect(a.enabled).toBe(false); + + c.enable(); + expect(a.enabled).toBe(true); + }); + + it('should update the parent array value when child control status changes', () => { + const c = new FormControl('one'); + const c2 = new FormControl('two'); + const a = new FormArray([c, c2]); + expect(a.value).toEqual(['one', 'two']); + + c.disable(); + expect(a.value).toEqual(['two']); + + c2.disable(); + expect(a.value).toEqual(['one', 'two']); + + c.enable(); + expect(a.value).toEqual(['one']); + }); + + it('should ignore disabled array controls when determining dirtiness', () => { + const c = new FormControl('one'); + const c2 = new FormControl('two'); + const a = new FormArray([c, c2]); + c.markAsDirty(); + expect(a.dirty).toBe(true); + + c.disable(); + expect(c.dirty).toBe(true); + expect(a.dirty).toBe(false); + + c.enable(); + expect(a.dirty).toBe(true); + }); + + it('should not make a dirty array not dirty when disabling controls', () => { + const c = new FormControl('one'); + const c2 = new FormControl('two'); + const a = new FormArray([c, c2]); + + a.markAsDirty(); + expect(a.dirty).toBe(true); + expect(c.dirty).toBe(false); + + c.disable(); + expect(a.dirty).toBe(true); + + c.enable(); + expect(a.dirty).toBe(true); + }); + + it('should ignore disabled controls in validation', () => { + const c = new FormControl(null, Validators.required); + const c2 = new FormControl(null); + const g = new FormGroup({one: c, two: c2}); + expect(g.valid).toBe(false); + + c.disable(); + expect(g.valid).toBe(true); + + c.enable(); + expect(g.valid).toBe(false); + }); + + it('should ignore disabled controls when serializing value in a group', () => { + const c = new FormControl('one'); + const c2 = new FormControl('two'); + const g = new FormGroup({one: c, two: c2}); + expect(g.value).toEqual({one: 'one', two: 'two'}); + + c.disable(); + expect(g.value).toEqual({two: 'two'}); + + c.enable(); + expect(g.value).toEqual({one: 'one', two: 'two'}); + }); + + it('should ignore disabled controls when serializing value in an array', () => { + const c = new FormControl('one'); + const c2 = new FormControl('two'); + const a = new FormArray([c, c2]); + expect(a.value).toEqual(['one', 'two']); + + c.disable(); + expect(a.value).toEqual(['two']); + + c.enable(); + expect(a.value).toEqual(['one', 'two']); + }); + + it('should ignore disabled controls when determining dirtiness', () => { + const c = new FormControl('one'); + const c2 = new FormControl('two'); + const g = new FormGroup({one: c, two: c2}); + c.markAsDirty(); + expect(g.dirty).toBe(true); + + c.disable(); + expect(c.dirty).toBe(true); + expect(g.dirty).toBe(false); + + c.enable(); + expect(g.dirty).toBe(true); + }); + + it('should not make a dirty group not dirty when disabling controls', () => { + const c = new FormControl('one'); + const c2 = new FormControl('two'); + const g = new FormGroup({one: c, two: c2}); + + g.markAsDirty(); + expect(g.dirty).toBe(true); + expect(c.dirty).toBe(false); + + c.disable(); + expect(g.dirty).toBe(true); + + c.enable(); + expect(g.dirty).toBe(true); + }); + + it('should ignore disabled controls when determining touched state', () => { + const c = new FormControl('one'); + const c2 = new FormControl('two'); + const g = new FormGroup({one: c, two: c2}); + c.markAsTouched(); + expect(g.touched).toBe(true); + + c.disable(); + expect(c.touched).toBe(true); + expect(g.touched).toBe(false); + + c.enable(); + expect(g.touched).toBe(true); + }); + + it('should not run validators on disabled controls', () => { + const validator = jasmine.createSpy('validator'); + const c = new FormControl('', validator); + expect(validator.calls.count()).toEqual(1); + + c.disable(); + expect(validator.calls.count()).toEqual(1); + + c.setValue('value'); + expect(validator.calls.count()).toEqual(1); + + c.enable(); + expect(validator.calls.count()).toEqual(2); + }); + + describe('disabled errors', () => { + it('should clear out the errors when disabled', () => { + const c = new FormControl('', Validators.required); + expect(c.errors).toEqual({required: true}); + + c.disable(); + expect(c.errors).toEqual(null); + + c.enable(); + expect(c.errors).toEqual({required: true}); + }); + + it('should clear out async errors when disabled', fakeAsync(() => { + const c = new FormControl('', null!, asyncValidator('expected')); + tick(); + expect(c.errors).toEqual({'async': true}); + + c.disable(); + expect(c.errors).toEqual(null); + + c.enable(); + tick(); + expect(c.errors).toEqual({'async': true}); + })); + }); + + describe('disabled events', () => { + let logger: string[]; + let c: FormControl; + let g: FormGroup; + + beforeEach(() => { + logger = []; + c = new FormControl('', Validators.required); + g = new FormGroup({one: c}); + }); + + it('should emit a statusChange event when disabled status changes', () => { + c.statusChanges.subscribe((status: string) => logger.push(status)); + + c.disable(); + expect(logger).toEqual(['DISABLED']); + + c.enable(); + expect(logger).toEqual(['DISABLED', 'INVALID']); + }); + + it('should emit status change events in correct order', () => { + c.statusChanges.subscribe(() => logger.push('control')); + g.statusChanges.subscribe(() => logger.push('group')); + + c.disable(); + expect(logger).toEqual(['control', 'group']); + }); + + it('should throw when sync validator passed into async validator param', () => { + const fn = () => new FormControl('', syncValidator, syncValidator); + // test for the specific error since without the error check it would still throw an error + // but + // not a meaningful one + expect(fn).toThrowError(`Expected validator to return Promise or Observable.`); + }); + + it('should not emit value change events when emitEvent = false', () => { + c.valueChanges.subscribe(() => logger.push('control')); + g.valueChanges.subscribe(() => logger.push('group')); + + c.disable({emitEvent: false}); + expect(logger).toEqual([]); + c.enable({emitEvent: false}); + expect(logger).toEqual([]); + }); + + it('should not emit status change events when emitEvent = false', () => { + c.statusChanges.subscribe(() => logger.push('control')); + g.statusChanges.subscribe(() => logger.push('form')); + + c.disable({emitEvent: false}); + expect(logger).toEqual([]); + c.enable({emitEvent: false}); + expect(logger).toEqual([]); + }); + }); + }); + describe('pending', () => { + let c: FormControl; + + beforeEach(() => { + c = new FormControl('value'); + }); + + it('should be false after creating a control', () => { + expect(c.pending).toEqual(false); + }); + + it('should be true after changing the value of the control', () => { + c.markAsPending(); + expect(c.pending).toEqual(true); + }); + + describe('status change events', () => { + let logger: string[]; + + beforeEach(() => { + logger = []; + c.statusChanges.subscribe((status) => logger.push(status)); + }); + + it('should emit event after marking control as pending', () => { + c.markAsPending(); + expect(logger).toEqual(['PENDING']); + }); + + it('should not emit event when emitEvent = false', () => { + c.markAsPending({emitEvent: false}); + expect(logger).toEqual([]); + }); + }); + }); +}); })(); diff --git a/packages/forms/test/form_group_spec.ts b/packages/forms/test/form_group_spec.ts index e72e5cfb48..ec90568a2e 100644 --- a/packages/forms/test/form_group_spec.ts +++ b/packages/forms/test/form_group_spec.ts @@ -10,1346 +10,1368 @@ import {EventEmitter} from '@angular/core'; import {async, fakeAsync, tick} from '@angular/core/testing'; import {AsyncTestCompleter, beforeEach, describe, inject, it} from '@angular/core/testing/src/testing_internal'; import {AbstractControl, FormArray, FormControl, FormGroup, ValidationErrors, Validators} from '@angular/forms'; -import {of } from 'rxjs'; +import {of} from 'rxjs'; (function() { - function simpleValidator(c: AbstractControl): ValidationErrors|null { - return c.get('one') !.value === 'correct' ? null : {'broken': true}; - } +function simpleValidator(c: AbstractControl): ValidationErrors|null { + return c.get('one')!.value === 'correct' ? null : {'broken': true}; +} - function asyncValidator(expected: string, timeouts = {}) { - return (c: AbstractControl) => { - let resolve: (result: any) => void = undefined !; - const promise = new Promise(res => { resolve = res; }); - const t = (timeouts as any)[c.value] != null ? (timeouts as any)[c.value] : 0; - const res = c.value != expected ? {'async': true} : null; +function asyncValidator(expected: string, timeouts = {}) { + return (c: AbstractControl) => { + let resolve: (result: any) => void = undefined!; + const promise = new Promise(res => { + resolve = res; + }); + const t = (timeouts as any)[c.value] != null ? (timeouts as any)[c.value] : 0; + const res = c.value != expected ? {'async': true} : null; - if (t == 0) { + if (t == 0) { + resolve(res); + } else { + setTimeout(() => { resolve(res); - } else { - setTimeout(() => { resolve(res); }, t); - } + }, t); + } - return promise; - }; - } + return promise; + }; +} - function asyncValidatorReturningObservable(c: AbstractControl) { - const e = new EventEmitter(); - Promise.resolve(null).then(() => { e.emit({'async': true}); }); - return e; - } +function asyncValidatorReturningObservable(c: AbstractControl) { + const e = new EventEmitter(); + Promise.resolve(null).then(() => { + e.emit({'async': true}); + }); + return e; +} - function otherObservableValidator() { return of ({'other': true}); } +function otherObservableValidator() { + return of({'other': true}); +} - describe('FormGroup', () => { - describe('value', () => { - it('should be the reduced value of the child controls', () => { - const g = new FormGroup({'one': new FormControl('111'), 'two': new FormControl('222')}); - expect(g.value).toEqual({'one': '111', 'two': '222'}); - }); - - it('should be empty when there are no child controls', () => { - const g = new FormGroup({}); - expect(g.value).toEqual({}); - }); - - it('should support nested groups', () => { - const g = new FormGroup({ - 'one': new FormControl('111'), - 'nested': new FormGroup({'two': new FormControl('222')}) - }); - expect(g.value).toEqual({'one': '111', 'nested': {'two': '222'}}); - - ((g.get('nested.two'))).setValue('333'); - - expect(g.value).toEqual({'one': '111', 'nested': {'two': '333'}}); - }); +describe('FormGroup', () => { + describe('value', () => { + it('should be the reduced value of the child controls', () => { + const g = new FormGroup({'one': new FormControl('111'), 'two': new FormControl('222')}); + expect(g.value).toEqual({'one': '111', 'two': '222'}); }); - describe('getRawValue', () => { - let fg: FormGroup; - - it('should work with nested form groups/arrays', () => { - fg = new FormGroup({ - 'c1': new FormControl('v1'), - 'group': new FormGroup({'c2': new FormControl('v2'), 'c3': new FormControl('v3')}), - 'array': new FormArray([new FormControl('v4'), new FormControl('v5')]) - }); - fg.get('group') !.get('c3') !.disable(); - (fg.get('array') as FormArray).at(1).disable(); - - expect(fg.getRawValue()) - .toEqual({'c1': 'v1', 'group': {'c2': 'v2', 'c3': 'v3'}, 'array': ['v4', 'v5']}); - }); - + it('should be empty when there are no child controls', () => { + const g = new FormGroup({}); + expect(g.value).toEqual({}); }); - describe('markAllAsTouched', () => { - it('should mark all descendants as touched', () => { - const formGroup: FormGroup = new FormGroup({ - 'c1': new FormControl('v1'), - 'group': new FormGroup({'c2': new FormControl('v2'), 'c3': new FormControl('v3')}), - 'array': new FormArray([ - new FormControl('v4'), new FormControl('v5'), - new FormGroup({'c4': new FormControl('v4')}) - ]) - }); - - expect(formGroup.touched).toBe(false); - - const control1 = formGroup.get('c1') as FormControl; - - expect(control1.touched).toBe(false); - - const innerGroup = formGroup.get('group') as FormGroup; - - expect(innerGroup.touched).toBe(false); - - const innerGroupFirstChildCtrl = innerGroup.get('c2') as FormControl; - - expect(innerGroupFirstChildCtrl.touched).toBe(false); - - formGroup.markAllAsTouched(); - - expect(formGroup.touched).toBe(true); - - expect(control1.touched).toBe(true); - - expect(innerGroup.touched).toBe(true); - - expect(innerGroupFirstChildCtrl.touched).toBe(true); - - const innerGroupSecondChildCtrl = innerGroup.get('c3') as FormControl; - - expect(innerGroupSecondChildCtrl.touched).toBe(true); - - const array = formGroup.get('array') as FormArray; - - expect(array.touched).toBe(true); - - const arrayFirstChildCtrl = array.at(0) as FormControl; - - expect(arrayFirstChildCtrl.touched).toBe(true); - - const arraySecondChildCtrl = array.at(1) as FormControl; - - expect(arraySecondChildCtrl.touched).toBe(true); - - const arrayFirstChildGroup = array.at(2) as FormGroup; - - expect(arrayFirstChildGroup.touched).toBe(true); - - const arrayFirstChildGroupFirstChildCtrl = arrayFirstChildGroup.get('c4') as FormControl; - - expect(arrayFirstChildGroupFirstChildCtrl.touched).toBe(true); + it('should support nested groups', () => { + const g = new FormGroup({ + 'one': new FormControl('111'), + 'nested': new FormGroup({'two': new FormControl('222')}) }); + expect(g.value).toEqual({'one': '111', 'nested': {'two': '222'}}); + + ((g.get('nested.two'))).setValue('333'); + + expect(g.value).toEqual({'one': '111', 'nested': {'two': '333'}}); + }); + }); + + describe('getRawValue', () => { + let fg: FormGroup; + + it('should work with nested form groups/arrays', () => { + fg = new FormGroup({ + 'c1': new FormControl('v1'), + 'group': new FormGroup({'c2': new FormControl('v2'), 'c3': new FormControl('v3')}), + 'array': new FormArray([new FormControl('v4'), new FormControl('v5')]) + }); + fg.get('group')!.get('c3')!.disable(); + (fg.get('array') as FormArray).at(1).disable(); + + expect(fg.getRawValue()) + .toEqual({'c1': 'v1', 'group': {'c2': 'v2', 'c3': 'v3'}, 'array': ['v4', 'v5']}); + }); + }); + + describe('markAllAsTouched', () => { + it('should mark all descendants as touched', () => { + const formGroup: FormGroup = new FormGroup({ + 'c1': new FormControl('v1'), + 'group': new FormGroup({'c2': new FormControl('v2'), 'c3': new FormControl('v3')}), + 'array': new FormArray([ + new FormControl('v4'), new FormControl('v5'), new FormGroup({'c4': new FormControl('v4')}) + ]) + }); + + expect(formGroup.touched).toBe(false); + + const control1 = formGroup.get('c1') as FormControl; + + expect(control1.touched).toBe(false); + + const innerGroup = formGroup.get('group') as FormGroup; + + expect(innerGroup.touched).toBe(false); + + const innerGroupFirstChildCtrl = innerGroup.get('c2') as FormControl; + + expect(innerGroupFirstChildCtrl.touched).toBe(false); + + formGroup.markAllAsTouched(); + + expect(formGroup.touched).toBe(true); + + expect(control1.touched).toBe(true); + + expect(innerGroup.touched).toBe(true); + + expect(innerGroupFirstChildCtrl.touched).toBe(true); + + const innerGroupSecondChildCtrl = innerGroup.get('c3') as FormControl; + + expect(innerGroupSecondChildCtrl.touched).toBe(true); + + const array = formGroup.get('array') as FormArray; + + expect(array.touched).toBe(true); + + const arrayFirstChildCtrl = array.at(0) as FormControl; + + expect(arrayFirstChildCtrl.touched).toBe(true); + + const arraySecondChildCtrl = array.at(1) as FormControl; + + expect(arraySecondChildCtrl.touched).toBe(true); + + const arrayFirstChildGroup = array.at(2) as FormGroup; + + expect(arrayFirstChildGroup.touched).toBe(true); + + const arrayFirstChildGroupFirstChildCtrl = arrayFirstChildGroup.get('c4') as FormControl; + + expect(arrayFirstChildGroupFirstChildCtrl.touched).toBe(true); + }); + }); + + describe('adding and removing controls', () => { + it('should update value and validity when control is added', () => { + const g = new FormGroup({'one': new FormControl('1')}); + expect(g.value).toEqual({'one': '1'}); + expect(g.valid).toBe(true); + + g.addControl('two', new FormControl('2', Validators.minLength(10))); + + expect(g.value).toEqual({'one': '1', 'two': '2'}); + expect(g.valid).toBe(false); }); - describe('adding and removing controls', () => { - it('should update value and validity when control is added', () => { - const g = new FormGroup({'one': new FormControl('1')}); - expect(g.value).toEqual({'one': '1'}); - expect(g.valid).toBe(true); + it('should update value and validity when control is removed', () => { + const g = new FormGroup( + {'one': new FormControl('1'), 'two': new FormControl('2', Validators.minLength(10))}); + expect(g.value).toEqual({'one': '1', 'two': '2'}); + expect(g.valid).toBe(false); - g.addControl('two', new FormControl('2', Validators.minLength(10))); + g.removeControl('two'); - expect(g.value).toEqual({'one': '1', 'two': '2'}); - expect(g.valid).toBe(false); - }); + expect(g.value).toEqual({'one': '1'}); + expect(g.valid).toBe(true); + }); + }); - it('should update value and validity when control is removed', () => { - const g = new FormGroup( - {'one': new FormControl('1'), 'two': new FormControl('2', Validators.minLength(10))}); - expect(g.value).toEqual({'one': '1', 'two': '2'}); - expect(g.valid).toBe(false); + describe('dirty', () => { + let c: FormControl, g: FormGroup; - g.removeControl('two'); - - expect(g.value).toEqual({'one': '1'}); - expect(g.valid).toBe(true); - }); + beforeEach(() => { + c = new FormControl('value'); + g = new FormGroup({'one': c}); }); - describe('dirty', () => { - let c: FormControl, g: FormGroup; + it('should be false after creating a control', () => { + expect(g.dirty).toEqual(false); + }); + + it('should be true after changing the value of the control', () => { + c.markAsDirty(); + + expect(g.dirty).toEqual(true); + }); + }); + + + describe('touched', () => { + let c: FormControl, g: FormGroup; + + beforeEach(() => { + c = new FormControl('value'); + g = new FormGroup({'one': c}); + }); + + it('should be false after creating a control', () => { + expect(g.touched).toEqual(false); + }); + + it('should be true after control is marked as touched', () => { + c.markAsTouched(); + + expect(g.touched).toEqual(true); + }); + }); + + describe('setValue', () => { + let c: FormControl, c2: FormControl, g: FormGroup; + + beforeEach(() => { + c = new FormControl(''); + c2 = new FormControl(''); + g = new FormGroup({'one': c, 'two': c2}); + }); + + it('should set its own value', () => { + g.setValue({'one': 'one', 'two': 'two'}); + expect(g.value).toEqual({'one': 'one', 'two': 'two'}); + }); + + it('should set child values', () => { + g.setValue({'one': 'one', 'two': 'two'}); + expect(c.value).toEqual('one'); + expect(c2.value).toEqual('two'); + }); + + it('should set child control values if disabled', () => { + c2.disable(); + g.setValue({'one': 'one', 'two': 'two'}); + expect(c2.value).toEqual('two'); + expect(g.value).toEqual({'one': 'one'}); + expect(g.getRawValue()).toEqual({'one': 'one', 'two': 'two'}); + }); + + it('should set group value if group is disabled', () => { + g.disable(); + g.setValue({'one': 'one', 'two': 'two'}); + expect(c.value).toEqual('one'); + expect(c2.value).toEqual('two'); + + expect(g.value).toEqual({'one': 'one', 'two': 'two'}); + }); + + it('should set parent values', () => { + const form = new FormGroup({'parent': g}); + g.setValue({'one': 'one', 'two': 'two'}); + expect(form.value).toEqual({'parent': {'one': 'one', 'two': 'two'}}); + }); + + it('should not update the parent when explicitly specified', () => { + const form = new FormGroup({'parent': g}); + g.setValue({'one': 'one', 'two': 'two'}, {onlySelf: true}); + + expect(form.value).toEqual({parent: {'one': '', 'two': ''}}); + }); + + it('should throw if fields are missing from supplied value (subset)', () => { + expect(() => g.setValue({ + 'one': 'one' + })).toThrowError(new RegExp(`Must supply a value for form control with name: 'two'`)); + }); + + it('should throw if a value is provided for a missing control (superset)', () => { + expect(() => g.setValue({'one': 'one', 'two': 'two', 'three': 'three'})) + .toThrowError(new RegExp(`Cannot find form control with name: three`)); + }); + + it('should throw if a value is not provided for a disabled control', () => { + c2.disable(); + expect(() => g.setValue({ + 'one': 'one' + })).toThrowError(new RegExp(`Must supply a value for form control with name: 'two'`)); + }); + + it('should throw if no controls are set yet', () => { + const empty = new FormGroup({}); + expect(() => empty.setValue({ + 'one': 'one' + })).toThrowError(new RegExp(`no form controls registered with this group`)); + }); + + describe('setValue() events', () => { + let form: FormGroup; + let logger: any[]; beforeEach(() => { - c = new FormControl('value'); - g = new FormGroup({'one': c}); + form = new FormGroup({'parent': g}); + logger = []; }); - it('should be false after creating a control', () => { expect(g.dirty).toEqual(false); }); + it('should emit one valueChange event per control', () => { + form.valueChanges.subscribe(() => logger.push('form')); + g.valueChanges.subscribe(() => logger.push('group')); + c.valueChanges.subscribe(() => logger.push('control1')); + c2.valueChanges.subscribe(() => logger.push('control2')); - it('should be true after changing the value of the control', () => { - c.markAsDirty(); - - expect(g.dirty).toEqual(true); - }); - }); - - - describe('touched', () => { - let c: FormControl, g: FormGroup; - - beforeEach(() => { - c = new FormControl('value'); - g = new FormGroup({'one': c}); - }); - - it('should be false after creating a control', () => { expect(g.touched).toEqual(false); }); - - it('should be true after control is marked as touched', () => { - c.markAsTouched(); - - expect(g.touched).toEqual(true); - }); - }); - - describe('setValue', () => { - let c: FormControl, c2: FormControl, g: FormGroup; - - beforeEach(() => { - c = new FormControl(''); - c2 = new FormControl(''); - g = new FormGroup({'one': c, 'two': c2}); - }); - - it('should set its own value', () => { g.setValue({'one': 'one', 'two': 'two'}); - expect(g.value).toEqual({'one': 'one', 'two': 'two'}); + expect(logger).toEqual(['control1', 'control2', 'group', 'form']); }); - it('should set child values', () => { - g.setValue({'one': 'one', 'two': 'two'}); - expect(c.value).toEqual('one'); - expect(c2.value).toEqual('two'); - }); - - it('should set child control values if disabled', () => { - c2.disable(); - g.setValue({'one': 'one', 'two': 'two'}); - expect(c2.value).toEqual('two'); - expect(g.value).toEqual({'one': 'one'}); - expect(g.getRawValue()).toEqual({'one': 'one', 'two': 'two'}); - }); - - it('should set group value if group is disabled', () => { - g.disable(); - g.setValue({'one': 'one', 'two': 'two'}); - expect(c.value).toEqual('one'); - expect(c2.value).toEqual('two'); - - expect(g.value).toEqual({'one': 'one', 'two': 'two'}); - }); - - it('should set parent values', () => { - const form = new FormGroup({'parent': g}); - g.setValue({'one': 'one', 'two': 'two'}); - expect(form.value).toEqual({'parent': {'one': 'one', 'two': 'two'}}); - }); - - it('should not update the parent when explicitly specified', () => { - const form = new FormGroup({'parent': g}); - g.setValue({'one': 'one', 'two': 'two'}, {onlySelf: true}); - - expect(form.value).toEqual({parent: {'one': '', 'two': ''}}); - }); - - it('should throw if fields are missing from supplied value (subset)', () => { - expect(() => g.setValue({ - 'one': 'one' - })).toThrowError(new RegExp(`Must supply a value for form control with name: 'two'`)); - }); - - it('should throw if a value is provided for a missing control (superset)', () => { - expect(() => g.setValue({'one': 'one', 'two': 'two', 'three': 'three'})) - .toThrowError(new RegExp(`Cannot find form control with name: three`)); - }); - - it('should throw if a value is not provided for a disabled control', () => { - c2.disable(); - expect(() => g.setValue({ - 'one': 'one' - })).toThrowError(new RegExp(`Must supply a value for form control with name: 'two'`)); - }); - - it('should throw if no controls are set yet', () => { - const empty = new FormGroup({}); - expect(() => empty.setValue({ - 'one': 'one' - })).toThrowError(new RegExp(`no form controls registered with this group`)); - }); - - describe('setValue() events', () => { - let form: FormGroup; - let logger: any[]; - - beforeEach(() => { - form = new FormGroup({'parent': g}); - logger = []; - }); - - it('should emit one valueChange event per control', () => { - form.valueChanges.subscribe(() => logger.push('form')); - g.valueChanges.subscribe(() => logger.push('group')); - c.valueChanges.subscribe(() => logger.push('control1')); - c2.valueChanges.subscribe(() => logger.push('control2')); - - g.setValue({'one': 'one', 'two': 'two'}); - expect(logger).toEqual(['control1', 'control2', 'group', 'form']); - }); - - it('should not fire an event when explicitly specified', fakeAsync(() => { - form.valueChanges.subscribe((value) => { throw 'Should not happen'; }); - g.valueChanges.subscribe((value) => { throw 'Should not happen'; }); - c.valueChanges.subscribe((value) => { throw 'Should not happen'; }); - - g.setValue({'one': 'one', 'two': 'two'}, {emitEvent: false}); - tick(); - })); - - it('should emit one statusChange event per control', () => { - form.statusChanges.subscribe(() => logger.push('form')); - g.statusChanges.subscribe(() => logger.push('group')); - c.statusChanges.subscribe(() => logger.push('control1')); - c2.statusChanges.subscribe(() => logger.push('control2')); - - g.setValue({'one': 'one', 'two': 'two'}); - expect(logger).toEqual(['control1', 'control2', 'group', 'form']); - }); - }); - }); - - describe('patchValue', () => { - let c: FormControl, c2: FormControl, g: FormGroup; - - beforeEach(() => { - c = new FormControl(''); - c2 = new FormControl(''); - g = new FormGroup({'one': c, 'two': c2}); - }); - - it('should set its own value', () => { - g.patchValue({'one': 'one', 'two': 'two'}); - expect(g.value).toEqual({'one': 'one', 'two': 'two'}); - }); - - it('should set child values', () => { - g.patchValue({'one': 'one', 'two': 'two'}); - expect(c.value).toEqual('one'); - expect(c2.value).toEqual('two'); - }); - - it('should patch disabled control values', () => { - c2.disable(); - g.patchValue({'one': 'one', 'two': 'two'}); - expect(c2.value).toEqual('two'); - expect(g.value).toEqual({'one': 'one'}); - expect(g.getRawValue()).toEqual({'one': 'one', 'two': 'two'}); - }); - - it('should patch disabled control groups', () => { - g.disable(); - g.patchValue({'one': 'one', 'two': 'two'}); - expect(c.value).toEqual('one'); - expect(c2.value).toEqual('two'); - expect(g.value).toEqual({'one': 'one', 'two': 'two'}); - }); - - it('should set parent values', () => { - const form = new FormGroup({'parent': g}); - g.patchValue({'one': 'one', 'two': 'two'}); - expect(form.value).toEqual({'parent': {'one': 'one', 'two': 'two'}}); - }); - - it('should not update the parent when explicitly specified', () => { - const form = new FormGroup({'parent': g}); - g.patchValue({'one': 'one', 'two': 'two'}, {onlySelf: true}); - - expect(form.value).toEqual({parent: {'one': '', 'two': ''}}); - }); - - it('should ignore fields that are missing from supplied value (subset)', () => { - g.patchValue({'one': 'one'}); - expect(g.value).toEqual({'one': 'one', 'two': ''}); - }); - - it('should not ignore fields that are null', () => { - g.patchValue({'one': null}); - expect(g.value).toEqual({'one': null, 'two': ''}); - }); - - it('should ignore any value provided for a missing control (superset)', () => { - g.patchValue({'three': 'three'}); - expect(g.value).toEqual({'one': '', 'two': ''}); - }); - - describe('patchValue() events', () => { - let form: FormGroup; - let logger: any[]; - - beforeEach(() => { - form = new FormGroup({'parent': g}); - logger = []; - }); - - it('should emit one valueChange event per control', () => { - form.valueChanges.subscribe(() => logger.push('form')); - g.valueChanges.subscribe(() => logger.push('group')); - c.valueChanges.subscribe(() => logger.push('control1')); - c2.valueChanges.subscribe(() => logger.push('control2')); - - g.patchValue({'one': 'one', 'two': 'two'}); - expect(logger).toEqual(['control1', 'control2', 'group', 'form']); - }); - - it('should not emit valueChange events for skipped controls', () => { - form.valueChanges.subscribe(() => logger.push('form')); - g.valueChanges.subscribe(() => logger.push('group')); - c.valueChanges.subscribe(() => logger.push('control1')); - c2.valueChanges.subscribe(() => logger.push('control2')); - - g.patchValue({'one': 'one'}); - expect(logger).toEqual(['control1', 'group', 'form']); - }); - - it('should not fire an event when explicitly specified', fakeAsync(() => { - form.valueChanges.subscribe((value) => { throw 'Should not happen'; }); - g.valueChanges.subscribe((value) => { throw 'Should not happen'; }); - c.valueChanges.subscribe((value) => { throw 'Should not happen'; }); - - g.patchValue({'one': 'one', 'two': 'two'}, {emitEvent: false}); - tick(); - })); - - it('should emit one statusChange event per control', () => { - form.statusChanges.subscribe(() => logger.push('form')); - g.statusChanges.subscribe(() => logger.push('group')); - c.statusChanges.subscribe(() => logger.push('control1')); - c2.statusChanges.subscribe(() => logger.push('control2')); - - g.patchValue({'one': 'one', 'two': 'two'}); - expect(logger).toEqual(['control1', 'control2', 'group', 'form']); - }); - }); - }); - - describe('reset()', () => { - let c: FormControl, c2: FormControl, g: FormGroup; - - beforeEach(() => { - c = new FormControl('initial value'); - c2 = new FormControl(''); - g = new FormGroup({'one': c, 'two': c2}); - }); - - it('should set its own value if value passed', () => { - g.setValue({'one': 'new value', 'two': 'new value'}); - - g.reset({'one': 'initial value', 'two': ''}); - expect(g.value).toEqual({'one': 'initial value', 'two': ''}); - }); - - it('should set its own value if boxed value passed', () => { - g.setValue({'one': 'new value', 'two': 'new value'}); - - g.reset({'one': {value: 'initial value', disabled: false}, 'two': ''}); - expect(g.value).toEqual({'one': 'initial value', 'two': ''}); - }); - - it('should clear its own value if no value passed', () => { - g.setValue({'one': 'new value', 'two': 'new value'}); - - g.reset(); - expect(g.value).toEqual({'one': null, 'two': null}); - }); - - it('should set the value of each of its child controls if value passed', () => { - g.setValue({'one': 'new value', 'two': 'new value'}); - - g.reset({'one': 'initial value', 'two': ''}); - expect(c.value).toBe('initial value'); - expect(c2.value).toBe(''); - }); - - it('should clear the value of each of its child controls if no value passed', () => { - g.setValue({'one': 'new value', 'two': 'new value'}); - - g.reset(); - expect(c.value).toBe(null); - expect(c2.value).toBe(null); - }); - - it('should set the value of its parent if value passed', () => { - const form = new FormGroup({'g': g}); - g.setValue({'one': 'new value', 'two': 'new value'}); - - g.reset({'one': 'initial value', 'two': ''}); - expect(form.value).toEqual({'g': {'one': 'initial value', 'two': ''}}); - }); - - it('should clear the value of its parent if no value passed', () => { - const form = new FormGroup({'g': g}); - g.setValue({'one': 'new value', 'two': 'new value'}); - - g.reset(); - expect(form.value).toEqual({'g': {'one': null, 'two': null}}); - }); - - it('should not update the parent when explicitly specified', () => { - const form = new FormGroup({'g': g}); - g.reset({'one': 'new value', 'two': 'new value'}, {onlySelf: true}); - - expect(form.value).toEqual({g: {'one': 'initial value', 'two': ''}}); - }); - - it('should mark itself as pristine', () => { - g.markAsDirty(); - expect(g.pristine).toBe(false); - - g.reset(); - expect(g.pristine).toBe(true); - }); - - it('should mark all child controls as pristine', () => { - c.markAsDirty(); - c2.markAsDirty(); - expect(c.pristine).toBe(false); - expect(c2.pristine).toBe(false); - - g.reset(); - expect(c.pristine).toBe(true); - expect(c2.pristine).toBe(true); - }); - - it('should mark the parent as pristine if all siblings pristine', () => { - const c3 = new FormControl(''); - const form = new FormGroup({'g': g, 'c3': c3}); - - g.markAsDirty(); - expect(form.pristine).toBe(false); - - g.reset(); - expect(form.pristine).toBe(true); - }); - - it('should not mark the parent pristine if any dirty siblings', () => { - const c3 = new FormControl(''); - const form = new FormGroup({'g': g, 'c3': c3}); - - g.markAsDirty(); - c3.markAsDirty(); - expect(form.pristine).toBe(false); - - g.reset(); - expect(form.pristine).toBe(false); - }); - - it('should mark itself as untouched', () => { - g.markAsTouched(); - expect(g.untouched).toBe(false); - - g.reset(); - expect(g.untouched).toBe(true); - }); - - it('should mark all child controls as untouched', () => { - c.markAsTouched(); - c2.markAsTouched(); - expect(c.untouched).toBe(false); - expect(c2.untouched).toBe(false); - - g.reset(); - expect(c.untouched).toBe(true); - expect(c2.untouched).toBe(true); - }); - - it('should mark the parent untouched if all siblings untouched', () => { - const c3 = new FormControl(''); - const form = new FormGroup({'g': g, 'c3': c3}); - - g.markAsTouched(); - expect(form.untouched).toBe(false); - - g.reset(); - expect(form.untouched).toBe(true); - }); - - it('should not mark the parent untouched if any touched siblings', () => { - const c3 = new FormControl(''); - const form = new FormGroup({'g': g, 'c3': c3}); - - g.markAsTouched(); - c3.markAsTouched(); - expect(form.untouched).toBe(false); - - g.reset(); - expect(form.untouched).toBe(false); - }); - - it('should retain previous disabled state', () => { - g.disable(); - g.reset(); - - expect(g.disabled).toBe(true); - }); - - it('should set child disabled state if boxed value passed', () => { - g.disable(); - g.reset({'one': {value: '', disabled: false}, 'two': ''}); - - expect(c.disabled).toBe(false); - expect(g.disabled).toBe(false); - }); - - describe('reset() events', () => { - let form: FormGroup, c3: FormControl, logger: any[]; - - beforeEach(() => { - c3 = new FormControl(''); - form = new FormGroup({'g': g, 'c3': c3}); - logger = []; - }); - - it('should emit one valueChange event per reset control', () => { - form.valueChanges.subscribe(() => logger.push('form')); - g.valueChanges.subscribe(() => logger.push('group')); - c.valueChanges.subscribe(() => logger.push('control1')); - c2.valueChanges.subscribe(() => logger.push('control2')); - c3.valueChanges.subscribe(() => logger.push('control3')); - - g.reset(); - expect(logger).toEqual(['control1', 'control2', 'group', 'form']); - }); - - it('should not fire an event when explicitly specified', fakeAsync(() => { - form.valueChanges.subscribe((value) => { throw 'Should not happen'; }); - g.valueChanges.subscribe((value) => { throw 'Should not happen'; }); - c.valueChanges.subscribe((value) => { throw 'Should not happen'; }); - - g.reset({}, {emitEvent: false}); - tick(); - })); - - it('should emit one statusChange event per reset control', () => { - form.statusChanges.subscribe(() => logger.push('form')); - g.statusChanges.subscribe(() => logger.push('group')); - c.statusChanges.subscribe(() => logger.push('control1')); - c2.statusChanges.subscribe(() => logger.push('control2')); - c3.statusChanges.subscribe(() => logger.push('control3')); - - g.reset(); - expect(logger).toEqual(['control1', 'control2', 'group', 'form']); - }); - - it('should emit one statusChange event per reset control', () => { - form.statusChanges.subscribe(() => logger.push('form')); - g.statusChanges.subscribe(() => logger.push('group')); - c.statusChanges.subscribe(() => logger.push('control1')); - c2.statusChanges.subscribe(() => logger.push('control2')); - c3.statusChanges.subscribe(() => logger.push('control3')); - - g.reset({'one': {value: '', disabled: true}}); - expect(logger).toEqual(['control1', 'control2', 'group', 'form']); - }); - - it('should mark as pristine and not dirty before emitting valueChange and statusChange events when resetting', - () => { - const pristineAndNotDirty = () => { - expect(form.pristine).toBe(true); - expect(form.dirty).toBe(false); - }; - - c3.markAsDirty(); - expect(form.pristine).toBe(false); - expect(form.dirty).toBe(true); - - form.valueChanges.subscribe(pristineAndNotDirty); - form.statusChanges.subscribe(pristineAndNotDirty); - - form.reset(); + it('should not fire an event when explicitly specified', fakeAsync(() => { + form.valueChanges.subscribe((value) => { + throw 'Should not happen'; }); - }); - - }); - - describe('contains', () => { - let group: FormGroup; - - beforeEach(() => { - group = new FormGroup({ - 'required': new FormControl('requiredValue'), - 'optional': new FormControl({value: 'disabled value', disabled: true}) - }); - }); - - it('should return false when the component is disabled', - () => { expect(group.contains('optional')).toEqual(false); }); - - it('should return false when there is no component with the given name', - () => { expect(group.contains('something else')).toEqual(false); }); - - it('should return true when the component is enabled', () => { - expect(group.contains('required')).toEqual(true); - - group.enable(); - - expect(group.contains('optional')).toEqual(true); - }); - - it('should support controls with dots in their name', () => { - expect(group.contains('some.name')).toBe(false); - group.addControl('some.name', new FormControl()); - - expect(group.contains('some.name')).toBe(true); - }); - }); - - describe('retrieve', () => { - let group: FormGroup; - - beforeEach(() => { - group = new FormGroup({ - 'required': new FormControl('requiredValue'), - }); - }); - - it('should not get inherited properties', - () => { expect(group.get('constructor')).toBe(null); }); - }); - - describe('statusChanges', () => { - let control: FormControl; - let group: FormGroup; - - beforeEach(async(() => { - control = new FormControl('', asyncValidatorReturningObservable); - group = new FormGroup({'one': control}); - })); - - - // TODO(kara): update these tests to use fake Async - it('should fire a statusChange if child has async validation change', - inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { - const loggedValues: string[] = []; - group.statusChanges.subscribe({ - next: (status: string) => { - loggedValues.push(status); - if (loggedValues.length === 2) { - expect(loggedValues).toEqual(['PENDING', 'INVALID']); - } - async.done(); - } + g.valueChanges.subscribe((value) => { + throw 'Should not happen'; }); - control.setValue(''); + c.valueChanges.subscribe((value) => { + throw 'Should not happen'; + }); + + g.setValue({'one': 'one', 'two': 'two'}, {emitEvent: false}); + tick(); })); + + it('should emit one statusChange event per control', () => { + form.statusChanges.subscribe(() => logger.push('form')); + g.statusChanges.subscribe(() => logger.push('group')); + c.statusChanges.subscribe(() => logger.push('control1')); + c2.statusChanges.subscribe(() => logger.push('control2')); + + g.setValue({'one': 'one', 'two': 'two'}); + expect(logger).toEqual(['control1', 'control2', 'group', 'form']); + }); + }); + }); + + describe('patchValue', () => { + let c: FormControl, c2: FormControl, g: FormGroup; + + beforeEach(() => { + c = new FormControl(''); + c2 = new FormControl(''); + g = new FormGroup({'one': c, 'two': c2}); }); - describe('getError', () => { - it('should return the error when it is present', () => { - const c = new FormControl('', Validators.required); - const g = new FormGroup({'one': c}); - expect(c.getError('required')).toEqual(true); - expect(g.getError('required', ['one'])).toEqual(true); + it('should set its own value', () => { + g.patchValue({'one': 'one', 'two': 'two'}); + expect(g.value).toEqual({'one': 'one', 'two': 'two'}); + }); + + it('should set child values', () => { + g.patchValue({'one': 'one', 'two': 'two'}); + expect(c.value).toEqual('one'); + expect(c2.value).toEqual('two'); + }); + + it('should patch disabled control values', () => { + c2.disable(); + g.patchValue({'one': 'one', 'two': 'two'}); + expect(c2.value).toEqual('two'); + expect(g.value).toEqual({'one': 'one'}); + expect(g.getRawValue()).toEqual({'one': 'one', 'two': 'two'}); + }); + + it('should patch disabled control groups', () => { + g.disable(); + g.patchValue({'one': 'one', 'two': 'two'}); + expect(c.value).toEqual('one'); + expect(c2.value).toEqual('two'); + expect(g.value).toEqual({'one': 'one', 'two': 'two'}); + }); + + it('should set parent values', () => { + const form = new FormGroup({'parent': g}); + g.patchValue({'one': 'one', 'two': 'two'}); + expect(form.value).toEqual({'parent': {'one': 'one', 'two': 'two'}}); + }); + + it('should not update the parent when explicitly specified', () => { + const form = new FormGroup({'parent': g}); + g.patchValue({'one': 'one', 'two': 'two'}, {onlySelf: true}); + + expect(form.value).toEqual({parent: {'one': '', 'two': ''}}); + }); + + it('should ignore fields that are missing from supplied value (subset)', () => { + g.patchValue({'one': 'one'}); + expect(g.value).toEqual({'one': 'one', 'two': ''}); + }); + + it('should not ignore fields that are null', () => { + g.patchValue({'one': null}); + expect(g.value).toEqual({'one': null, 'two': ''}); + }); + + it('should ignore any value provided for a missing control (superset)', () => { + g.patchValue({'three': 'three'}); + expect(g.value).toEqual({'one': '', 'two': ''}); + }); + + describe('patchValue() events', () => { + let form: FormGroup; + let logger: any[]; + + beforeEach(() => { + form = new FormGroup({'parent': g}); + logger = []; }); - it('should return null otherwise', () => { - const c = new FormControl('not empty', Validators.required); - const g = new FormGroup({'one': c}); - expect(c.getError('invalid')).toEqual(null); - expect(g.getError('required', ['one'])).toEqual(null); - expect(g.getError('required', ['invalid'])).toEqual(null); + it('should emit one valueChange event per control', () => { + form.valueChanges.subscribe(() => logger.push('form')); + g.valueChanges.subscribe(() => logger.push('group')); + c.valueChanges.subscribe(() => logger.push('control1')); + c2.valueChanges.subscribe(() => logger.push('control2')); + + g.patchValue({'one': 'one', 'two': 'two'}); + expect(logger).toEqual(['control1', 'control2', 'group', 'form']); }); - it('should be able to traverse group with single string', () => { - const c = new FormControl('', Validators.required); - const g = new FormGroup({'one': c}); - expect(c.getError('required')).toEqual(true); - expect(g.getError('required', 'one')).toEqual(true); + it('should not emit valueChange events for skipped controls', () => { + form.valueChanges.subscribe(() => logger.push('form')); + g.valueChanges.subscribe(() => logger.push('group')); + c.valueChanges.subscribe(() => logger.push('control1')); + c2.valueChanges.subscribe(() => logger.push('control2')); + + g.patchValue({'one': 'one'}); + expect(logger).toEqual(['control1', 'group', 'form']); }); - it('should be able to traverse group with string delimited by dots', () => { - const c = new FormControl('', Validators.required); - const g2 = new FormGroup({'two': c}); - const g1 = new FormGroup({'one': g2}); - expect(c.getError('required')).toEqual(true); - expect(g1.getError('required', 'one.two')).toEqual(true); + it('should not fire an event when explicitly specified', fakeAsync(() => { + form.valueChanges.subscribe((value) => { + throw 'Should not happen'; + }); + g.valueChanges.subscribe((value) => { + throw 'Should not happen'; + }); + c.valueChanges.subscribe((value) => { + throw 'Should not happen'; + }); + + g.patchValue({'one': 'one', 'two': 'two'}, {emitEvent: false}); + tick(); + })); + + it('should emit one statusChange event per control', () => { + form.statusChanges.subscribe(() => logger.push('form')); + g.statusChanges.subscribe(() => logger.push('group')); + c.statusChanges.subscribe(() => logger.push('control1')); + c2.statusChanges.subscribe(() => logger.push('control2')); + + g.patchValue({'one': 'one', 'two': 'two'}); + expect(logger).toEqual(['control1', 'control2', 'group', 'form']); + }); + }); + }); + + describe('reset()', () => { + let c: FormControl, c2: FormControl, g: FormGroup; + + beforeEach(() => { + c = new FormControl('initial value'); + c2 = new FormControl(''); + g = new FormGroup({'one': c, 'two': c2}); + }); + + it('should set its own value if value passed', () => { + g.setValue({'one': 'new value', 'two': 'new value'}); + + g.reset({'one': 'initial value', 'two': ''}); + expect(g.value).toEqual({'one': 'initial value', 'two': ''}); + }); + + it('should set its own value if boxed value passed', () => { + g.setValue({'one': 'new value', 'two': 'new value'}); + + g.reset({'one': {value: 'initial value', disabled: false}, 'two': ''}); + expect(g.value).toEqual({'one': 'initial value', 'two': ''}); + }); + + it('should clear its own value if no value passed', () => { + g.setValue({'one': 'new value', 'two': 'new value'}); + + g.reset(); + expect(g.value).toEqual({'one': null, 'two': null}); + }); + + it('should set the value of each of its child controls if value passed', () => { + g.setValue({'one': 'new value', 'two': 'new value'}); + + g.reset({'one': 'initial value', 'two': ''}); + expect(c.value).toBe('initial value'); + expect(c2.value).toBe(''); + }); + + it('should clear the value of each of its child controls if no value passed', () => { + g.setValue({'one': 'new value', 'two': 'new value'}); + + g.reset(); + expect(c.value).toBe(null); + expect(c2.value).toBe(null); + }); + + it('should set the value of its parent if value passed', () => { + const form = new FormGroup({'g': g}); + g.setValue({'one': 'new value', 'two': 'new value'}); + + g.reset({'one': 'initial value', 'two': ''}); + expect(form.value).toEqual({'g': {'one': 'initial value', 'two': ''}}); + }); + + it('should clear the value of its parent if no value passed', () => { + const form = new FormGroup({'g': g}); + g.setValue({'one': 'new value', 'two': 'new value'}); + + g.reset(); + expect(form.value).toEqual({'g': {'one': null, 'two': null}}); + }); + + it('should not update the parent when explicitly specified', () => { + const form = new FormGroup({'g': g}); + g.reset({'one': 'new value', 'two': 'new value'}, {onlySelf: true}); + + expect(form.value).toEqual({g: {'one': 'initial value', 'two': ''}}); + }); + + it('should mark itself as pristine', () => { + g.markAsDirty(); + expect(g.pristine).toBe(false); + + g.reset(); + expect(g.pristine).toBe(true); + }); + + it('should mark all child controls as pristine', () => { + c.markAsDirty(); + c2.markAsDirty(); + expect(c.pristine).toBe(false); + expect(c2.pristine).toBe(false); + + g.reset(); + expect(c.pristine).toBe(true); + expect(c2.pristine).toBe(true); + }); + + it('should mark the parent as pristine if all siblings pristine', () => { + const c3 = new FormControl(''); + const form = new FormGroup({'g': g, 'c3': c3}); + + g.markAsDirty(); + expect(form.pristine).toBe(false); + + g.reset(); + expect(form.pristine).toBe(true); + }); + + it('should not mark the parent pristine if any dirty siblings', () => { + const c3 = new FormControl(''); + const form = new FormGroup({'g': g, 'c3': c3}); + + g.markAsDirty(); + c3.markAsDirty(); + expect(form.pristine).toBe(false); + + g.reset(); + expect(form.pristine).toBe(false); + }); + + it('should mark itself as untouched', () => { + g.markAsTouched(); + expect(g.untouched).toBe(false); + + g.reset(); + expect(g.untouched).toBe(true); + }); + + it('should mark all child controls as untouched', () => { + c.markAsTouched(); + c2.markAsTouched(); + expect(c.untouched).toBe(false); + expect(c2.untouched).toBe(false); + + g.reset(); + expect(c.untouched).toBe(true); + expect(c2.untouched).toBe(true); + }); + + it('should mark the parent untouched if all siblings untouched', () => { + const c3 = new FormControl(''); + const form = new FormGroup({'g': g, 'c3': c3}); + + g.markAsTouched(); + expect(form.untouched).toBe(false); + + g.reset(); + expect(form.untouched).toBe(true); + }); + + it('should not mark the parent untouched if any touched siblings', () => { + const c3 = new FormControl(''); + const form = new FormGroup({'g': g, 'c3': c3}); + + g.markAsTouched(); + c3.markAsTouched(); + expect(form.untouched).toBe(false); + + g.reset(); + expect(form.untouched).toBe(false); + }); + + it('should retain previous disabled state', () => { + g.disable(); + g.reset(); + + expect(g.disabled).toBe(true); + }); + + it('should set child disabled state if boxed value passed', () => { + g.disable(); + g.reset({'one': {value: '', disabled: false}, 'two': ''}); + + expect(c.disabled).toBe(false); + expect(g.disabled).toBe(false); + }); + + describe('reset() events', () => { + let form: FormGroup, c3: FormControl, logger: any[]; + + beforeEach(() => { + c3 = new FormControl(''); + form = new FormGroup({'g': g, 'c3': c3}); + logger = []; }); - it('should traverse group with form array using string and numbers', () => { - const c = new FormControl('', Validators.required); - const g2 = new FormGroup({'two': c}); - const a = new FormArray([g2]); - const g1 = new FormGroup({'one': a}); - expect(c.getError('required')).toEqual(true); - expect(g1.getError('required', ['one', 0, 'two'])).toEqual(true); + it('should emit one valueChange event per reset control', () => { + form.valueChanges.subscribe(() => logger.push('form')); + g.valueChanges.subscribe(() => logger.push('group')); + c.valueChanges.subscribe(() => logger.push('control1')); + c2.valueChanges.subscribe(() => logger.push('control2')); + c3.valueChanges.subscribe(() => logger.push('control3')); + + g.reset(); + expect(logger).toEqual(['control1', 'control2', 'group', 'form']); + }); + + it('should not fire an event when explicitly specified', fakeAsync(() => { + form.valueChanges.subscribe((value) => { + throw 'Should not happen'; + }); + g.valueChanges.subscribe((value) => { + throw 'Should not happen'; + }); + c.valueChanges.subscribe((value) => { + throw 'Should not happen'; + }); + + g.reset({}, {emitEvent: false}); + tick(); + })); + + it('should emit one statusChange event per reset control', () => { + form.statusChanges.subscribe(() => logger.push('form')); + g.statusChanges.subscribe(() => logger.push('group')); + c.statusChanges.subscribe(() => logger.push('control1')); + c2.statusChanges.subscribe(() => logger.push('control2')); + c3.statusChanges.subscribe(() => logger.push('control3')); + + g.reset(); + expect(logger).toEqual(['control1', 'control2', 'group', 'form']); + }); + + it('should emit one statusChange event per reset control', () => { + form.statusChanges.subscribe(() => logger.push('form')); + g.statusChanges.subscribe(() => logger.push('group')); + c.statusChanges.subscribe(() => logger.push('control1')); + c2.statusChanges.subscribe(() => logger.push('control2')); + c3.statusChanges.subscribe(() => logger.push('control3')); + + g.reset({'one': {value: '', disabled: true}}); + expect(logger).toEqual(['control1', 'control2', 'group', 'form']); + }); + + it('should mark as pristine and not dirty before emitting valueChange and statusChange events when resetting', + () => { + const pristineAndNotDirty = () => { + expect(form.pristine).toBe(true); + expect(form.dirty).toBe(false); + }; + + c3.markAsDirty(); + expect(form.pristine).toBe(false); + expect(form.dirty).toBe(true); + + form.valueChanges.subscribe(pristineAndNotDirty); + form.statusChanges.subscribe(pristineAndNotDirty); + + form.reset(); + }); + }); + }); + + describe('contains', () => { + let group: FormGroup; + + beforeEach(() => { + group = new FormGroup({ + 'required': new FormControl('requiredValue'), + 'optional': new FormControl({value: 'disabled value', disabled: true}) }); }); - describe('hasError', () => { - it('should return true when it is present', () => { - const c = new FormControl('', Validators.required); - const g = new FormGroup({'one': c}); - expect(c.hasError('required')).toEqual(true); - expect(g.hasError('required', ['one'])).toEqual(true); - }); + it('should return false when the component is disabled', () => { + expect(group.contains('optional')).toEqual(false); + }); - it('should return false otherwise', () => { - const c = new FormControl('not empty', Validators.required); - const g = new FormGroup({'one': c}); - expect(c.hasError('invalid')).toEqual(false); - expect(g.hasError('required', ['one'])).toEqual(false); - expect(g.hasError('required', ['invalid'])).toEqual(false); - }); + it('should return false when there is no component with the given name', () => { + expect(group.contains('something else')).toEqual(false); + }); - it('should be able to traverse group with single string', () => { - const c = new FormControl('', Validators.required); - const g = new FormGroup({'one': c}); - expect(c.hasError('required')).toEqual(true); - expect(g.hasError('required', 'one')).toEqual(true); - }); + it('should return true when the component is enabled', () => { + expect(group.contains('required')).toEqual(true); - it('should be able to traverse group with string delimited by dots', () => { - const c = new FormControl('', Validators.required); - const g2 = new FormGroup({'two': c}); - const g1 = new FormGroup({'one': g2}); - expect(c.hasError('required')).toEqual(true); - expect(g1.hasError('required', 'one.two')).toEqual(true); - }); - it('should traverse group with form array using string and numbers', () => { - const c = new FormControl('', Validators.required); - const g2 = new FormGroup({'two': c}); - const a = new FormArray([g2]); - const g1 = new FormGroup({'one': a}); - expect(c.getError('required')).toEqual(true); - expect(g1.getError('required', ['one', 0, 'two'])).toEqual(true); + group.enable(); + + expect(group.contains('optional')).toEqual(true); + }); + + it('should support controls with dots in their name', () => { + expect(group.contains('some.name')).toBe(false); + group.addControl('some.name', new FormControl()); + + expect(group.contains('some.name')).toBe(true); + }); + }); + + describe('retrieve', () => { + let group: FormGroup; + + beforeEach(() => { + group = new FormGroup({ + 'required': new FormControl('requiredValue'), }); }); - describe('validator', () => { + it('should not get inherited properties', () => { + expect(group.get('constructor')).toBe(null); + }); + }); - function containsValidator(c: AbstractControl): ValidationErrors|null { - return c.get('one') !.value && c.get('one') !.value.indexOf('c') !== -1 ? null : - {'missing': true}; - } + describe('statusChanges', () => { + let control: FormControl; + let group: FormGroup; - it('should run a single validator when the value changes', () => { - const c = new FormControl(null); - const g = new FormGroup({'one': c}, simpleValidator); + beforeEach(async(() => { + control = new FormControl('', asyncValidatorReturningObservable); + group = new FormGroup({'one': control}); + })); - c.setValue('correct'); - expect(g.valid).toEqual(true); + // TODO(kara): update these tests to use fake Async + it('should fire a statusChange if child has async validation change', + inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { + const loggedValues: string[] = []; + group.statusChanges.subscribe({ + next: (status: string) => { + loggedValues.push(status); + if (loggedValues.length === 2) { + expect(loggedValues).toEqual(['PENDING', 'INVALID']); + } + async.done(); + } + }); + control.setValue(''); + })); + }); + + describe('getError', () => { + it('should return the error when it is present', () => { + const c = new FormControl('', Validators.required); + const g = new FormGroup({'one': c}); + expect(c.getError('required')).toEqual(true); + expect(g.getError('required', ['one'])).toEqual(true); + }); + + it('should return null otherwise', () => { + const c = new FormControl('not empty', Validators.required); + const g = new FormGroup({'one': c}); + expect(c.getError('invalid')).toEqual(null); + expect(g.getError('required', ['one'])).toEqual(null); + expect(g.getError('required', ['invalid'])).toEqual(null); + }); + + it('should be able to traverse group with single string', () => { + const c = new FormControl('', Validators.required); + const g = new FormGroup({'one': c}); + expect(c.getError('required')).toEqual(true); + expect(g.getError('required', 'one')).toEqual(true); + }); + + it('should be able to traverse group with string delimited by dots', () => { + const c = new FormControl('', Validators.required); + const g2 = new FormGroup({'two': c}); + const g1 = new FormGroup({'one': g2}); + expect(c.getError('required')).toEqual(true); + expect(g1.getError('required', 'one.two')).toEqual(true); + }); + + it('should traverse group with form array using string and numbers', () => { + const c = new FormControl('', Validators.required); + const g2 = new FormGroup({'two': c}); + const a = new FormArray([g2]); + const g1 = new FormGroup({'one': a}); + expect(c.getError('required')).toEqual(true); + expect(g1.getError('required', ['one', 0, 'two'])).toEqual(true); + }); + }); + + describe('hasError', () => { + it('should return true when it is present', () => { + const c = new FormControl('', Validators.required); + const g = new FormGroup({'one': c}); + expect(c.hasError('required')).toEqual(true); + expect(g.hasError('required', ['one'])).toEqual(true); + }); + + it('should return false otherwise', () => { + const c = new FormControl('not empty', Validators.required); + const g = new FormGroup({'one': c}); + expect(c.hasError('invalid')).toEqual(false); + expect(g.hasError('required', ['one'])).toEqual(false); + expect(g.hasError('required', ['invalid'])).toEqual(false); + }); + + it('should be able to traverse group with single string', () => { + const c = new FormControl('', Validators.required); + const g = new FormGroup({'one': c}); + expect(c.hasError('required')).toEqual(true); + expect(g.hasError('required', 'one')).toEqual(true); + }); + + it('should be able to traverse group with string delimited by dots', () => { + const c = new FormControl('', Validators.required); + const g2 = new FormGroup({'two': c}); + const g1 = new FormGroup({'one': g2}); + expect(c.hasError('required')).toEqual(true); + expect(g1.hasError('required', 'one.two')).toEqual(true); + }); + it('should traverse group with form array using string and numbers', () => { + const c = new FormControl('', Validators.required); + const g2 = new FormGroup({'two': c}); + const a = new FormArray([g2]); + const g1 = new FormGroup({'one': a}); + expect(c.getError('required')).toEqual(true); + expect(g1.getError('required', ['one', 0, 'two'])).toEqual(true); + }); + }); + + describe('validator', () => { + function containsValidator(c: AbstractControl): ValidationErrors|null { + return c.get('one')!.value && c.get('one')!.value.indexOf('c') !== -1 ? null : + {'missing': true}; + } + + it('should run a single validator when the value changes', () => { + const c = new FormControl(null); + const g = new FormGroup({'one': c}, simpleValidator); + + c.setValue('correct'); + + expect(g.valid).toEqual(true); + expect(g.errors).toEqual(null); + + c.setValue('incorrect'); + + expect(g.valid).toEqual(false); + expect(g.errors).toEqual({'broken': true}); + }); + + it('should support multiple validators from array', () => { + const g = new FormGroup({one: new FormControl()}, [simpleValidator, containsValidator]); + expect(g.valid).toEqual(false); + expect(g.errors).toEqual({missing: true, broken: true}); + + g.setValue({one: 'c'}); + expect(g.valid).toEqual(false); + expect(g.errors).toEqual({broken: true}); + + g.setValue({one: 'correct'}); + expect(g.valid).toEqual(true); + }); + + it('should set single validator from options obj', () => { + const g = new FormGroup({one: new FormControl()}, {validators: simpleValidator}); + expect(g.valid).toEqual(false); + expect(g.errors).toEqual({broken: true}); + + g.setValue({one: 'correct'}); + expect(g.valid).toEqual(true); + }); + + it('should set multiple validators from options obj', () => { + const g = new FormGroup( + {one: new FormControl()}, {validators: [simpleValidator, containsValidator]}); + expect(g.valid).toEqual(false); + expect(g.errors).toEqual({missing: true, broken: true}); + + g.setValue({one: 'c'}); + expect(g.valid).toEqual(false); + expect(g.errors).toEqual({broken: true}); + + g.setValue({one: 'correct'}); + expect(g.valid).toEqual(true); + }); + }); + + describe('asyncValidator', () => { + it('should run the async validator', fakeAsync(() => { + const c = new FormControl('value'); + const g = new FormGroup({'one': c}, null!, asyncValidator('expected')); + + expect(g.pending).toEqual(true); + + tick(1); + + expect(g.errors).toEqual({'async': true}); + expect(g.pending).toEqual(false); + })); + + it('should set multiple async validators from array', fakeAsync(() => { + const g = new FormGroup( + {'one': new FormControl('value')}, null!, + [asyncValidator('expected'), otherObservableValidator]); + expect(g.pending).toEqual(true); + + tick(); + expect(g.errors).toEqual({'async': true, 'other': true}); + expect(g.pending).toEqual(false); + })); + + it('should set single async validator from options obj', fakeAsync(() => { + const g = new FormGroup( + {'one': new FormControl('value')}, {asyncValidators: asyncValidator('expected')}); + expect(g.pending).toEqual(true); + + tick(); + expect(g.errors).toEqual({'async': true}); + expect(g.pending).toEqual(false); + })); + + it('should set multiple async validators from options obj', fakeAsync(() => { + const g = new FormGroup( + {'one': new FormControl('value')}, + {asyncValidators: [asyncValidator('expected'), otherObservableValidator]}); + expect(g.pending).toEqual(true); + + tick(); + expect(g.errors).toEqual({'async': true, 'other': true}); + expect(g.pending).toEqual(false); + })); + + it('should set the parent group\'s status to pending', fakeAsync(() => { + const c = new FormControl('value', null!, asyncValidator('expected')); + const g = new FormGroup({'one': c}); + + expect(g.pending).toEqual(true); + + tick(1); + + expect(g.pending).toEqual(false); + })); + + it('should run the parent group\'s async validator when children are pending', fakeAsync(() => { + const c = new FormControl('value', null!, asyncValidator('expected')); + const g = new FormGroup({'one': c}, null!, asyncValidator('expected')); + + tick(1); + + expect(g.errors).toEqual({'async': true}); + expect(g.get('one')!.errors).toEqual({'async': true}); + })); + }); + + describe('disable() & enable()', () => { + it('should mark the group as disabled', () => { + const g = new FormGroup({'one': new FormControl(null)}); + expect(g.disabled).toBe(false); + expect(g.valid).toBe(true); + + g.disable(); + expect(g.disabled).toBe(true); + expect(g.valid).toBe(false); + + g.enable(); + expect(g.disabled).toBe(false); + expect(g.valid).toBe(true); + }); + + it('should set the group status as disabled', () => { + const g = new FormGroup({'one': new FormControl(null)}); + expect(g.status).toEqual('VALID'); + + g.disable(); + expect(g.status).toEqual('DISABLED'); + + g.enable(); + expect(g.status).toBe('VALID'); + }); + + it('should mark children of the group as disabled', () => { + const c1 = new FormControl(null); + const c2 = new FormControl(null); + const g = new FormGroup({'one': c1, 'two': c2}); + expect(c1.disabled).toBe(false); + expect(c2.disabled).toBe(false); + + g.disable(); + expect(c1.disabled).toBe(true); + expect(c2.disabled).toBe(true); + + g.enable(); + expect(c1.disabled).toBe(false); + expect(c2.disabled).toBe(false); + }); + + it('should ignore disabled controls in validation', () => { + const g = new FormGroup({ + nested: new FormGroup({one: new FormControl(null, Validators.required)}), + two: new FormControl('two') + }); + expect(g.valid).toBe(false); + + g.get('nested')!.disable(); + expect(g.valid).toBe(true); + + g.get('nested')!.enable(); + expect(g.valid).toBe(false); + }); + + it('should ignore disabled controls when serializing value', () => { + const g = new FormGroup( + {nested: new FormGroup({one: new FormControl('one')}), two: new FormControl('two')}); + expect(g.value).toEqual({'nested': {'one': 'one'}, 'two': 'two'}); + + g.get('nested')!.disable(); + expect(g.value).toEqual({'two': 'two'}); + + g.get('nested')!.enable(); + expect(g.value).toEqual({'nested': {'one': 'one'}, 'two': 'two'}); + }); + + it('should update its value when disabled with disabled children', () => { + const g = new FormGroup( + {nested: new FormGroup({one: new FormControl('one'), two: new FormControl('two')})}); + + g.get('nested.two')!.disable(); + expect(g.value).toEqual({nested: {one: 'one'}}); + + g.get('nested')!.disable(); + expect(g.value).toEqual({nested: {one: 'one', two: 'two'}}); + + g.get('nested')!.enable(); + expect(g.value).toEqual({nested: {one: 'one', two: 'two'}}); + }); + + it('should update its value when enabled with disabled children', () => { + const g = new FormGroup( + {nested: new FormGroup({one: new FormControl('one'), two: new FormControl('two')})}); + + g.get('nested.two')!.disable(); + expect(g.value).toEqual({nested: {one: 'one'}}); + + g.get('nested')!.enable(); + expect(g.value).toEqual({nested: {one: 'one', two: 'two'}}); + }); + + it('should ignore disabled controls when determining dirtiness', () => { + const g = new FormGroup( + {nested: new FormGroup({one: new FormControl('one')}), two: new FormControl('two')}); + g.get('nested.one')!.markAsDirty(); + expect(g.dirty).toBe(true); + + g.get('nested')!.disable(); + expect(g.get('nested')!.dirty).toBe(true); + expect(g.dirty).toEqual(false); + + g.get('nested')!.enable(); + expect(g.dirty).toEqual(true); + }); + + it('should ignore disabled controls when determining touched state', () => { + const g = new FormGroup( + {nested: new FormGroup({one: new FormControl('one')}), two: new FormControl('two')}); + g.get('nested.one')!.markAsTouched(); + expect(g.touched).toBe(true); + + g.get('nested')!.disable(); + expect(g.get('nested')!.touched).toBe(true); + expect(g.touched).toEqual(false); + + g.get('nested')!.enable(); + expect(g.touched).toEqual(true); + }); + + it('should keep empty, disabled groups disabled when updating validity', () => { + const group = new FormGroup({}); + expect(group.status).toEqual('VALID'); + + group.disable(); + expect(group.status).toEqual('DISABLED'); + + group.updateValueAndValidity(); + expect(group.status).toEqual('DISABLED'); + + group.addControl('one', new FormControl({value: '', disabled: true})); + expect(group.status).toEqual('DISABLED'); + + group.addControl('two', new FormControl()); + expect(group.status).toEqual('VALID'); + }); + + it('should re-enable empty, disabled groups', () => { + const group = new FormGroup({}); + group.disable(); + expect(group.status).toEqual('DISABLED'); + + group.enable(); + expect(group.status).toEqual('VALID'); + }); + + it('should not run validators on disabled controls', () => { + const validator = jasmine.createSpy('validator'); + const g = new FormGroup({'one': new FormControl()}, validator); + expect(validator.calls.count()).toEqual(1); + + g.disable(); + expect(validator.calls.count()).toEqual(1); + + g.setValue({one: 'value'}); + expect(validator.calls.count()).toEqual(1); + + g.enable(); + expect(validator.calls.count()).toEqual(2); + }); + + describe('disabled errors', () => { + it('should clear out group errors when disabled', () => { + const g = new FormGroup({'one': new FormControl()}, () => ({'expected': true})); + expect(g.errors).toEqual({'expected': true}); + + g.disable(); expect(g.errors).toEqual(null); - c.setValue('incorrect'); - - expect(g.valid).toEqual(false); - expect(g.errors).toEqual({'broken': true}); + g.enable(); + expect(g.errors).toEqual({'expected': true}); }); - it('should support multiple validators from array', () => { - const g = new FormGroup({one: new FormControl()}, [simpleValidator, containsValidator]); - expect(g.valid).toEqual(false); - expect(g.errors).toEqual({missing: true, broken: true}); + it('should re-populate group errors when enabled from a child', () => { + const g = new FormGroup({'one': new FormControl()}, () => ({'expected': true})); + g.disable(); + expect(g.errors).toEqual(null); - g.setValue({one: 'c'}); - expect(g.valid).toEqual(false); - expect(g.errors).toEqual({broken: true}); - - g.setValue({one: 'correct'}); - expect(g.valid).toEqual(true); + g.addControl('two', new FormControl()); + expect(g.errors).toEqual({'expected': true}); }); - it('should set single validator from options obj', () => { - const g = new FormGroup({one: new FormControl()}, {validators: simpleValidator}); - expect(g.valid).toEqual(false); - expect(g.errors).toEqual({broken: true}); - - g.setValue({one: 'correct'}); - expect(g.valid).toEqual(true); - }); - - it('should set multiple validators from options obj', () => { - const g = new FormGroup( - {one: new FormControl()}, {validators: [simpleValidator, containsValidator]}); - expect(g.valid).toEqual(false); - expect(g.errors).toEqual({missing: true, broken: true}); - - g.setValue({one: 'c'}); - expect(g.valid).toEqual(false); - expect(g.errors).toEqual({broken: true}); - - g.setValue({one: 'correct'}); - expect(g.valid).toEqual(true); - }); - - }); - - describe('asyncValidator', () => { - it('should run the async validator', fakeAsync(() => { - const c = new FormControl('value'); - const g = new FormGroup({'one': c}, null !, asyncValidator('expected')); - - expect(g.pending).toEqual(true); - - tick(1); - - expect(g.errors).toEqual({'async': true}); - expect(g.pending).toEqual(false); - })); - - it('should set multiple async validators from array', fakeAsync(() => { - const g = new FormGroup( - {'one': new FormControl('value')}, null !, - [asyncValidator('expected'), otherObservableValidator]); - expect(g.pending).toEqual(true); - - tick(); - expect(g.errors).toEqual({'async': true, 'other': true}); - expect(g.pending).toEqual(false); - })); - - it('should set single async validator from options obj', fakeAsync(() => { - const g = new FormGroup( - {'one': new FormControl('value')}, {asyncValidators: asyncValidator('expected')}); - expect(g.pending).toEqual(true); - + it('should clear out async group errors when disabled', fakeAsync(() => { + const g = new FormGroup({'one': new FormControl()}, null!, asyncValidator('expected')); tick(); expect(g.errors).toEqual({'async': true}); - expect(g.pending).toEqual(false); - })); - it('should set multiple async validators from options obj', fakeAsync(() => { - const g = new FormGroup( - {'one': new FormControl('value')}, - {asyncValidators: [asyncValidator('expected'), otherObservableValidator]}); - expect(g.pending).toEqual(true); + g.disable(); + expect(g.errors).toEqual(null); + g.enable(); + tick(); + expect(g.errors).toEqual({'async': true}); + })); + + it('should re-populate async group errors when enabled from a child', fakeAsync(() => { + const g = new FormGroup({'one': new FormControl()}, null!, asyncValidator('expected')); + tick(); + expect(g.errors).toEqual({'async': true}); + + g.disable(); + expect(g.errors).toEqual(null); + + g.addControl('two', new FormControl()); tick(); - expect(g.errors).toEqual({'async': true, 'other': true}); - expect(g.pending).toEqual(false); - })); - - it('should set the parent group\'s status to pending', fakeAsync(() => { - const c = new FormControl('value', null !, asyncValidator('expected')); - const g = new FormGroup({'one': c}); - - expect(g.pending).toEqual(true); - - tick(1); - - expect(g.pending).toEqual(false); - })); - - it('should run the parent group\'s async validator when children are pending', - fakeAsync(() => { - const c = new FormControl('value', null !, asyncValidator('expected')); - const g = new FormGroup({'one': c}, null !, asyncValidator('expected')); - - tick(1); - expect(g.errors).toEqual({'async': true}); - expect(g.get('one') !.errors).toEqual({'async': true}); })); }); - describe('disable() & enable()', () => { - it('should mark the group as disabled', () => { - const g = new FormGroup({'one': new FormControl(null)}); - expect(g.disabled).toBe(false); - expect(g.valid).toBe(true); - - g.disable(); - expect(g.disabled).toBe(true); - expect(g.valid).toBe(false); - - g.enable(); - expect(g.disabled).toBe(false); - expect(g.valid).toBe(true); - }); - - it('should set the group status as disabled', () => { - const g = new FormGroup({'one': new FormControl(null)}); - expect(g.status).toEqual('VALID'); - - g.disable(); - expect(g.status).toEqual('DISABLED'); - - g.enable(); - expect(g.status).toBe('VALID'); - }); - - it('should mark children of the group as disabled', () => { - const c1 = new FormControl(null); - const c2 = new FormControl(null); - const g = new FormGroup({'one': c1, 'two': c2}); - expect(c1.disabled).toBe(false); - expect(c2.disabled).toBe(false); - - g.disable(); - expect(c1.disabled).toBe(true); - expect(c2.disabled).toBe(true); - - g.enable(); - expect(c1.disabled).toBe(false); - expect(c2.disabled).toBe(false); - }); - - it('should ignore disabled controls in validation', () => { - const g = new FormGroup({ - nested: new FormGroup({one: new FormControl(null, Validators.required)}), - two: new FormControl('two') - }); - expect(g.valid).toBe(false); - - g.get('nested') !.disable(); - expect(g.valid).toBe(true); - - g.get('nested') !.enable(); - expect(g.valid).toBe(false); - }); - - it('should ignore disabled controls when serializing value', () => { - const g = new FormGroup( - {nested: new FormGroup({one: new FormControl('one')}), two: new FormControl('two')}); - expect(g.value).toEqual({'nested': {'one': 'one'}, 'two': 'two'}); - - g.get('nested') !.disable(); - expect(g.value).toEqual({'two': 'two'}); - - g.get('nested') !.enable(); - expect(g.value).toEqual({'nested': {'one': 'one'}, 'two': 'two'}); - }); - - it('should update its value when disabled with disabled children', () => { - const g = new FormGroup( - {nested: new FormGroup({one: new FormControl('one'), two: new FormControl('two')})}); - - g.get('nested.two') !.disable(); - expect(g.value).toEqual({nested: {one: 'one'}}); - - g.get('nested') !.disable(); - expect(g.value).toEqual({nested: {one: 'one', two: 'two'}}); - - g.get('nested') !.enable(); - expect(g.value).toEqual({nested: {one: 'one', two: 'two'}}); - }); - - it('should update its value when enabled with disabled children', () => { - const g = new FormGroup( - {nested: new FormGroup({one: new FormControl('one'), two: new FormControl('two')})}); - - g.get('nested.two') !.disable(); - expect(g.value).toEqual({nested: {one: 'one'}}); - - g.get('nested') !.enable(); - expect(g.value).toEqual({nested: {one: 'one', two: 'two'}}); - }); - - it('should ignore disabled controls when determining dirtiness', () => { - const g = new FormGroup( - {nested: new FormGroup({one: new FormControl('one')}), two: new FormControl('two')}); - g.get('nested.one') !.markAsDirty(); - expect(g.dirty).toBe(true); - - g.get('nested') !.disable(); - expect(g.get('nested') !.dirty).toBe(true); - expect(g.dirty).toEqual(false); - - g.get('nested') !.enable(); - expect(g.dirty).toEqual(true); - }); - - it('should ignore disabled controls when determining touched state', () => { - const g = new FormGroup( - {nested: new FormGroup({one: new FormControl('one')}), two: new FormControl('two')}); - g.get('nested.one') !.markAsTouched(); - expect(g.touched).toBe(true); - - g.get('nested') !.disable(); - expect(g.get('nested') !.touched).toBe(true); - expect(g.touched).toEqual(false); - - g.get('nested') !.enable(); - expect(g.touched).toEqual(true); - }); - - it('should keep empty, disabled groups disabled when updating validity', () => { - const group = new FormGroup({}); - expect(group.status).toEqual('VALID'); - - group.disable(); - expect(group.status).toEqual('DISABLED'); - - group.updateValueAndValidity(); - expect(group.status).toEqual('DISABLED'); - - group.addControl('one', new FormControl({value: '', disabled: true})); - expect(group.status).toEqual('DISABLED'); - - group.addControl('two', new FormControl()); - expect(group.status).toEqual('VALID'); - }); - - it('should re-enable empty, disabled groups', () => { - const group = new FormGroup({}); - group.disable(); - expect(group.status).toEqual('DISABLED'); - - group.enable(); - expect(group.status).toEqual('VALID'); - }); - - it('should not run validators on disabled controls', () => { - const validator = jasmine.createSpy('validator'); - const g = new FormGroup({'one': new FormControl()}, validator); - expect(validator.calls.count()).toEqual(1); - - g.disable(); - expect(validator.calls.count()).toEqual(1); - - g.setValue({one: 'value'}); - expect(validator.calls.count()).toEqual(1); - - g.enable(); - expect(validator.calls.count()).toEqual(2); - }); - - describe('disabled errors', () => { - it('should clear out group errors when disabled', () => { - const g = new FormGroup({'one': new FormControl()}, () => ({'expected': true})); - expect(g.errors).toEqual({'expected': true}); - - g.disable(); - expect(g.errors).toEqual(null); - - g.enable(); - expect(g.errors).toEqual({'expected': true}); - }); - - it('should re-populate group errors when enabled from a child', () => { - const g = new FormGroup({'one': new FormControl()}, () => ({'expected': true})); - g.disable(); - expect(g.errors).toEqual(null); - - g.addControl('two', new FormControl()); - expect(g.errors).toEqual({'expected': true}); - }); - - it('should clear out async group errors when disabled', fakeAsync(() => { - const g = - new FormGroup({'one': new FormControl()}, null !, asyncValidator('expected')); - tick(); - expect(g.errors).toEqual({'async': true}); - - g.disable(); - expect(g.errors).toEqual(null); - - g.enable(); - tick(); - expect(g.errors).toEqual({'async': true}); - })); - - it('should re-populate async group errors when enabled from a child', fakeAsync(() => { - const g = - new FormGroup({'one': new FormControl()}, null !, asyncValidator('expected')); - tick(); - expect(g.errors).toEqual({'async': true}); - - g.disable(); - expect(g.errors).toEqual(null); - - g.addControl('two', new FormControl()); - tick(); - expect(g.errors).toEqual({'async': true}); - })); - }); - - describe('disabled events', () => { - let logger: string[]; - let c: FormControl; - let g: FormGroup; - let form: FormGroup; - - beforeEach(() => { - logger = []; - c = new FormControl('', Validators.required); - g = new FormGroup({one: c}); - form = new FormGroup({g: g}); - }); - - it('should emit value change events in the right order', () => { - c.valueChanges.subscribe(() => logger.push('control')); - g.valueChanges.subscribe(() => logger.push('group')); - form.valueChanges.subscribe(() => logger.push('form')); - - g.disable(); - expect(logger).toEqual(['control', 'group', 'form']); - }); - - it('should emit status change events in the right order', () => { - c.statusChanges.subscribe(() => logger.push('control')); - g.statusChanges.subscribe(() => logger.push('group')); - form.statusChanges.subscribe(() => logger.push('form')); - - g.disable(); - expect(logger).toEqual(['control', 'group', 'form']); - }); - - it('should not emit value change events when emitEvent = false', () => { - c.valueChanges.subscribe(() => logger.push('control')); - g.valueChanges.subscribe(() => logger.push('group')); - form.valueChanges.subscribe(() => logger.push('form')); - - g.disable({emitEvent: false}); - expect(logger).toEqual([]); - g.enable({emitEvent: false}); - expect(logger).toEqual([]); - }); - - it('should not emit status change events when emitEvent = false', () => { - c.statusChanges.subscribe(() => logger.push('control')); - g.statusChanges.subscribe(() => logger.push('group')); - form.statusChanges.subscribe(() => logger.push('form')); - - g.disable({emitEvent: false}); - expect(logger).toEqual([]); - g.enable({emitEvent: false}); - expect(logger).toEqual([]); - }); - - }); - - }); - - describe('updateTreeValidity()', () => { - let c: FormControl, c2: FormControl, c3: FormControl; - let nested: FormGroup, form: FormGroup; + describe('disabled events', () => { let logger: string[]; + let c: FormControl; + let g: FormGroup; + let form: FormGroup; 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 = new FormControl('', Validators.required); + g = new FormGroup({one: c}); + form = new FormGroup({g: g}); + }); - c.statusChanges.subscribe(() => logger.push('one')); - c2.statusChanges.subscribe(() => logger.push('two')); - c3.statusChanges.subscribe(() => logger.push('three')); - nested.statusChanges.subscribe(() => logger.push('nested')); + it('should emit value change events in the right order', () => { + c.valueChanges.subscribe(() => logger.push('control')); + g.valueChanges.subscribe(() => logger.push('group')); + form.valueChanges.subscribe(() => logger.push('form')); + + g.disable(); + expect(logger).toEqual(['control', 'group', 'form']); + }); + + it('should emit status change events in the right order', () => { + c.statusChanges.subscribe(() => logger.push('control')); + g.statusChanges.subscribe(() => logger.push('group')); form.statusChanges.subscribe(() => logger.push('form')); + + g.disable(); + expect(logger).toEqual(['control', 'group', 'form']); }); - it('should update tree validity', () => { - (form as any)._updateTreeValidity(); - expect(logger).toEqual(['one', 'two', 'nested', 'three', 'form']); - }); + it('should not emit value change events when emitEvent = false', () => { + c.valueChanges.subscribe(() => logger.push('control')); + g.valueChanges.subscribe(() => logger.push('group')); + form.valueChanges.subscribe(() => logger.push('form')); - it('should not emit events when turned off', () => { - (form as any)._updateTreeValidity({emitEvent: false}); + g.disable({emitEvent: false}); + expect(logger).toEqual([]); + g.enable({emitEvent: false}); expect(logger).toEqual([]); }); - }); + it('should not emit status change events when emitEvent = false', () => { + c.statusChanges.subscribe(() => logger.push('control')); + g.statusChanges.subscribe(() => logger.push('group')); + form.statusChanges.subscribe(() => logger.push('form')); - describe('setControl()', () => { - let c: FormControl; - let g: FormGroup; - - beforeEach(() => { - c = new FormControl('one'); - g = new FormGroup({one: c}); - }); - - it('should replace existing control with new control', () => { - const c2 = new FormControl('new!', Validators.minLength(10)); - g.setControl('one', c2); - - expect(g.controls['one']).toEqual(c2); - expect(g.value).toEqual({one: 'new!'}); - expect(g.valid).toBe(false); - }); - - it('should add control if control did not exist before', () => { - const c2 = new FormControl('new!', Validators.minLength(10)); - g.setControl('two', c2); - - expect(g.controls['two']).toEqual(c2); - expect(g.value).toEqual({one: 'one', two: 'new!'}); - expect(g.valid).toBe(false); - }); - - it('should remove control if new control is null', () => { - g.setControl('one', null !); - expect(g.controls['one']).not.toBeDefined(); - expect(g.value).toEqual({}); - }); - - it('should only emit value change event once', () => { - const logger: string[] = []; - const c2 = new FormControl('new!'); - g.valueChanges.subscribe(() => logger.push('change!')); - g.setControl('one', c2); - expect(logger).toEqual(['change!']); - }); - - }); - - describe('pending', () => { - let c: FormControl; - let g: FormGroup; - - beforeEach(() => { - c = new FormControl('value'); - g = new FormGroup({'one': c}); - }); - - it('should be false after creating a control', () => { expect(g.pending).toEqual(false); }); - - it('should be true after changing the value of the control', () => { - c.markAsPending(); - expect(g.pending).toEqual(true); - }); - - it('should not update the parent when onlySelf = true', () => { - c.markAsPending({onlySelf: true}); - expect(g.pending).toEqual(false); - }); - - describe('status change events', () => { - let logger: string[]; - - beforeEach(() => { - logger = []; - g.statusChanges.subscribe((status) => logger.push(status)); - }); - - it('should emit event after marking control as pending', () => { - c.markAsPending(); - expect(logger).toEqual(['PENDING']); - }); - - it('should not emit event when onlySelf = true', () => { - c.markAsPending({onlySelf: true}); - expect(logger).toEqual([]); - }); - - it('should not emit event when emitEvent = false', () => { - c.markAsPending({emitEvent: false}); - expect(logger).toEqual([]); - }); - - it('should emit event when parent is markedAsPending', () => { - g.markAsPending(); - expect(logger).toEqual(['PENDING']); - }); + g.disable({emitEvent: false}); + expect(logger).toEqual([]); + g.enable({emitEvent: false}); + expect(logger).toEqual([]); }); }); - }); + + 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 as any)._updateTreeValidity(); + expect(logger).toEqual(['one', 'two', 'nested', 'three', 'form']); + }); + + it('should not emit events when turned off', () => { + (form as any)._updateTreeValidity({emitEvent: false}); + expect(logger).toEqual([]); + }); + }); + + describe('setControl()', () => { + let c: FormControl; + let g: FormGroup; + + beforeEach(() => { + c = new FormControl('one'); + g = new FormGroup({one: c}); + }); + + it('should replace existing control with new control', () => { + const c2 = new FormControl('new!', Validators.minLength(10)); + g.setControl('one', c2); + + expect(g.controls['one']).toEqual(c2); + expect(g.value).toEqual({one: 'new!'}); + expect(g.valid).toBe(false); + }); + + it('should add control if control did not exist before', () => { + const c2 = new FormControl('new!', Validators.minLength(10)); + g.setControl('two', c2); + + expect(g.controls['two']).toEqual(c2); + expect(g.value).toEqual({one: 'one', two: 'new!'}); + expect(g.valid).toBe(false); + }); + + it('should remove control if new control is null', () => { + g.setControl('one', null!); + expect(g.controls['one']).not.toBeDefined(); + expect(g.value).toEqual({}); + }); + + it('should only emit value change event once', () => { + const logger: string[] = []; + const c2 = new FormControl('new!'); + g.valueChanges.subscribe(() => logger.push('change!')); + g.setControl('one', c2); + expect(logger).toEqual(['change!']); + }); + }); + + describe('pending', () => { + let c: FormControl; + let g: FormGroup; + + beforeEach(() => { + c = new FormControl('value'); + g = new FormGroup({'one': c}); + }); + + it('should be false after creating a control', () => { + expect(g.pending).toEqual(false); + }); + + it('should be true after changing the value of the control', () => { + c.markAsPending(); + expect(g.pending).toEqual(true); + }); + + it('should not update the parent when onlySelf = true', () => { + c.markAsPending({onlySelf: true}); + expect(g.pending).toEqual(false); + }); + + describe('status change events', () => { + let logger: string[]; + + beforeEach(() => { + logger = []; + g.statusChanges.subscribe((status) => logger.push(status)); + }); + + it('should emit event after marking control as pending', () => { + c.markAsPending(); + expect(logger).toEqual(['PENDING']); + }); + + it('should not emit event when onlySelf = true', () => { + c.markAsPending({onlySelf: true}); + expect(logger).toEqual([]); + }); + + it('should not emit event when emitEvent = false', () => { + c.markAsPending({emitEvent: false}); + expect(logger).toEqual([]); + }); + + it('should emit event when parent is markedAsPending', () => { + g.markAsPending(); + expect(logger).toEqual(['PENDING']); + }); + }); + }); +}); })(); diff --git a/packages/forms/test/reactive_integration_spec.ts b/packages/forms/test/reactive_integration_spec.ts index 9a1b144dda..8e8d5e0f4c 100644 --- a/packages/forms/test/reactive_integration_spec.ts +++ b/packages/forms/test/reactive_integration_spec.ts @@ -7,8 +7,8 @@ */ import {ɵgetDOM as getDOM} from '@angular/common'; -import {Component, Directive, Input, Type, forwardRef} from '@angular/core'; -import {ComponentFixture, TestBed, fakeAsync, tick} from '@angular/core/testing'; +import {Component, Directive, forwardRef, Input, Type} from '@angular/core'; +import {ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing'; import {AbstractControl, AsyncValidator, AsyncValidatorFn, COMPOSITION_BUFFER_MODE, FormArray, FormControl, FormControlDirective, FormControlName, FormGroup, FormGroupDirective, FormsModule, NG_ASYNC_VALIDATORS, NG_VALIDATORS, ReactiveFormsModule, Validators} from '@angular/forms'; import {By} from '@angular/platform-browser/src/dom/debug/by'; import {dispatchEvent, sortedClassList} from '@angular/platform-browser/testing/src/browser_util'; @@ -19,7 +19,6 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec'; { describe('reactive forms integration tests', () => { - function initTest(component: Type, ...directives: Type[]): ComponentFixture { TestBed.configureTestingModule( {declarations: [component, ...directives], imports: [FormsModule, ReactiveFormsModule]}); @@ -74,11 +73,9 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec'; expect(form.value).toEqual({'login': 'updatedValue'}); }); - }); describe('re-bound form groups', () => { - it('should update DOM elements initially', () => { const fixture = initTest(FormGroupComp); fixture.componentInstance.form = new FormGroup({'login': new FormControl('oldValue')}); @@ -150,7 +147,7 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec'; }); fixture.componentInstance.form = form; fixture.detectChanges(); - expect(form.get('login') !.errors).toEqual({required: true}); + expect(form.get('login')!.errors).toEqual({required: true}); const newForm = new FormGroup({ 'login': new FormControl(''), @@ -161,34 +158,31 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec'; fixture.componentInstance.form = newForm; fixture.detectChanges(); - expect(newForm.get('login') !.errors).toEqual({required: true}); + expect(newForm.get('login')!.errors).toEqual({required: true}); }); it('should pick up dir validators from nested form groups', () => { const fixture = initTest(NestedFormGroupComp, LoginIsEmptyValidator); const form = new FormGroup({ - 'signin': - new FormGroup({'login': new FormControl(''), 'password': new FormControl('')}) + 'signin': new FormGroup({'login': new FormControl(''), 'password': new FormControl('')}) }); fixture.componentInstance.form = form; fixture.detectChanges(); - expect(form.get('signin') !.valid).toBe(false); + expect(form.get('signin')!.valid).toBe(false); const newForm = new FormGroup({ - 'signin': - new FormGroup({'login': new FormControl(''), 'password': new FormControl('')}) + 'signin': new FormGroup({'login': new FormControl(''), 'password': new FormControl('')}) }); fixture.componentInstance.form = newForm; fixture.detectChanges(); - expect(form.get('signin') !.valid).toBe(false); + expect(form.get('signin')!.valid).toBe(false); }); it('should strip named controls that are not found', () => { const fixture = initTest(NestedFormGroupComp, LoginIsEmptyValidator); const form = new FormGroup({ - 'signin': - new FormGroup({'login': new FormControl(''), 'password': new FormControl('')}) + 'signin': new FormGroup({'login': new FormControl(''), 'password': new FormControl('')}) }); fixture.componentInstance.form = form; fixture.detectChanges(); @@ -200,8 +194,7 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec'; expect(emailInput.nativeElement.value).toEqual('email'); const newForm = new FormGroup({ - 'signin': - new FormGroup({'login': new FormControl(''), 'password': new FormControl('')}) + 'signin': new FormGroup({'login': new FormControl(''), 'password': new FormControl('')}) }); fixture.componentInstance.form = newForm; fixture.detectChanges(); @@ -237,7 +230,6 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec'; }); describe('nested control rebinding', () => { - it('should attach dir to control when leaf control changes', () => { const form = new FormGroup({'login': new FormControl('oldValue')}); const fixture = initTest(FormGroupComp); @@ -359,7 +351,7 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec'; expect(newArr.value).toEqual(['last one']); - newArr.get([0]) !.setValue('set value'); + newArr.get([0])!.setValue('set value'); fixture.detectChanges(); firstInput = fixture.debugElement.query(By.css('input')).nativeElement; @@ -416,14 +408,12 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec'; expect(newArr.value).toEqual(['SF', 'LA', 'Tulsa']); - newArr.get([2]) !.setValue('NY'); + newArr.get([2])!.setValue('NY'); fixture.detectChanges(); expect(lastInput.value).toEqual('NY'); }); - }); - }); describe('form arrays', () => { @@ -494,7 +484,6 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec'; cities: [{town: 'LA', state: 'CA'}, {town: 'NY', state: 'NY'}] }); }); - }); describe('programmatic changes', () => { @@ -592,13 +581,10 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec'; const input = fixture.debugElement.query(By.css('my-input')); expect(input.nativeElement.getAttribute('disabled')).toBe(null); }); - }); - }); describe('user input', () => { - it('should mark controls as touched after interacting with the DOM control', () => { const fixture = initTest(FormGroupComp); const login = new FormControl('oldValue'); @@ -613,14 +599,13 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec'; expect(login.touched).toBe(true); }); - }); describe('submit and reset events', () => { it('should emit ngSubmit event with the original submit event on submit', () => { const fixture = initTest(FormGroupComp); fixture.componentInstance.form = new FormGroup({'login': new FormControl('loginValue')}); - fixture.componentInstance.event = null !; + fixture.componentInstance.event = null!; fixture.detectChanges(); const formEl = fixture.debugElement.query(By.css('form')).nativeElement; @@ -672,18 +657,18 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec'; form.reset(); expect(loginEl.value).toBe(''); }); - }); describe('value changes and status changes', () => { - it('should mark controls as dirty before emitting a value change event', () => { const fixture = initTest(FormGroupComp); const login = new FormControl('oldValue'); fixture.componentInstance.form = new FormGroup({'login': login}); fixture.detectChanges(); - login.valueChanges.subscribe(() => { expect(login.dirty).toBe(true); }); + login.valueChanges.subscribe(() => { + expect(login.dirty).toBe(true); + }); const loginEl = fixture.debugElement.query(By.css('input')).nativeElement; loginEl.value = 'newValue'; @@ -705,11 +690,12 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec'; expect(login.pristine).toBe(false); - login.valueChanges.subscribe(() => { expect(login.pristine).toBe(true); }); + login.valueChanges.subscribe(() => { + expect(login.pristine).toBe(true); + }); form.reset(); }); - }); describe('setting status classes', () => { @@ -736,7 +722,7 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec'; it('should work with single fields and async validators', fakeAsync(() => { const fixture = initTest(FormControlComp); - const control = new FormControl('', null !, uniqLoginAsyncValidator('good')); + const control = new FormControl('', null!, uniqLoginAsyncValidator('good')); fixture.debugElement.componentInstance.control = control; fixture.detectChanges(); @@ -831,13 +817,10 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec'; expect(sortedClassList(formEl)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']); }); - }); describe('updateOn options', () => { - describe('on blur', () => { - it('should not update value or validity based on user input until blur', () => { const fixture = initTest(FormControlComp); const control = new FormControl('', {validators: Validators.required, updateOn: 'blur'}); @@ -1141,7 +1124,6 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec'; expect(control.value) .toEqual('Nancy', 'Expected value to change once control is blurred.'); expect(control.valid).toBe(true, 'Expected validation to run once control is blurred.'); - }); it('should update on blur with array updateOn', () => { @@ -1167,7 +1149,6 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec'; expect(control.value) .toEqual('Nancy', 'Expected value to change once control is blurred.'); expect(control.valid).toBe(true, 'Expected validation to run once control is blurred.'); - }); @@ -1206,17 +1187,13 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec'; expect(passwordControl.valid) .toBe(true, 'Expected validation to run once control is blurred.'); }); - - }); describe('on submit', () => { - it('should set initial value and validity on init', () => { const fixture = initTest(FormGroupComp); const form = new FormGroup({ - login: - new FormControl('Nancy', {validators: Validators.required, updateOn: 'submit'}) + login: new FormControl('Nancy', {validators: Validators.required, updateOn: 'submit'}) }); fixture.componentInstance.form = form; fixture.detectChanges(); @@ -1430,7 +1407,7 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec'; fixture.componentInstance.form = formGroup; fixture.detectChanges(); - const values: (string | {[key: string]: string})[] = []; + const values: (string|{[key: string]: string})[] = []; const streams = merge( control.valueChanges, control.statusChanges, formGroup.valueChanges, formGroup.statusChanges); @@ -1493,8 +1470,8 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec'; fixture.componentInstance.form = formGroup; fixture.detectChanges(); - formGroup.get('signin.login') !.setValidators(validatorSpy); - formGroup.get('signin') !.setValidators(groupValidatorSpy); + formGroup.get('signin.login')!.setValidators(validatorSpy); + formGroup.get('signin')!.setValidators(groupValidatorSpy); const form = fixture.debugElement.query(By.css('form')).nativeElement; dispatchEvent(form, 'submit'); @@ -1502,7 +1479,6 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec'; expect(validatorSpy).not.toHaveBeenCalled(); expect(groupValidatorSpy).not.toHaveBeenCalled(); - }); it('should mark as untouched properly if pending touched', () => { @@ -1554,7 +1530,6 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec'; expect(control.value).toEqual('Nancy', 'Expected value to change on submit.'); expect(control.valid).toBe(true, 'Expected validation to run on submit.'); - }); it('should update on submit with array updateOn', () => { @@ -1581,7 +1556,6 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec'; expect(control.value).toEqual('Nancy', 'Expected value to change once control on submit'); expect(control.valid).toBe(true, 'Expected validation to run on submit.'); - }); it('should allow child control updateOn submit to override group updateOn', () => { @@ -1619,9 +1593,7 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec'; expect(passwordControl.value).toEqual('Carson', 'Expected value to change on submit.'); expect(passwordControl.valid).toBe(true, 'Expected validation to run on submit.'); }); - }); - }); describe('ngModel interactions', () => { @@ -1636,7 +1608,6 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec'; }); describe('deprecation warnings', () => { - it('should warn once by default when using ngModel with formControlName', fakeAsync(() => { const fixture = initTest(FormGroupNgModel); fixture.componentInstance.form = @@ -1679,8 +1650,7 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec'; fakeAsync(() => { TestBed.configureTestingModule({ declarations: [FormControlNgModel], - imports: - [ReactiveFormsModule.withConfig({warnOnNgModelWithFormControl: 'always'})] + imports: [ReactiveFormsModule.withConfig({warnOnNgModelWithFormControl: 'always'})] }); const fixture = TestBed.createComponent(FormControlNgModel); @@ -1710,7 +1680,6 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec'; expect(warnSpy).not.toHaveBeenCalled(); })); - }); it('should support ngModel for complex forms', fakeAsync(() => { @@ -1794,9 +1763,7 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec'; expect(fixture.componentInstance.login) .toEqual('Nancy', 'Expected ngModel value to update on submit.'); - })); - }); describe('validations', () => { @@ -1969,9 +1936,9 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec'; .toEqual(pattern.nativeElement.getAttribute('pattern')); fixture.componentInstance.required = false; - fixture.componentInstance.minLen = null !; - fixture.componentInstance.maxLen = null !; - fixture.componentInstance.pattern = null !; + fixture.componentInstance.minLen = null!; + fixture.componentInstance.maxLen = null!; + fixture.componentInstance.pattern = null!; fixture.detectChanges(); expect(form.hasError('required', ['login'])).toEqual(false); @@ -2011,9 +1978,9 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec'; fixture.detectChanges(); fixture.componentInstance.required = false; - fixture.componentInstance.minLen = null !; - fixture.componentInstance.maxLen = null !; - fixture.componentInstance.pattern = null !; + fixture.componentInstance.minLen = null!; + fixture.componentInstance.maxLen = null!; + fixture.componentInstance.pattern = null!; fixture.detectChanges(); expect(newForm.hasError('required', ['login'])).toEqual(false); @@ -2111,7 +2078,7 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec'; const fixture = initTest(FormControlComp); const resultArr: number[] = []; fixture.componentInstance.control = - new FormControl('', null !, observableValidator(resultArr)); + new FormControl('', null!, observableValidator(resultArr)); fixture.detectChanges(); tick(100); @@ -2130,12 +2097,9 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec'; expect(resultArr.length) .toEqual(2, `Expected original observable to be canceled on the next value change.`); })); - - }); describe('errors', () => { - it('should throw if a form isn\'t passed into formGroup', () => { const fixture = initTest(FormGroupComp); @@ -2335,11 +2299,9 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec'; expect(() => fixture.detectChanges()) .toThrowError(new RegExp('If you define both a name and a formControlName')); }); - }); describe('IME events', () => { - it('should determine IME event handling depending on platform by default', () => { const fixture = initTest(FormControlComp); fixture.componentInstance.control = new FormControl('oldValue'); @@ -2417,16 +2379,16 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec'; // formControl should update normally expect(fixture.componentInstance.control.value).toEqual('updatedValue'); }); - }); - }); } function uniqLoginAsyncValidator(expectedValue: string, timeout: number = 0) { return (c: AbstractControl) => { let resolve: (result: any) => void; - const promise = new Promise(res => { resolve = res; }); + const promise = new Promise(res => { + resolve = res; + }); const res = (c.value == expectedValue) ? null : {'uniqLogin': true}; setTimeout(() => resolve(res), timeout); return promise; @@ -2452,22 +2414,22 @@ class LoginIsEmptyValidator { @Directive({ selector: '[uniq-login-validator]', - providers: [{ - provide: NG_ASYNC_VALIDATORS, - useExisting: forwardRef(() => UniqLoginValidator), - multi: true - }] + providers: [ + {provide: NG_ASYNC_VALIDATORS, useExisting: forwardRef(() => UniqLoginValidator), multi: true} + ] }) class UniqLoginValidator implements AsyncValidator { @Input('uniq-login-validator') expected: any; - validate(c: AbstractControl) { return uniqLoginAsyncValidator(this.expected)(c); } + validate(c: AbstractControl) { + return uniqLoginAsyncValidator(this.expected)(c); + } } @Component({selector: 'form-control-comp', template: ``}) class FormControlComp { // TODO(issue/24571): remove '!'. - control !: FormControl; + control!: FormControl; } @Component({ @@ -2479,11 +2441,11 @@ class FormControlComp { }) class FormGroupComp { // TODO(issue/24571): remove '!'. - control !: FormControl; + control!: FormControl; // TODO(issue/24571): remove '!'. - form !: FormGroup; + form!: FormGroup; // TODO(issue/24571): remove '!'. - event !: Event; + event!: Event; } @Component({ @@ -2499,7 +2461,7 @@ class FormGroupComp { }) class NestedFormGroupComp { // TODO(issue/24571): remove '!'. - form !: FormGroup; + form!: FormGroup; } @Component({ @@ -2515,9 +2477,9 @@ class NestedFormGroupComp { }) class FormArrayComp { // TODO(issue/24571): remove '!'. - form !: FormGroup; + form!: FormGroup; // TODO(issue/24571): remove '!'. - cityArray !: FormArray; + cityArray!: FormArray; } @Component({ @@ -2534,9 +2496,9 @@ class FormArrayComp { }) class FormArrayNestedGroup { // TODO(issue/24571): remove '!'. - form !: FormGroup; + form!: FormGroup; // TODO(issue/24571): remove '!'. - cityArray !: FormArray; + cityArray!: FormArray; } @Component({ @@ -2549,11 +2511,11 @@ class FormArrayNestedGroup { }) class FormGroupNgModel { // TODO(issue/24571): remove '!'. - form !: FormGroup; + form!: FormGroup; // TODO(issue/24571): remove '!'. - login !: string; + login!: string; // TODO(issue/24571): remove '!'. - password !: string; + password!: string; } @Component({ @@ -2565,13 +2527,13 @@ class FormGroupNgModel { }) class FormControlNgModel { // TODO(issue/24571): remove '!'. - control !: FormControl; + control!: FormControl; // TODO(issue/24571): remove '!'. - login !: string; + login!: string; // TODO(issue/24571): remove '!'. - passwordControl !: FormControl; + passwordControl!: FormControl; // TODO(issue/24571): remove '!'. - password !: string; + password!: string; } @Component({ @@ -2586,7 +2548,7 @@ class FormControlNgModel { }) class LoginIsEmptyWrapper { // TODO(issue/24571): remove '!'. - form !: FormGroup; + form!: FormGroup; } @Component({ @@ -2601,15 +2563,15 @@ class LoginIsEmptyWrapper { }) class ValidationBindingsForm { // TODO(issue/24571): remove '!'. - form !: FormGroup; + form!: FormGroup; // TODO(issue/24571): remove '!'. - required !: boolean; + required!: boolean; // TODO(issue/24571): remove '!'. - minLen !: number; + minLen!: number; // TODO(issue/24571): remove '!'. - maxLen !: number; + maxLen!: number; // TODO(issue/24571): remove '!'. - pattern !: string; + pattern!: string; } @Component({ @@ -2618,7 +2580,7 @@ class ValidationBindingsForm { }) class FormControlCheckboxRequiredValidator { // TODO(issue/24571): remove '!'. - control !: FormControl; + control!: FormControl; } @Component({ @@ -2630,5 +2592,5 @@ class FormControlCheckboxRequiredValidator { }) class UniqLoginWrapper { // TODO(issue/24571): remove '!'. - form !: FormGroup; + form!: FormGroup; } diff --git a/packages/forms/test/spies.ts b/packages/forms/test/spies.ts index 1f6b3940c7..27bbcd0d98 100644 --- a/packages/forms/test/spies.ts +++ b/packages/forms/test/spies.ts @@ -16,6 +16,10 @@ export class SpyChangeDetectorRef extends SpyObject { } } -export class SpyNgControl extends SpyObject { path = []; } +export class SpyNgControl extends SpyObject { + path = []; +} -export class SpyValueAccessor extends SpyObject { writeValue: any; } +export class SpyValueAccessor extends SpyObject { + writeValue: any; +} diff --git a/packages/forms/test/template_integration_spec.ts b/packages/forms/test/template_integration_spec.ts index a78ad61c26..0cde9665d1 100644 --- a/packages/forms/test/template_integration_spec.ts +++ b/packages/forms/test/template_integration_spec.ts @@ -7,8 +7,8 @@ */ import {ɵgetDOM as getDOM} from '@angular/common'; -import {Component, Directive, Type, forwardRef} from '@angular/core'; -import {ComponentFixture, TestBed, async, fakeAsync, tick} from '@angular/core/testing'; +import {Component, Directive, forwardRef, Type} from '@angular/core'; +import {async, ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing'; import {AbstractControl, AsyncValidator, COMPOSITION_BUFFER_MODE, FormControl, FormsModule, NG_ASYNC_VALIDATORS, NgForm, NgModel} from '@angular/forms'; import {By} from '@angular/platform-browser/src/dom/debug/by'; import {dispatchEvent, sortedClassList} from '@angular/platform-browser/testing/src/browser_util'; @@ -18,7 +18,6 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat { describe('template-driven forms integration tests', () => { - function initTest(component: Type, ...directives: Type[]): ComponentFixture { TestBed.configureTestingModule( {declarations: [component, ...directives], imports: [FormsModule]}); @@ -57,7 +56,7 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat expect(form.valid).toBe(false); })); - it('should report properties which are written outside of template bindings', async() => { + it('should report properties which are written outside of template bindings', async () => { // For example ngModel writes to `checked` property programmatically // (template does not contain binding to `checked` explicitly) // https://github.com/angular/angular/issues/33695 @@ -131,9 +130,9 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat tick(); const form = fixture.debugElement.children[0].injector.get(NgForm); - expect(form.control.get('name') !.value).toEqual({first: 'Nancy', last: 'Drew'}); - expect(form.control.get('name.first') !.value).toEqual('Nancy'); - expect(form.control.get('email') !.value).toEqual('some email'); + expect(form.control.get('name')!.value).toEqual({first: 'Nancy', last: 'Drew'}); + expect(form.control.get('name.first')!.value).toEqual('Nancy'); + expect(form.control.get('email')!.value).toEqual('some email'); })); it('should remove controls and control groups from form control model', fakeAsync(() => { @@ -145,7 +144,7 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat tick(); const form = fixture.debugElement.children[0].injector.get(NgForm); - expect(form.control.get('email') !.value).toEqual('some email'); + expect(form.control.get('email')!.value).toEqual('some email'); expect(form.value).toEqual({name: {first: 'Nancy'}, email: 'some email'}); // should remove individual control successfully @@ -156,8 +155,8 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat expect(form.control.get('email')).toBe(null); expect(form.value).toEqual({name: {first: 'Nancy'}}); - expect(form.control.get('name') !.value).toEqual({first: 'Nancy'}); - expect(form.control.get('name.first') !.value).toEqual('Nancy'); + expect(form.control.get('name')!.value).toEqual({first: 'Nancy'}); + expect(form.control.get('name.first')!.value).toEqual('Nancy'); // should remove form group successfully fixture.componentInstance.groupShowing = false; @@ -192,7 +191,6 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat })); it('should set status classes with ngModel and async validators', fakeAsync(() => { - const fixture = initTest(NgModelAsyncValidation, NgAsyncValidator); fixture.whenStable().then(() => { fixture.detectChanges(); @@ -252,7 +250,7 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat it('should not create a template-driven form when ngNoForm is used', () => { const fixture = initTest(NgNoFormComp); fixture.detectChanges(); - expect(fixture.debugElement.children[0].providerTokens !.length).toEqual(0); + expect(fixture.debugElement.children[0].providerTokens!.length).toEqual(0); }); it('should not add novalidate when ngNoForm is used', () => { @@ -304,9 +302,7 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat }); describe('updateOn', () => { - describe('blur', () => { - it('should default updateOn to change', fakeAsync(() => { const fixture = initTest(NgModelForm); fixture.componentInstance.name = ''; @@ -484,8 +480,8 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat const values: any[] = []; const form = fixture.debugElement.children[0].injector.get(NgForm); - const sub = merge(form.valueChanges !, form.statusChanges !) - .subscribe(val => values.push(val)); + const sub = + merge(form.valueChanges!, form.statusChanges!).subscribe(val => values.push(val)); const input = fixture.debugElement.query(By.css('input')).nativeElement; input.value = 'Nancy Drew'; @@ -560,13 +556,10 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat .toEqual( ['fired', 'fired'], 'Expected ngModelChanges to fire again on blur if value changed.'); - })); - }); describe('submit', () => { - it('should set control updateOn to submit properly', fakeAsync(() => { const fixture = initTest(NgModelForm); fixture.componentInstance.name = ''; @@ -700,8 +693,8 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat tick(); const form = fixture.debugElement.children[0].injector.get(NgForm); - form.control.get('name') !.setValidators(groupValidatorSpy); - form.control.get('name.last') !.setValidators(validatorSpy); + form.control.get('name')!.setValidators(groupValidatorSpy); + form.control.get('name.last')!.setValidators(validatorSpy); const formEl = fixture.debugElement.query(By.css('form')).nativeElement; dispatchEvent(formEl, 'submit'); @@ -816,8 +809,8 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat const values: any[] = []; const form = fixture.debugElement.children[0].injector.get(NgForm); - const sub = merge(form.valueChanges !, form.statusChanges !) - .subscribe(val => values.push(val)); + const sub = + merge(form.valueChanges!, form.statusChanges!).subscribe(val => values.push(val)); const input = fixture.debugElement.query(By.css('input')).nativeElement; input.value = 'Nancy Drew'; @@ -898,11 +891,9 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat ['fired', 'fired'], 'Expected ngModelChanges to fire again on submit if value changed.'); })); - }); describe('ngFormOptions', () => { - it('should use ngFormOptions value when ngModelOptions are not set', fakeAsync(() => { const fixture = initTest(NgModelOptionsStandalone); fixture.componentInstance.options = {name: 'two'}; @@ -911,12 +902,12 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat tick(); const form = fixture.debugElement.children[0].injector.get(NgForm); - const controlOne = form.control.get('one') !as FormControl; + const controlOne = form.control.get('one')! as FormControl; expect((controlOne as any)._updateOn).toBeUndefined(); expect(controlOne.updateOn) .toEqual('blur', 'Expected first control to inherit updateOn from parent form.'); - const controlTwo = form.control.get('two') !as FormControl; + const controlTwo = form.control.get('two')! as FormControl; expect((controlTwo as any)._updateOn).toBeUndefined(); expect(controlTwo.updateOn) .toEqual('blur', 'Expected last control to inherit updateOn from parent form.'); @@ -952,12 +943,12 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat tick(); const form = fixture.debugElement.children[0].injector.get(NgForm); - const controlOne = form.control.get('one') !as FormControl; + const controlOne = form.control.get('one')! as FormControl; expect((controlOne as any)._updateOn).toBeUndefined(); expect(controlOne.updateOn) .toEqual('change', 'Expected control updateOn to inherit form updateOn.'); - const controlTwo = form.control.get('two') !as FormControl; + const controlTwo = form.control.get('two')! as FormControl; expect((controlTwo as any)._updateOn) .toEqual('blur', 'Expected control to set blur override.'); expect(controlTwo.updateOn) @@ -1016,15 +1007,13 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat expect(fixture.componentInstance.two) .toEqual('Nancy Drew', 'Expected standalone ngModel not to inherit blur update.'); })); - }); - }); describe('submit and reset events', () => { it('should emit ngSubmit event with the original submit event on submit', fakeAsync(() => { const fixture = initTest(NgModelForm); - fixture.componentInstance.event = null !; + fixture.componentInstance.event = null!; const form = fixture.debugElement.query(By.css('form')); dispatchEvent(form.nativeElement, 'submit'); @@ -1097,11 +1086,11 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat expect(form.valid).toEqual(true); expect(form.value).toEqual({}); - let formValidity: string = undefined !; - let formValue: Object = undefined !; + let formValidity: string = undefined!; + let formValue: Object = undefined!; - form.statusChanges !.subscribe((status: string) => formValidity = status); - form.valueChanges !.subscribe((value: string) => formValue = value); + form.statusChanges!.subscribe((status: string) => formValidity = status); + form.valueChanges!.subscribe((value: string) => formValue = value); tick(); @@ -1116,8 +1105,9 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat fixture.detectChanges(); tick(); - form.get('name') !.valueChanges.subscribe( - () => { expect(form.get('name') !.dirty).toBe(true); }); + form.get('name')!.valueChanges.subscribe(() => { + expect(form.get('name')!.dirty).toBe(true); + }); const inputEl = fixture.debugElement.query(By.css('input')).nativeElement; inputEl.value = 'newValue'; @@ -1138,10 +1128,11 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat inputEl.value = 'newValue'; dispatchEvent(inputEl, 'input'); - expect(form.get('name') !.pristine).toBe(false); + expect(form.get('name')!.pristine).toBe(false); - form.get('name') !.valueChanges.subscribe( - () => { expect(form.get('name') !.pristine).toBe(true); }); + form.get('name')!.valueChanges.subscribe(() => { + expect(form.get('name')!.pristine).toBe(true); + }); dispatchEvent(formEl, 'reset'); })); @@ -1160,7 +1151,7 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat const form = fixture.debugElement.children[0].injector.get(NgForm); expect(form.value).toEqual({name: {first: '', last: 'Drew'}, email: 'some email'}); expect(form.valid).toBe(false); - expect(form.control.get('name.first') !.disabled).toBe(false); + expect(form.control.get('name.first')!.disabled).toBe(false); fixture.componentInstance.isDisabled = true; fixture.detectChanges(); @@ -1168,7 +1159,7 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat expect(form.value).toEqual({name: {last: 'Drew'}, email: 'some email'}); expect(form.valid).toBe(true); - expect(form.control.get('name.first') !.disabled).toBe(true); + expect(form.control.get('name.first')!.disabled).toBe(true); })); it('should add disabled attribute in the UI if disable() is called programmatically', @@ -1180,7 +1171,7 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat tick(); const form = fixture.debugElement.children[0].injector.get(NgForm); - form.control.get('name.first') !.disable(); + form.control.get('name.first')!.disable(); fixture.detectChanges(); tick(); @@ -1197,7 +1188,7 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat fixture.detectChanges(); fixture.whenStable().then(() => { const form = fixture.debugElement.children[0].injector.get(NgForm); - expect(form.control.get('name') !.disabled).toBe(true); + expect(form.control.get('name')!.disabled).toBe(true); const customInput = fixture.debugElement.query(By.css('[name="custom"]')); expect(customInput.nativeElement.disabled).toEqual(true); @@ -1219,7 +1210,7 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat fixture.detectChanges(); tick(); const form = fixture.debugElement.children[0].injector.get(NgForm); - expect(form.control.get('name') !.disabled).toBe(true); + expect(form.control.get('name')!.disabled).toBe(true); const input = fixture.debugElement.query(By.css('input')); expect(input.nativeElement.disabled).toEqual(true); @@ -1229,18 +1220,16 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat tick(); expect(input.nativeElement.disabled).toEqual(false); })); - }); describe('validation directives', () => { - it('required validator should validate checkbox', fakeAsync(() => { const fixture = initTest(NgModelCheckboxRequiredValidator); fixture.detectChanges(); tick(); const control = - fixture.debugElement.children[0].injector.get(NgForm).control.get('checkbox') !; + fixture.debugElement.children[0].injector.get(NgForm).control.get('checkbox')!; const input = fixture.debugElement.query(By.css('input')); expect(input.nativeElement.checked).toBe(false); @@ -1276,7 +1265,7 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat tick(); const control = - fixture.debugElement.children[0].injector.get(NgForm).control.get('email') !; + fixture.debugElement.children[0].injector.get(NgForm).control.get('email')!; const input = fixture.debugElement.query(By.css('input')); expect(control.hasError('email')).toBe(false); @@ -1476,9 +1465,9 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat .toEqual(pattern.nativeElement.getAttribute('pattern')); fixture.componentInstance.required = false; - fixture.componentInstance.minLen = null !; - fixture.componentInstance.maxLen = null !; - fixture.componentInstance.pattern = null !; + fixture.componentInstance.minLen = null!; + fixture.componentInstance.maxLen = null!; + fixture.componentInstance.pattern = null!; fixture.detectChanges(); expect(form.control.hasError('required', ['required'])).toEqual(false); @@ -1522,7 +1511,6 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat expect(onNgModelChange).toHaveBeenCalledTimes(2); tick(); })); - }); describe('IME events', () => { @@ -1611,7 +1599,6 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat // ngModel should update normally expect(fixture.componentInstance.name).toEqual('updatedValue'); })); - }); describe('ngModel corner cases', () => { @@ -1650,7 +1637,6 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat expect(() => fixture.detectChanges()).not.toThrowError(); })); }); - }); } @@ -1662,7 +1648,7 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat }) class StandaloneNgModel { // TODO(issue/24571): remove '!'. - name !: string; + name!: string; } @Component({ @@ -1675,9 +1661,9 @@ class StandaloneNgModel { }) class NgModelForm { // TODO(issue/24571): remove '!'. - name !: string | null; + name!: string|null; // TODO(issue/24571): remove '!'. - event !: Event; + event!: Event; options = {}; onReset() {} @@ -1701,13 +1687,13 @@ class NgModelNativeValidateForm { }) class NgModelGroupForm { // TODO(issue/24571): remove '!'. - first !: string; + first!: string; // TODO(issue/24571): remove '!'. - last !: string; + last!: string; // TODO(issue/24571): remove '!'. - email !: string; + email!: string; // TODO(issue/24571): remove '!'. - isDisabled !: boolean; + isDisabled!: boolean; options = {updateOn: 'change'}; } @@ -1724,7 +1710,7 @@ class NgModelGroupForm { }) class NgModelValidBinding { // TODO(issue/24571): remove '!'. - first !: string; + first!: string; } @@ -1741,11 +1727,11 @@ class NgModelValidBinding { }) class NgModelNgIfForm { // TODO(issue/24571): remove '!'. - first !: string; + first!: string; groupShowing = true; emailShowing = true; // TODO(issue/24571): remove '!'. - email !: string; + email!: string; } @Component({ @@ -1781,9 +1767,9 @@ class InvalidNgModelNoName { }) class NgModelOptionsStandalone { // TODO(issue/24571): remove '!'. - one !: string; + one!: string; // TODO(issue/24571): remove '!'. - two !: string; + two!: string; options: {name?: string, standalone?: boolean, updateOn?: string} = {standalone: true}; formOptions = {}; } @@ -1801,13 +1787,13 @@ class NgModelOptionsStandalone { }) class NgModelValidationBindings { // TODO(issue/24571): remove '!'. - required !: boolean; + required!: boolean; // TODO(issue/24571): remove '!'. - minLen !: number; + minLen!: number; // TODO(issue/24571): remove '!'. - maxLen !: number; + maxLen!: number; // TODO(issue/24571): remove '!'. - pattern !: string; + pattern!: string; } @Component({ @@ -1820,11 +1806,11 @@ class NgModelValidationBindings { }) class NgModelMultipleValidators { // TODO(issue/24571): remove '!'. - required !: boolean; + required!: boolean; // TODO(issue/24571): remove '!'. - minLen !: number; + minLen!: number; // TODO(issue/24571): remove '!'. - pattern !: string | RegExp; + pattern!: string|RegExp; } @Component({ @@ -1847,12 +1833,13 @@ class NgModelEmailValidator { @Directive({ selector: '[ng-async-validator]', - providers: [ - {provide: NG_ASYNC_VALIDATORS, useExisting: forwardRef(() => NgAsyncValidator), multi: true} - ] + providers: + [{provide: NG_ASYNC_VALIDATORS, useExisting: forwardRef(() => NgAsyncValidator), multi: true}] }) class NgAsyncValidator implements AsyncValidator { - validate(c: AbstractControl) { return Promise.resolve(null); } + validate(c: AbstractControl) { + return Promise.resolve(null); + } } @Component({ @@ -1873,11 +1860,13 @@ class NgModelAsyncValidation { }) class NgModelChangesForm { // TODO(issue/24571): remove '!'. - name !: string; + name!: string; events: string[] = []; options: any; - log() { this.events.push('fired'); } + log() { + this.events.push('fired'); + } } @Component({ diff --git a/packages/forms/test/validators_spec.ts b/packages/forms/test/validators_spec.ts index 0d3f118965..0a6719421b 100644 --- a/packages/forms/test/validators_spec.ts +++ b/packages/forms/test/validators_spec.ts @@ -11,453 +11,487 @@ import {describe, expect, it} from '@angular/core/testing/src/testing_internal'; import {AbstractControl, AsyncValidatorFn, FormArray, FormControl, Validators} from '@angular/forms'; import {normalizeAsyncValidator} from '@angular/forms/src/directives/normalize_validator'; import {AsyncValidator, ValidationErrors, ValidatorFn} from '@angular/forms/src/directives/validators'; -import {Observable, of , timer} from 'rxjs'; +import {Observable, of, timer} from 'rxjs'; import {first, map} from 'rxjs/operators'; (function() { - function validator(key: string, error: any): ValidatorFn { - return (c: AbstractControl) => { - const r: ValidationErrors = {}; - r[key] = error; - return r; - }; +function validator(key: string, error: any): ValidatorFn { + return (c: AbstractControl) => { + const r: ValidationErrors = {}; + r[key] = error; + return r; + }; +} + +class AsyncValidatorDirective implements AsyncValidator { + constructor(private expected: string, private error: any) {} + + validate(c: any): Observable { + return Observable.create((obs: any) => { + const error = this.expected !== c.value ? this.error : null; + obs.next(error); + obs.complete(); + }); } +} - class AsyncValidatorDirective implements AsyncValidator { - constructor(private expected: string, private error: any) {} - - validate(c: any): Observable { - return Observable.create((obs: any) => { - const error = this.expected !== c.value ? this.error : null; - obs.next(error); - obs.complete(); - }); - } - } - - describe('Validators', () => { - describe('min', () => { - it('should not error on an empty string', - () => { expect(Validators.min(2)(new FormControl(''))).toBeNull(); }); - - it('should not error on null', - () => { expect(Validators.min(2)(new FormControl(null))).toBeNull(); }); - - it('should not error on undefined', - () => { expect(Validators.min(2)(new FormControl(undefined))).toBeNull(); }); - - it('should return null if NaN after parsing', - () => { expect(Validators.min(2)(new FormControl('a'))).toBeNull(); }); - - it('should return a validation error on small values', () => { - expect(Validators.min(2)(new FormControl(1))).toEqual({'min': {'min': 2, 'actual': 1}}); - }); - - it('should return a validation error on small values converted from strings', () => { - expect(Validators.min(2)(new FormControl('1'))).toEqual({'min': {'min': 2, 'actual': '1'}}); - }); - - it('should not error on big values', - () => { expect(Validators.min(2)(new FormControl(3))).toBeNull(); }); - - it('should not error on equal values', - () => { expect(Validators.min(2)(new FormControl(2))).toBeNull(); }); - - it('should not error on equal values when value is string', - () => { expect(Validators.min(2)(new FormControl('2'))).toBeNull(); }); - - it('should validate as expected when min value is a string', () => { - expect(Validators.min('2' as any)(new FormControl(1))).toEqual({ - 'min': {'min': '2', 'actual': 1} - }); - }); - - it('should return null if min value is undefined', - () => { expect(Validators.min(undefined as any)(new FormControl(3))).toBeNull(); }); - - it('should return null if min value is null', - () => { expect(Validators.min(null as any)(new FormControl(3))).toBeNull(); }); +describe('Validators', () => { + describe('min', () => { + it('should not error on an empty string', () => { + expect(Validators.min(2)(new FormControl(''))).toBeNull(); }); - describe('max', () => { - it('should not error on an empty string', - () => { expect(Validators.max(2)(new FormControl(''))).toBeNull(); }); - - it('should not error on null', - () => { expect(Validators.max(2)(new FormControl(null))).toBeNull(); }); - - it('should not error on undefined', - () => { expect(Validators.max(2)(new FormControl(undefined))).toBeNull(); }); - - it('should return null if NaN after parsing', - () => { expect(Validators.max(2)(new FormControl('aaa'))).toBeNull(); }); - - it('should return a validation error on big values', () => { - expect(Validators.max(2)(new FormControl(3))).toEqual({'max': {'max': 2, 'actual': 3}}); - }); - - it('should return a validation error on big values converted from strings', () => { - expect(Validators.max(2)(new FormControl('3'))).toEqual({'max': {'max': 2, 'actual': '3'}}); - }); - - it('should not error on small values', - () => { expect(Validators.max(2)(new FormControl(1))).toBeNull(); }); - - it('should not error on equal values', - () => { expect(Validators.max(2)(new FormControl(2))).toBeNull(); }); - - it('should not error on equal values when value is string', - () => { expect(Validators.max(2)(new FormControl('2'))).toBeNull(); }); - - it('should validate as expected when max value is a string', () => { - expect(Validators.max('2' as any)(new FormControl(3))).toEqual({ - 'max': {'max': '2', 'actual': 3} - }); - }); - - it('should return null if max value is undefined', - () => { expect(Validators.max(undefined as any)(new FormControl(3))).toBeNull(); }); - - it('should return null if max value is null', - () => { expect(Validators.max(null as any)(new FormControl(3))).toBeNull(); }); + it('should not error on null', () => { + expect(Validators.min(2)(new FormControl(null))).toBeNull(); }); - - describe('required', () => { - it('should error on an empty string', - () => { expect(Validators.required(new FormControl(''))).toEqual({'required': true}); }); - - it('should error on null', - () => { expect(Validators.required(new FormControl(null))).toEqual({'required': true}); }); - - it('should not error on undefined', () => { - expect(Validators.required(new FormControl(undefined))).toEqual({'required': true}); - }); - - it('should not error on a non-empty string', - () => { expect(Validators.required(new FormControl('not empty'))).toBeNull(); }); - - it('should accept zero as valid', - () => { expect(Validators.required(new FormControl(0))).toBeNull(); }); - - it('should error on an empty array', - () => expect(Validators.required(new FormControl([]))).toEqual({'required': true})); - - it('should not error on a non-empty array', - () => expect(Validators.required(new FormControl([1, 2]))).toBeNull()); + it('should not error on undefined', () => { + expect(Validators.min(2)(new FormControl(undefined))).toBeNull(); }); - describe('requiredTrue', () => { - it('should error on false', - () => expect(Validators.requiredTrue(new FormControl(false))).toEqual({'required': true})); - - it('should not error on true', - () => expect(Validators.requiredTrue(new FormControl(true))).toBeNull()); + it('should return null if NaN after parsing', () => { + expect(Validators.min(2)(new FormControl('a'))).toBeNull(); }); - describe('email', () => { - it('should not error on an empty string', - () => expect(Validators.email(new FormControl(''))).toBeNull()); - - it('should not error on null', - () => expect(Validators.email(new FormControl(null))).toBeNull()); - - it('should error on invalid email', - () => expect(Validators.email(new FormControl('some text'))).toEqual({'email': true})); - - it('should not error on valid email', - () => expect(Validators.email(new FormControl('test@gmail.com'))).toBeNull()); + it('should return a validation error on small values', () => { + expect(Validators.min(2)(new FormControl(1))).toEqual({'min': {'min': 2, 'actual': 1}}); }); - describe('minLength', () => { - it('should not error on an empty string', - () => { expect(Validators.minLength(2)(new FormControl(''))).toBeNull(); }); + it('should return a validation error on small values converted from strings', () => { + expect(Validators.min(2)(new FormControl('1'))).toEqual({'min': {'min': 2, 'actual': '1'}}); + }); - it('should not error on null', - () => { expect(Validators.minLength(2)(new FormControl(null))).toBeNull(); }); + it('should not error on big values', () => { + expect(Validators.min(2)(new FormControl(3))).toBeNull(); + }); - it('should not error on undefined', - () => { expect(Validators.minLength(2)(new FormControl(undefined))).toBeNull(); }); + it('should not error on equal values', () => { + expect(Validators.min(2)(new FormControl(2))).toBeNull(); + }); - it('should not error on valid strings', - () => { expect(Validators.minLength(2)(new FormControl('aa'))).toBeNull(); }); + it('should not error on equal values when value is string', () => { + expect(Validators.min(2)(new FormControl('2'))).toBeNull(); + }); - it('should error on short strings', () => { - expect(Validators.minLength(2)(new FormControl('a'))).toEqual({ - 'minlength': {'requiredLength': 2, 'actualLength': 1} - }); - }); - - it('should not error when FormArray has valid length', () => { - const fa = new FormArray([new FormControl(''), new FormControl('')]); - expect(Validators.minLength(2)(fa)).toBeNull(); - }); - - it('should error when FormArray has invalid length', () => { - const fa = new FormArray([new FormControl('')]); - expect(Validators.minLength(2)(fa)).toEqual({ - 'minlength': {'requiredLength': 2, 'actualLength': 1} - }); + it('should validate as expected when min value is a string', () => { + expect(Validators.min('2' as any)(new FormControl(1))).toEqual({ + 'min': {'min': '2', 'actual': 1} }); }); - describe('maxLength', () => { - it('should not error on an empty string', - () => { expect(Validators.maxLength(2)(new FormControl(''))).toBeNull(); }); + it('should return null if min value is undefined', () => { + expect(Validators.min(undefined as any)(new FormControl(3))).toBeNull(); + }); - it('should not error on null', - () => { expect(Validators.maxLength(2)(new FormControl(null))).toBeNull(); }); + it('should return null if min value is null', () => { + expect(Validators.min(null as any)(new FormControl(3))).toBeNull(); + }); + }); - it('should not error on undefined', - () => { expect(Validators.maxLength(2)(new FormControl(undefined))).toBeNull(); }); + describe('max', () => { + it('should not error on an empty string', () => { + expect(Validators.max(2)(new FormControl(''))).toBeNull(); + }); - it('should not error on valid strings', - () => { expect(Validators.maxLength(2)(new FormControl('aa'))).toBeNull(); }); + it('should not error on null', () => { + expect(Validators.max(2)(new FormControl(null))).toBeNull(); + }); - it('should error on long strings', () => { - expect(Validators.maxLength(2)(new FormControl('aaa'))).toEqual({ - 'maxlength': {'requiredLength': 2, 'actualLength': 3} - }); - }); + it('should not error on undefined', () => { + expect(Validators.max(2)(new FormControl(undefined))).toBeNull(); + }); - it('should not error when FormArray has valid length', () => { - const fa = new FormArray([new FormControl(''), new FormControl('')]); - expect(Validators.maxLength(2)(fa)).toBeNull(); - }); + it('should return null if NaN after parsing', () => { + expect(Validators.max(2)(new FormControl('aaa'))).toBeNull(); + }); - it('should error when FormArray has invalid length', () => { - const fa = new FormArray([new FormControl(''), new FormControl('')]); - expect(Validators.maxLength(1)(fa)).toEqual({ - 'maxlength': {'requiredLength': 1, 'actualLength': 2} - }); + it('should return a validation error on big values', () => { + expect(Validators.max(2)(new FormControl(3))).toEqual({'max': {'max': 2, 'actual': 3}}); + }); + + it('should return a validation error on big values converted from strings', () => { + expect(Validators.max(2)(new FormControl('3'))).toEqual({'max': {'max': 2, 'actual': '3'}}); + }); + + it('should not error on small values', () => { + expect(Validators.max(2)(new FormControl(1))).toBeNull(); + }); + + it('should not error on equal values', () => { + expect(Validators.max(2)(new FormControl(2))).toBeNull(); + }); + + it('should not error on equal values when value is string', () => { + expect(Validators.max(2)(new FormControl('2'))).toBeNull(); + }); + + it('should validate as expected when max value is a string', () => { + expect(Validators.max('2' as any)(new FormControl(3))).toEqual({ + 'max': {'max': '2', 'actual': 3} }); }); - describe('pattern', () => { - it('should not error on an empty string', - () => { expect(Validators.pattern('[a-zA-Z ]+')(new FormControl(''))).toBeNull(); }); - - it('should not error on 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(undefined))).toBeNull(); - }); - - it('should not error on null value and "null" pattern', - () => { expect(Validators.pattern('null')(new FormControl(null))).toBeNull(); }); - - it('should not error on valid strings', - () => expect(Validators.pattern('[a-zA-Z ]*')(new FormControl('aaAA'))).toBeNull()); - - it('should error on failure to match string', () => { - expect(Validators.pattern('[a-zA-Z ]*')(new FormControl('aaa0'))).toEqual({ - 'pattern': {'requiredPattern': '^[a-zA-Z ]*$', 'actualValue': 'aaa0'} - }); - }); - - it('should accept RegExp object', () => { - const pattern: RegExp = new RegExp('[a-zA-Z ]+'); - expect(Validators.pattern(pattern)(new FormControl('aaAA'))).toBeNull(); - }); - - it('should error on failure to match RegExp object', () => { - const pattern: RegExp = new RegExp('^[a-zA-Z ]*$'); - expect(Validators.pattern(pattern)(new FormControl('aaa0'))).toEqual({ - 'pattern': {'requiredPattern': '/^[a-zA-Z ]*$/', 'actualValue': 'aaa0'} - }); - }); - - it('should not error on "null" pattern', - () => expect(Validators.pattern(null !)(new FormControl('aaAA'))).toBeNull()); - - it('should not error on "undefined" pattern', - () => expect(Validators.pattern(undefined !)(new FormControl('aaAA'))).toBeNull()); - - it('should work with pattern string containing both boundary symbols', - () => expect(Validators.pattern('^[aA]*$')(new FormControl('aaAA'))).toBeNull()); - - it('should work with pattern string containing only start boundary symbols', - () => expect(Validators.pattern('^[aA]*')(new FormControl('aaAA'))).toBeNull()); - - it('should work with pattern string containing only end boundary symbols', - () => expect(Validators.pattern('[aA]*$')(new FormControl('aaAA'))).toBeNull()); - - it('should work with pattern string not containing any boundary symbols', - () => expect(Validators.pattern('[aA]*')(new FormControl('aaAA'))).toBeNull()); + it('should return null if max value is undefined', () => { + expect(Validators.max(undefined as any)(new FormControl(3))).toBeNull(); }); - describe('compose', () => { - it('should return null when given null', - () => { expect(Validators.compose(null !)).toBe(null); }); + it('should return null if max value is null', () => { + expect(Validators.max(null as any)(new FormControl(3))).toBeNull(); + }); + }); + + + describe('required', () => { + it('should error on an empty string', () => { + expect(Validators.required(new FormControl(''))).toEqual({'required': true}); + }); + + it('should error on null', () => { + expect(Validators.required(new FormControl(null))).toEqual({'required': true}); + }); + + it('should not error on undefined', () => { + expect(Validators.required(new FormControl(undefined))).toEqual({'required': true}); + }); + + it('should not error on a non-empty string', () => { + expect(Validators.required(new FormControl('not empty'))).toBeNull(); + }); + + it('should accept zero as valid', () => { + expect(Validators.required(new FormControl(0))).toBeNull(); + }); + + it('should error on an empty array', + () => expect(Validators.required(new FormControl([]))).toEqual({'required': true})); + + it('should not error on a non-empty array', + () => expect(Validators.required(new FormControl([1, 2]))).toBeNull()); + }); + + describe('requiredTrue', () => { + it('should error on false', + () => expect(Validators.requiredTrue(new FormControl(false))).toEqual({'required': true})); + + it('should not error on true', + () => expect(Validators.requiredTrue(new FormControl(true))).toBeNull()); + }); + + describe('email', () => { + it('should not error on an empty string', + () => expect(Validators.email(new FormControl(''))).toBeNull()); + + it('should not error on null', + () => expect(Validators.email(new FormControl(null))).toBeNull()); + + it('should error on invalid email', + () => expect(Validators.email(new FormControl('some text'))).toEqual({'email': true})); + + it('should not error on valid email', + () => expect(Validators.email(new FormControl('test@gmail.com'))).toBeNull()); + }); + + describe('minLength', () => { + it('should not error on an empty string', () => { + expect(Validators.minLength(2)(new FormControl(''))).toBeNull(); + }); + + it('should not error on null', () => { + expect(Validators.minLength(2)(new FormControl(null))).toBeNull(); + }); + + it('should not error on undefined', () => { + expect(Validators.minLength(2)(new FormControl(undefined))).toBeNull(); + }); + + it('should not error on valid strings', () => { + expect(Validators.minLength(2)(new FormControl('aa'))).toBeNull(); + }); + + it('should error on short strings', () => { + expect(Validators.minLength(2)(new FormControl('a'))).toEqual({ + 'minlength': {'requiredLength': 2, 'actualLength': 1} + }); + }); + + it('should not error when FormArray has valid length', () => { + const fa = new FormArray([new FormControl(''), new FormControl('')]); + expect(Validators.minLength(2)(fa)).toBeNull(); + }); + + it('should error when FormArray has invalid length', () => { + const fa = new FormArray([new FormControl('')]); + expect(Validators.minLength(2)(fa)).toEqual({ + 'minlength': {'requiredLength': 2, 'actualLength': 1} + }); + }); + }); + + describe('maxLength', () => { + it('should not error on an empty string', () => { + expect(Validators.maxLength(2)(new FormControl(''))).toBeNull(); + }); + + it('should not error on null', () => { + expect(Validators.maxLength(2)(new FormControl(null))).toBeNull(); + }); + + it('should not error on undefined', () => { + expect(Validators.maxLength(2)(new FormControl(undefined))).toBeNull(); + }); + + it('should not error on valid strings', () => { + expect(Validators.maxLength(2)(new FormControl('aa'))).toBeNull(); + }); + + it('should error on long strings', () => { + expect(Validators.maxLength(2)(new FormControl('aaa'))).toEqual({ + 'maxlength': {'requiredLength': 2, 'actualLength': 3} + }); + }); + + it('should not error when FormArray has valid length', () => { + const fa = new FormArray([new FormControl(''), new FormControl('')]); + expect(Validators.maxLength(2)(fa)).toBeNull(); + }); + + it('should error when FormArray has invalid length', () => { + const fa = new FormArray([new FormControl(''), new FormControl('')]); + expect(Validators.maxLength(1)(fa)).toEqual({ + 'maxlength': {'requiredLength': 1, 'actualLength': 2} + }); + }); + }); + + describe('pattern', () => { + it('should not error on an empty string', () => { + expect(Validators.pattern('[a-zA-Z ]+')(new FormControl(''))).toBeNull(); + }); + + it('should not error on 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(undefined))).toBeNull(); + }); + + it('should not error on null value and "null" pattern', () => { + expect(Validators.pattern('null')(new FormControl(null))).toBeNull(); + }); + + it('should not error on valid strings', + () => expect(Validators.pattern('[a-zA-Z ]*')(new FormControl('aaAA'))).toBeNull()); + + it('should error on failure to match string', () => { + expect(Validators.pattern('[a-zA-Z ]*')(new FormControl('aaa0'))).toEqual({ + 'pattern': {'requiredPattern': '^[a-zA-Z ]*$', 'actualValue': 'aaa0'} + }); + }); + + it('should accept RegExp object', () => { + const pattern: RegExp = new RegExp('[a-zA-Z ]+'); + expect(Validators.pattern(pattern)(new FormControl('aaAA'))).toBeNull(); + }); + + it('should error on failure to match RegExp object', () => { + const pattern: RegExp = new RegExp('^[a-zA-Z ]*$'); + expect(Validators.pattern(pattern)(new FormControl('aaa0'))).toEqual({ + 'pattern': {'requiredPattern': '/^[a-zA-Z ]*$/', 'actualValue': 'aaa0'} + }); + }); + + it('should not error on "null" pattern', + () => expect(Validators.pattern(null!)(new FormControl('aaAA'))).toBeNull()); + + it('should not error on "undefined" pattern', + () => expect(Validators.pattern(undefined!)(new FormControl('aaAA'))).toBeNull()); + + it('should work with pattern string containing both boundary symbols', + () => expect(Validators.pattern('^[aA]*$')(new FormControl('aaAA'))).toBeNull()); + + it('should work with pattern string containing only start boundary symbols', + () => expect(Validators.pattern('^[aA]*')(new FormControl('aaAA'))).toBeNull()); + + it('should work with pattern string containing only end boundary symbols', + () => expect(Validators.pattern('[aA]*$')(new FormControl('aaAA'))).toBeNull()); + + it('should work with pattern string not containing any boundary symbols', + () => expect(Validators.pattern('[aA]*')(new FormControl('aaAA'))).toBeNull()); + }); + + describe('compose', () => { + it('should return null when given null', () => { + expect(Validators.compose(null!)).toBe(null); + }); + + it('should collect errors from all the validators', () => { + const c = Validators.compose([validator('a', true), validator('b', true)])!; + expect(c(new FormControl(''))).toEqual({'a': true, 'b': true}); + }); + + it('should run validators left to right', () => { + const c = Validators.compose([validator('a', 1), validator('a', 2)])!; + expect(c(new FormControl(''))).toEqual({'a': 2}); + }); + + it('should return null when no errors', () => { + const c = Validators.compose([Validators.nullValidator, Validators.nullValidator])!; + expect(c(new FormControl(''))).toBeNull(); + }); + + it('should ignore nulls', () => { + const c = Validators.compose([null!, Validators.required])!; + expect(c(new FormControl(''))).toEqual({'required': true}); + }); + }); + + describe('composeAsync', () => { + describe('promises', () => { + function promiseValidator(response: {[key: string]: any}): AsyncValidatorFn { + return (c: AbstractControl) => { + const res = c.value != 'expected' ? response : null; + return Promise.resolve(res); + }; + } + + it('should return null when given null', () => { + expect(Validators.composeAsync(null!)).toBeNull(); + }); + + it('should collect errors from all the validators', fakeAsync(() => { + const v = Validators.composeAsync( + [promiseValidator({'one': true}), promiseValidator({'two': true})])!; + + let errorMap: {[key: string]: any}|null = undefined!; + (v(new FormControl('invalid')) as Observable) + .pipe(first()) + .subscribe((errors: {[key: string]: any}|null) => errorMap = errors); + tick(); + + expect(errorMap!).toEqual({'one': true, 'two': true}); + })); + + it('should normalize and evaluate async validator-directives correctly', fakeAsync(() => { + const v = Validators.composeAsync( + [normalizeAsyncValidator(new AsyncValidatorDirective('expected', {'one': true}))])!; + + let errorMap: {[key: string]: any}|null = undefined!; + (v(new FormControl('invalid')) as Observable) + .pipe(first()) + .subscribe((errors: {[key: string]: any}|null) => errorMap = errors); + tick(); + + expect(errorMap!).toEqual({'one': true}); + })); + + it('should return null when no errors', fakeAsync(() => { + const v = Validators.composeAsync([promiseValidator({'one': true})])!; + + let errorMap: {[key: string]: any}|null = undefined!; + (v(new FormControl('expected')) as Observable) + .pipe(first()) + .subscribe((errors: {[key: string]: any}|null) => errorMap = errors); + tick(); + + expect(errorMap).toBeNull(); + })); + + it('should ignore nulls', fakeAsync(() => { + const v = Validators.composeAsync([promiseValidator({'one': true}), null!])!; + + let errorMap: {[key: string]: any}|null = undefined!; + (v(new FormControl('invalid')) as Observable) + .pipe(first()) + .subscribe((errors: {[key: string]: any}|null) => errorMap = errors); + tick(); + + expect(errorMap!).toEqual({'one': true}); + })); + }); + + describe('observables', () => { + function observableValidator(response: {[key: string]: any}): AsyncValidatorFn { + return (c: AbstractControl) => { + const res = c.value != 'expected' ? response : null; + return of(res); + }; + } + + it('should return null when given null', () => { + expect(Validators.composeAsync(null!)).toBeNull(); + }); it('should collect errors from all the validators', () => { - const c = Validators.compose([validator('a', true), validator('b', true)]) !; - expect(c(new FormControl(''))).toEqual({'a': true, 'b': true}); + const v = Validators.composeAsync( + [observableValidator({'one': true}), observableValidator({'two': true})])!; + + let errorMap: {[key: string]: any}|null = undefined!; + (v(new FormControl('invalid')) as Observable) + .pipe(first()) + .subscribe((errors: {[key: string]: any}|null) => errorMap = errors); + + expect(errorMap!).toEqual({'one': true, 'two': true}); }); - it('should run validators left to right', () => { - const c = Validators.compose([validator('a', 1), validator('a', 2)]) !; - expect(c(new FormControl(''))).toEqual({'a': 2}); + it('should normalize and evaluate async validator-directives correctly', () => { + const v = Validators.composeAsync( + [normalizeAsyncValidator(new AsyncValidatorDirective('expected', {'one': true}))])!; + + let errorMap: {[key: string]: any}|null = undefined!; + (v(new FormControl('invalid')) as Observable) + .pipe(first()) + .subscribe((errors: {[key: string]: any}|null) => errorMap = errors)!; + + expect(errorMap!).toEqual({'one': true}); }); it('should return null when no errors', () => { - const c = Validators.compose([Validators.nullValidator, Validators.nullValidator]) !; - expect(c(new FormControl(''))).toBeNull(); + const v = Validators.composeAsync([observableValidator({'one': true})])!; + + let errorMap: {[key: string]: any}|null = undefined!; + (v(new FormControl('expected')) as Observable) + .pipe(first()) + .subscribe((errors: {[key: string]: any}|null) => errorMap = errors); + + expect(errorMap).toBeNull(); }); it('should ignore nulls', () => { - const c = Validators.compose([null !, Validators.required]) !; - expect(c(new FormControl(''))).toEqual({'required': true}); - }); - }); + const v = Validators.composeAsync([observableValidator({'one': true}), null!])!; - describe('composeAsync', () => { + let errorMap: {[key: string]: any}|null = undefined!; + (v(new FormControl('invalid')) as Observable) + .pipe(first()) + .subscribe((errors: {[key: string]: any}|null) => errorMap = errors); - describe('promises', () => { - function promiseValidator(response: {[key: string]: any}): AsyncValidatorFn { - return (c: AbstractControl) => { - const res = c.value != 'expected' ? response : null; - return Promise.resolve(res); - }; - } - - it('should return null when given null', - () => { expect(Validators.composeAsync(null !)).toBeNull(); }); - - it('should collect errors from all the validators', fakeAsync(() => { - const v = Validators.composeAsync( - [promiseValidator({'one': true}), promiseValidator({'two': true})]) !; - - let errorMap: {[key: string]: any}|null = undefined !; - (v(new FormControl('invalid')) as Observable) - .pipe(first()) - .subscribe((errors: {[key: string]: any} | null) => errorMap = errors); - tick(); - - expect(errorMap !).toEqual({'one': true, 'two': true}); - })); - - it('should normalize and evaluate async validator-directives correctly', fakeAsync(() => { - const v = Validators.composeAsync([normalizeAsyncValidator( - new AsyncValidatorDirective('expected', {'one': true}))]) !; - - let errorMap: {[key: string]: any}|null = undefined !; - (v(new FormControl('invalid')) as Observable) - .pipe(first()) - .subscribe((errors: {[key: string]: any} | null) => errorMap = errors); - tick(); - - expect(errorMap !).toEqual({'one': true}); - })); - - it('should return null when no errors', fakeAsync(() => { - const v = Validators.composeAsync([promiseValidator({'one': true})]) !; - - let errorMap: {[key: string]: any}|null = undefined !; - (v(new FormControl('expected')) as Observable) - .pipe(first()) - .subscribe((errors: {[key: string]: any} | null) => errorMap = errors); - tick(); - - expect(errorMap).toBeNull(); - })); - - it('should ignore nulls', fakeAsync(() => { - const v = Validators.composeAsync([promiseValidator({'one': true}), null !]) !; - - let errorMap: {[key: string]: any}|null = undefined !; - (v(new FormControl('invalid')) as Observable) - .pipe(first()) - .subscribe((errors: {[key: string]: any} | null) => errorMap = errors); - tick(); - - expect(errorMap !).toEqual({'one': true}); - })); + expect(errorMap!).toEqual({'one': true}); }); - describe('observables', () => { - function observableValidator(response: {[key: string]: any}): AsyncValidatorFn { - return (c: AbstractControl) => { - const res = c.value != 'expected' ? response : null; - return of (res); - }; - } + it('should wait for all validators before setting errors', fakeAsync(() => { + function getTimerObs(time: number, errorMap: {[key: string]: any}): AsyncValidatorFn { + return (c: AbstractControl) => { + return timer(time).pipe(map(() => errorMap)); + }; + } - it('should return null when given null', - () => { expect(Validators.composeAsync(null !)).toBeNull(); }); + const v = Validators.composeAsync( + [getTimerObs(100, {one: true}), getTimerObs(200, {two: true})])!; - it('should collect errors from all the validators', () => { - const v = Validators.composeAsync( - [observableValidator({'one': true}), observableValidator({'two': true})]) !; + let errorMap: {[key: string]: any}|null = undefined!; + (v(new FormControl('invalid')) as Observable) + .pipe(first()) + .subscribe((errors: {[key: string]: any}|null) => errorMap = errors); - let errorMap: {[key: string]: any}|null = undefined !; - (v(new FormControl('invalid')) as Observable) - .pipe(first()) - .subscribe((errors: {[key: string]: any} | null) => errorMap = errors); - - expect(errorMap !).toEqual({'one': true, 'two': true}); - }); - - it('should normalize and evaluate async validator-directives correctly', () => { - const v = Validators.composeAsync( - [normalizeAsyncValidator(new AsyncValidatorDirective('expected', {'one': true}))]) !; - - let errorMap: {[key: string]: any}|null = undefined !; - (v(new FormControl('invalid')) as Observable) - .pipe(first()) - .subscribe((errors: {[key: string]: any} | null) => errorMap = errors) !; - - expect(errorMap !).toEqual({'one': true}); - }); - - it('should return null when no errors', () => { - const v = Validators.composeAsync([observableValidator({'one': true})]) !; - - let errorMap: {[key: string]: any}|null = undefined !; - (v(new FormControl('expected')) as Observable) - .pipe(first()) - .subscribe((errors: {[key: string]: any} | null) => errorMap = errors); - - expect(errorMap).toBeNull(); - }); - - it('should ignore nulls', () => { - const v = Validators.composeAsync([observableValidator({'one': true}), null !]) !; - - let errorMap: {[key: string]: any}|null = undefined !; - (v(new FormControl('invalid')) as Observable) - .pipe(first()) - .subscribe((errors: {[key: string]: any} | null) => errorMap = errors); - - expect(errorMap !).toEqual({'one': true}); - }); - - it('should wait for all validators before setting errors', fakeAsync(() => { - function getTimerObs(time: number, errorMap: {[key: string]: any}): AsyncValidatorFn { - return (c: AbstractControl) => { return timer(time).pipe(map(() => errorMap)); }; - } - - const v = Validators.composeAsync( - [getTimerObs(100, {one: true}), getTimerObs(200, {two: true})]) !; - - let errorMap: {[key: string]: any}|null = undefined !; - (v(new FormControl('invalid')) as Observable) - .pipe(first()) - .subscribe((errors: {[key: string]: any} | null) => errorMap = errors); - - tick(100); - expect(errorMap).not.toBeDefined( - `Expected errors not to be set until all validators came back.`); - - tick(100); - expect(errorMap !) - .toEqual( - {one: true, two: true}, - `Expected errors to merge once all validators resolved.`); - })); - }); + tick(100); + expect(errorMap).not.toBeDefined( + `Expected errors not to be set until all validators came back.`); + tick(100); + expect(errorMap!).toEqual( + {one: true, two: true}, `Expected errors to merge once all validators resolved.`); + })); }); }); +}); })(); diff --git a/packages/forms/test/value_accessor_integration_spec.ts b/packages/forms/test/value_accessor_integration_spec.ts index 7eac63877d..852e04bde4 100644 --- a/packages/forms/test/value_accessor_integration_spec.ts +++ b/packages/forms/test/value_accessor_integration_spec.ts @@ -7,14 +7,13 @@ */ import {Component, Directive, EventEmitter, Input, Output, Type, ViewChild} from '@angular/core'; -import {ComponentFixture, TestBed, async, fakeAsync, tick} from '@angular/core/testing'; +import {async, ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing'; import {AbstractControl, ControlValueAccessor, FormControl, FormGroup, FormsModule, NG_VALIDATORS, NG_VALUE_ACCESSOR, NgControl, NgForm, NgModel, ReactiveFormsModule, Validators} from '@angular/forms'; import {By} from '@angular/platform-browser/src/dom/debug/by'; import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util'; { describe('value accessors', () => { - function initTest(component: Type, ...directives: Type[]): ComponentFixture { TestBed.configureTestingModule( {declarations: [component, ...directives], imports: [FormsModule, ReactiveFormsModule]}); @@ -64,7 +63,11 @@ import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util' fixture.detectChanges(); const input = fixture.debugElement.query(By.css('input')); - form.valueChanges.subscribe({next: (value) => { throw 'Should not happen'; }}); + form.valueChanges.subscribe({ + next: (value) => { + throw 'Should not happen'; + } + }); input.nativeElement.value = 'updatedValue'; dispatchEvent(input.nativeElement, 'change'); @@ -160,9 +163,7 @@ import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util' }); describe('select controls', () => { - describe('in reactive forms', () => { - it(`should support primitive values`, () => { if (isNode) return; const fixture = initTest(FormControlNameSelect); @@ -197,7 +198,7 @@ import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util' it('should throw an error if compareWith is not a function', () => { const fixture = initTest(FormControlSelectWithCompareFn); - fixture.componentInstance.compareFn = null !; + fixture.componentInstance.compareFn = null!; expect(() => fixture.detectChanges()) .toThrowError(/compareWith must be a function, but received null/); }); @@ -238,7 +239,6 @@ import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util' expect(select.nativeElement.value).toEqual('3: Object'); expect(nyOption.nativeElement.selected).toBe(true); }); - }); describe('in template-driven forms', () => { @@ -336,7 +336,7 @@ import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util' dispatchEvent(select.nativeElement, 'change'); fixture.detectChanges(); tick(); - expect(comp.selectedCity !['name']).toEqual('NYC'); + expect(comp.selectedCity!['name']).toEqual('NYC'); select.nativeElement.value = '0: null'; dispatchEvent(select.nativeElement, 'change'); @@ -348,7 +348,7 @@ import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util' it('should throw an error when compareWith is not a function', () => { const fixture = initTest(NgModelSelectWithCustomCompareFnForm); const comp = fixture.componentInstance; - comp.compareFn = null !; + comp.compareFn = null!; expect(() => fixture.detectChanges()) .toThrowError(/compareWith must be a function, but received null/); }); @@ -398,16 +398,11 @@ import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util' expect(select.nativeElement.value).toEqual('3: Object'); expect(nyOption.nativeElement.selected).toBe(true); })); - - }); - }); describe('select multiple controls', () => { - describe('in reactive forms', () => { - it('should support primitive values', () => { if (isNode) return; const fixture = initTest(FormControlSelectMultiple); @@ -432,7 +427,7 @@ import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util' it('should throw an error when compareWith is not a function', () => { const fixture = initTest(FormControlSelectMultipleWithCompareFn); - fixture.componentInstance.compareFn = null !; + fixture.componentInstance.compareFn = null!; expect(() => fixture.detectChanges()) .toThrowError(/compareWith must be a function, but received null/); }); @@ -448,7 +443,6 @@ import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util' expect(select.nativeElement.value).toEqual('0: Object'); expect(sfOption.nativeElement.selected).toBe(true); })); - }); describe('in template-driven forms', () => { @@ -520,7 +514,7 @@ import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util' it('should throw an error when compareWith is not a function', () => { const fixture = initTest(NgModelSelectMultipleWithCustomCompareFnForm); const comp = fixture.componentInstance; - comp.compareFn = null !; + comp.compareFn = null!; expect(() => fixture.detectChanges()) .toThrowError(/compareWith must be a function, but received null/); }); @@ -539,13 +533,10 @@ import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util' expect(select.nativeElement.value).toEqual('0: Object'); expect(sfOption.nativeElement.selected).toBe(true); })); - }); describe('should support ', () => { - describe('in reactive forms', () => { - it('should support basic functionality', () => { const fixture = initTest(FormControlRadioButtons); const form = @@ -562,10 +553,10 @@ import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util' fixture.detectChanges(); // view -> model - expect(form.get('food') !.value).toEqual('chicken'); + expect(form.get('food')!.value).toEqual('chicken'); expect(inputs[1].nativeElement.checked).toEqual(false); - form.get('food') !.setValue('fish'); + form.get('food')!.setValue('fish'); fixture.detectChanges(); // programmatic change -> view @@ -606,16 +597,16 @@ import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util' fixture.componentInstance.form = form; fixture.detectChanges(); - form.get('food') !.setValue(null); + form.get('food')!.setValue(null); fixture.detectChanges(); const inputs = fixture.debugElement.queryAll(By.css('input')); expect(inputs[0].nativeElement.checked).toEqual(false); - form.get('food') !.setValue('chicken'); + form.get('food')!.setValue('chicken'); fixture.detectChanges(); - form.get('food') !.setValue(undefined); + form.get('food')!.setValue(undefined); fixture.detectChanges(); expect(inputs[0].nativeElement.checked).toEqual(false); }); @@ -706,13 +697,12 @@ import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util' fixture.detectChanges(); // view -> model - expect(form.get('food') !.value).toEqual('chicken'); - expect(form.get('nested.food') !.value).toEqual('fish'); + expect(form.get('food')!.value).toEqual('chicken'); + expect(form.get('nested.food')!.value).toEqual('fish'); expect(inputs[1].nativeElement.checked).toEqual(false); expect(inputs[2].nativeElement.checked).toEqual(false); expect(inputs[3].nativeElement.checked).toEqual(true); - }); it('should disable all radio buttons when disable() is called', () => { @@ -728,7 +718,7 @@ import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util' expect(inputs[2].nativeElement.disabled).toEqual(false); expect(inputs[3].nativeElement.disabled).toEqual(false); - form.get('food') !.disable(); + form.get('food')!.disable(); expect(inputs[0].nativeElement.disabled).toEqual(true); expect(inputs[1].nativeElement.disabled).toEqual(true); expect(inputs[2].nativeElement.disabled).toEqual(false); @@ -780,7 +770,6 @@ import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util' expect(inputs[0].nativeElement.checked).toBe(false); expect(inputs[1].nativeElement.checked).toBe(true); }); - }); describe('in template-driven forms', () => { @@ -860,7 +849,7 @@ import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util' fixture.detectChanges(); tick(); - fixture.componentInstance.food = null !; + fixture.componentInstance.food = null!; fixture.detectChanges(); tick(); @@ -872,7 +861,7 @@ import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util' fixture.detectChanges(); tick(); - fixture.componentInstance.food = undefined !; + fixture.componentInstance.food = undefined!; fixture.detectChanges(); tick(); expect(inputs[0].nativeElement.checked).toEqual(false); @@ -886,7 +875,7 @@ import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util' tick(); const form = fixture.debugElement.children[0].injector.get(NgForm); - form.control.get('food') !.disable(); + form.control.get('food')!.disable(); tick(); const inputs = fixture.debugElement.queryAll(By.css('input')); @@ -911,15 +900,11 @@ import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util' expect(inputs[2].nativeElement.disabled).toBe(false); expect(inputs[3].nativeElement.disabled).toBe(false); })); - }); - }); describe('should support ', () => { - describe('in reactive forms', () => { - it('with basic use case', () => { const fixture = initTest(FormControlRangeInput); const control = new FormControl(10); @@ -968,7 +953,6 @@ import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util' const input = fixture.debugElement.query(By.css('input')); expect(input.nativeElement.value).toEqual(''); }); - }); describe('in template-driven forms', () => { @@ -987,15 +971,12 @@ import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util' tick(); // view -> model fixture.detectChanges(); - expect(typeof(fixture.componentInstance.val)).toBe('number'); + expect(typeof (fixture.componentInstance.val)).toBe('number'); })); - }); - }); describe('custom value accessors', () => { - describe('in reactive forms', () => { it('should support basic functionality', () => { const fixture = initTest(WrappedValueForm, WrappedValue); @@ -1014,9 +995,9 @@ import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util' expect(form.value).toEqual({'login': 'bb'}); // custom validator - expect(form.get('login') !.errors).toEqual({'err': true}); + expect(form.get('login')!.errors).toEqual({'err': true}); form.setValue({login: 'expected'}); - expect(form.get('login') !.errors).toEqual(null); + expect(form.get('login')!.errors).toEqual(null); }); it('should support non builtin input elements that fire a change event without a \'target\' property', @@ -1042,7 +1023,7 @@ import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util' }); fixture.detectChanges(); expect(fixture.componentInstance.form.status).toEqual('DISABLED'); - expect(fixture.componentInstance.form.get('login') !.status).toEqual('DISABLED'); + expect(fixture.componentInstance.form.get('login')!.status).toEqual('DISABLED'); }); it('should support custom accessors without setDisabledState - formControlDirective', @@ -1061,9 +1042,9 @@ import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util' fixture.componentInstance.form = new FormGroup({'login': new FormControl('aa')}); fixture.detectChanges(); - expect(fixture.componentInstance.myInput !.control).toBeDefined(); - expect(fixture.componentInstance.myInput !.control) - .toEqual(fixture.componentInstance.myInput !.controlDir.control); + expect(fixture.componentInstance.myInput!.control).toBeDefined(); + expect(fixture.componentInstance.myInput!.control) + .toEqual(fixture.componentInstance.myInput!.controlDir.control); }); }); @@ -1089,16 +1070,14 @@ import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util' }); })); }); - }); - }); } @Component({selector: 'form-control-comp', template: ``}) export class FormControlComp { // TODO(issue/24571): remove '!'. - control !: FormControl; + control!: FormControl; } @Component({ @@ -1110,13 +1089,13 @@ export class FormControlComp { }) export class FormGroupComp { // TODO(issue/24571): remove '!'. - control !: FormControl; + control!: FormControl; // TODO(issue/24571): remove '!'. - form !: FormGroup; + form!: FormGroup; // TODO(issue/24571): remove '!'. - myGroup !: FormGroup; + myGroup!: FormGroup; // TODO(issue/24571): remove '!'. - event !: Event; + event!: Event; } @Component({ @@ -1125,7 +1104,7 @@ export class FormGroupComp { }) class FormControlNumberInput { // TODO(issue/24571): remove '!'. - control !: FormControl; + control!: FormControl; } @Component({ @@ -1167,7 +1146,7 @@ class FormControlSelectNgValue { }) class FormControlSelectWithCompareFn { compareFn: - (o1: any, o2: any) => boolean = (o1: any, o2: any) => o1 && o2? o1.id === o2.id: o1 === o2 + (o1: any, o2: any) => boolean = (o1: any, o2: any) => o1 && o2 ? o1.id === o2.id : o1 === o2 cities = [{id: 1, name: 'SF'}, {id: 2, name: 'NY'}]; form = new FormGroup({city: new FormControl({id: 1, name: 'SF'})}); } @@ -1211,7 +1190,7 @@ class FormControlSelectMultipleNgValue { }) class FormControlSelectMultipleWithCompareFn { compareFn: - (o1: any, o2: any) => boolean = (o1: any, o2: any) => o1 && o2? o1.id === o2.id: o1 === o2 + (o1: any, o2: any) => boolean = (o1: any, o2: any) => o1 && o2 ? o1.id === o2.id : o1 === o2 cities = [{id: 1, name: 'SF'}, {id: 2, name: 'NY'}]; form = new FormGroup({city: new FormControl([{id: 1, name: 'SF'}])}); } @@ -1254,7 +1233,7 @@ class NgModelSelectWithNullForm { }) class NgModelSelectWithCustomCompareFnForm { compareFn: - (o1: any, o2: any) => boolean = (o1: any, o2: any) => o1 && o2? o1.id === o2.id: o1 === o2 + (o1: any, o2: any) => boolean = (o1: any, o2: any) => o1 && o2 ? o1.id === o2.id : o1 === o2 selectedCity: any = {}; cities: any[] = []; } @@ -1270,7 +1249,7 @@ class NgModelSelectWithCustomCompareFnForm { }) class NgModelSelectMultipleWithCustomCompareFnForm { compareFn: - (o1: any, o2: any) => boolean = (o1: any, o2: any) => o1 && o2? o1.id === o2.id: o1 === o2 + (o1: any, o2: any) => boolean = (o1: any, o2: any) => o1 && o2 ? o1.id === o2.id : o1 === o2 selectedCities: any[] = []; cities: any[] = []; } @@ -1285,7 +1264,7 @@ class NgModelSelectMultipleWithCustomCompareFnForm { }) class NgModelSelectMultipleForm { // TODO(issue/24571): remove '!'. - selectedCities !: any[]; + selectedCities!: any[]; cities: any[] = []; } @@ -1295,7 +1274,7 @@ class NgModelSelectMultipleForm { }) class FormControlRangeInput { // TODO(issue/24571): remove '!'. - control !: FormControl; + control!: FormControl; } @Component({selector: 'ng-model-range-form', template: ''}) @@ -1317,7 +1296,7 @@ class NgModelRangeForm { }) export class FormControlRadioButtons { // TODO(issue/24571): remove '!'. - form !: FormGroup; + form!: FormGroup; showRadio = new FormControl('yes'); } @@ -1335,9 +1314,9 @@ export class FormControlRadioButtons { }) class NgModelRadioForm { // TODO(issue/24571): remove '!'. - food !: string; + food!: string; // TODO(issue/24571): remove '!'. - drink !: string; + drink!: string; } @Directive({ @@ -1351,37 +1330,55 @@ class NgModelRadioForm { class WrappedValue implements ControlValueAccessor { value: any; // TODO(issue/24571): remove '!'. - onChange !: Function; + onChange!: Function; - writeValue(value: any) { this.value = `!${value}!`; } + writeValue(value: any) { + this.value = `!${value}!`; + } - registerOnChange(fn: (value: any) => void) { this.onChange = fn; } + registerOnChange(fn: (value: any) => void) { + this.onChange = fn; + } registerOnTouched(fn: any) {} - handleOnInput(value: any) { this.onChange(value.substring(1, value.length - 1)); } + handleOnInput(value: any) { + this.onChange(value.substring(1, value.length - 1)); + } - validate(c: AbstractControl) { return c.value === 'expected' ? null : {'err': true}; } + validate(c: AbstractControl) { + return c.value === 'expected' ? null : {'err': true}; + } } @Component({selector: 'my-input', template: ''}) export class MyInput implements ControlValueAccessor { @Output('input') onInput = new EventEmitter(); // TODO(issue/24571): remove '!'. - value !: string; + value!: string; control: AbstractControl|null = null; - constructor(public controlDir: NgControl) { controlDir.valueAccessor = this; } + constructor(public controlDir: NgControl) { + controlDir.valueAccessor = this; + } - ngOnInit() { this.control = this.controlDir.control; } + ngOnInit() { + this.control = this.controlDir.control; + } - writeValue(value: any) { this.value = `!${value}!`; } + writeValue(value: any) { + this.value = `!${value}!`; + } - registerOnChange(fn: (value: any) => void) { this.onInput.subscribe({next: fn}); } + registerOnChange(fn: (value: any) => void) { + this.onInput.subscribe({next: fn}); + } registerOnTouched(fn: any) {} - dispatchChangeEvent() { this.onInput.emit(this.value.substring(1, this.value.length - 1)); } + dispatchChangeEvent() { + this.onInput.emit(this.value.substring(1, this.value.length - 1)); + } } @Component({ @@ -1393,7 +1390,7 @@ export class MyInput implements ControlValueAccessor { }) export class MyInputForm { // TODO(issue/24571): remove '!'. - form !: FormGroup; + form!: FormGroup; @ViewChild(MyInput) myInput: MyInput|null = null; } @@ -1406,7 +1403,7 @@ export class MyInputForm { }) class WrappedValueForm { // TODO(issue/24571): remove '!'. - form !: FormGroup; + form!: FormGroup; } @Component({ @@ -1418,18 +1415,24 @@ class WrappedValueForm { }) export class NgModelCustomComp implements ControlValueAccessor { // TODO(issue/24571): remove '!'. - model !: string; + model!: string; @Input('disabled') isDisabled: boolean = false; // TODO(issue/24571): remove '!'. - changeFn !: (value: any) => void; + changeFn!: (value: any) => void; - writeValue(value: any) { this.model = value; } + writeValue(value: any) { + this.model = value; + } - registerOnChange(fn: (value: any) => void) { this.changeFn = fn; } + registerOnChange(fn: (value: any) => void) { + this.changeFn = fn; + } registerOnTouched() {} - setDisabledState(isDisabled: boolean) { this.isDisabled = isDisabled; } + setDisabledState(isDisabled: boolean) { + this.isDisabled = isDisabled; + } } @Component({ @@ -1442,6 +1445,6 @@ export class NgModelCustomComp implements ControlValueAccessor { }) export class NgModelCustomWrapper { // TODO(issue/24571): remove '!'. - name !: string; + name!: string; isDisabled = false; }