From 2b313e49791b932634cc58cf913a8cf867671d37 Mon Sep 17 00:00:00 2001 From: Kara Date: Wed, 24 Aug 2016 16:58:43 -0700 Subject: [PATCH] feat(forms): add support for disabled controls (#10994) --- .../directives/abstract_control_directive.ts | 4 + .../src/directives/checkbox_value_accessor.ts | 4 + .../src/directives/control_value_accessor.ts | 8 + .../src/directives/default_value_accessor.ts | 4 + .../@angular/forms/src/directives/ng_form.ts | 4 +- .../@angular/forms/src/directives/ng_model.ts | 20 +- .../src/directives/number_value_accessor.ts | 4 + .../radio_control_value_accessor.ts | 4 + .../form_control_directive.ts | 6 +- .../reactive_directives/form_control_name.ts | 89 ++-- .../forms/src/directives/reactive_errors.ts | 14 + .../select_control_value_accessor.ts | 4 + .../select_multiple_control_value_accessor.ts | 6 +- .../@angular/forms/src/directives/shared.ts | 20 +- modules/@angular/forms/src/form_builder.ts | 18 +- modules/@angular/forms/src/model.ts | 176 ++++++-- .../@angular/forms/test/directives_spec.ts | 12 +- .../@angular/forms/test/form_array_spec.ts | 237 +++++++++- .../@angular/forms/test/form_builder_spec.ts | 15 +- .../@angular/forms/test/form_control_spec.ts | 348 ++++++++++++-- .../@angular/forms/test/form_group_spec.ts | 423 +++++++++++------- .../forms/test/reactive_integration_spec.ts | 87 +++- .../forms/test/template_integration_spec.ts | 136 +++++- tools/public_api_guard/forms/index.d.ts | 35 +- 24 files changed, 1335 insertions(+), 343 deletions(-) diff --git a/modules/@angular/forms/src/directives/abstract_control_directive.ts b/modules/@angular/forms/src/directives/abstract_control_directive.ts index c0ec26a84f..a29864a9f8 100644 --- a/modules/@angular/forms/src/directives/abstract_control_directive.ts +++ b/modules/@angular/forms/src/directives/abstract_control_directive.ts @@ -41,6 +41,10 @@ export abstract class AbstractControlDirective { get untouched(): boolean { return isPresent(this.control) ? this.control.untouched : null; } + get disabled(): boolean { return isPresent(this.control) ? this.control.disabled : null; } + + get enabled(): boolean { return isPresent(this.control) ? this.control.enabled : null; } + get statusChanges(): Observable { return isPresent(this.control) ? this.control.statusChanges : null; } diff --git a/modules/@angular/forms/src/directives/checkbox_value_accessor.ts b/modules/@angular/forms/src/directives/checkbox_value_accessor.ts index e19e551596..8ce20b4f4d 100644 --- a/modules/@angular/forms/src/directives/checkbox_value_accessor.ts +++ b/modules/@angular/forms/src/directives/checkbox_value_accessor.ts @@ -43,4 +43,8 @@ export class CheckboxControlValueAccessor implements ControlValueAccessor { } registerOnChange(fn: (_: any) => {}): void { this.onChange = fn; } registerOnTouched(fn: () => {}): void { this.onTouched = fn; } + + setDisabledState(isDisabled: boolean): void { + this._renderer.setElementProperty(this._elementRef.nativeElement, 'disabled', isDisabled); + } } diff --git a/modules/@angular/forms/src/directives/control_value_accessor.ts b/modules/@angular/forms/src/directives/control_value_accessor.ts index 210dbee44c..d0c9c13942 100644 --- a/modules/@angular/forms/src/directives/control_value_accessor.ts +++ b/modules/@angular/forms/src/directives/control_value_accessor.ts @@ -33,6 +33,14 @@ export interface ControlValueAccessor { * Set the function to be called when the control receives a touch event. */ registerOnTouched(fn: any): void; + + /** + * This function is called when the control status changes to or from "DISABLED". + * Depending on the value, it will enable or disable the appropriate DOM element. + * + * @param isDisabled + */ + setDisabledState?(isDisabled: boolean): void; } /** diff --git a/modules/@angular/forms/src/directives/default_value_accessor.ts b/modules/@angular/forms/src/directives/default_value_accessor.ts index 2124f697ec..6307456677 100644 --- a/modules/@angular/forms/src/directives/default_value_accessor.ts +++ b/modules/@angular/forms/src/directives/default_value_accessor.ts @@ -51,4 +51,8 @@ export class DefaultValueAccessor implements ControlValueAccessor { registerOnChange(fn: (_: any) => void): void { this.onChange = fn; } registerOnTouched(fn: () => void): void { this.onTouched = fn; } + + setDisabledState(isDisabled: boolean): void { + this._renderer.setElementProperty(this._elementRef.nativeElement, 'disabled', isDisabled); + } } diff --git a/modules/@angular/forms/src/directives/ng_form.ts b/modules/@angular/forms/src/directives/ng_form.ts index a7f26bfe39..7b7b5988f4 100644 --- a/modules/@angular/forms/src/directives/ng_form.ts +++ b/modules/@angular/forms/src/directives/ng_form.ts @@ -104,8 +104,8 @@ export class NgForm extends ControlContainer implements Form { @Optional() @Self() @Inject(NG_VALIDATORS) validators: any[], @Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: any[]) { super(); - this.form = new FormGroup( - {}, null, composeValidators(validators), composeAsyncValidators(asyncValidators)); + this.form = + new FormGroup({}, composeValidators(validators), composeAsyncValidators(asyncValidators)); } get submitted(): boolean { return this._submitted; } diff --git a/modules/@angular/forms/src/directives/ng_model.ts b/modules/@angular/forms/src/directives/ng_model.ts index d10b4734d2..81e4af0c7f 100644 --- a/modules/@angular/forms/src/directives/ng_model.ts +++ b/modules/@angular/forms/src/directives/ng_model.ts @@ -64,9 +64,11 @@ export class NgModel extends NgControl implements OnChanges, _registered = false; viewModel: any; - @Input('ngModel') model: any; @Input() name: string; + @Input() disabled: boolean; + @Input('ngModel') model: any; @Input('ngModelOptions') options: {name?: string, standalone?: boolean}; + @Output('ngModelChange') update = new EventEmitter(); constructor(@Optional() @Host() private _parent: ControlContainer, @@ -81,6 +83,9 @@ export class NgModel extends NgControl implements OnChanges, ngOnChanges(changes: SimpleChanges) { this._checkForErrors(); if (!this._registered) this._setUpControl(); + if ('disabled' in changes) { + this._updateDisabled(changes); + } if (isPropertyUpdated(changes, this.viewModel)) { this._updateValue(this.model); @@ -153,4 +158,17 @@ export class NgModel extends NgControl implements OnChanges, resolvedPromise.then( () => { this.control.setValue(value, {emitViewToModelChange: false}); }); } + + private _updateDisabled(changes: SimpleChanges) { + const disabledValue = changes['disabled'].currentValue; + const isDisabled = disabledValue != null && disabledValue != false; + + resolvedPromise.then(() => { + if (isDisabled && !this.control.disabled) { + this.control.disable(); + } else if (!isDisabled && this.control.disabled) { + this.control.enable(); + } + }); + } } diff --git a/modules/@angular/forms/src/directives/number_value_accessor.ts b/modules/@angular/forms/src/directives/number_value_accessor.ts index 8b55e61001..4fec7a9f77 100644 --- a/modules/@angular/forms/src/directives/number_value_accessor.ts +++ b/modules/@angular/forms/src/directives/number_value_accessor.ts @@ -53,4 +53,8 @@ export class NumberValueAccessor implements ControlValueAccessor { this.onChange = (value) => { fn(value == '' ? null : NumberWrapper.parseFloat(value)); }; } registerOnTouched(fn: () => void): void { this.onTouched = fn; } + + setDisabledState(isDisabled: boolean): void { + this._renderer.setElementProperty(this._elementRef.nativeElement, 'disabled', isDisabled); + } } diff --git a/modules/@angular/forms/src/directives/radio_control_value_accessor.ts b/modules/@angular/forms/src/directives/radio_control_value_accessor.ts index f33e2665e0..fd9e069001 100644 --- a/modules/@angular/forms/src/directives/radio_control_value_accessor.ts +++ b/modules/@angular/forms/src/directives/radio_control_value_accessor.ts @@ -127,6 +127,10 @@ export class RadioControlValueAccessor implements ControlValueAccessor, registerOnTouched(fn: () => {}): void { this.onTouched = fn; } + setDisabledState(isDisabled: boolean): void { + this._renderer.setElementProperty(this._elementRef.nativeElement, 'disabled', isDisabled); + } + private _checkName(): void { if (this.name && this.formControlName && this.name !== this.formControlName) { this._throwNameError(); diff --git a/modules/@angular/forms/src/directives/reactive_directives/form_control_directive.ts b/modules/@angular/forms/src/directives/reactive_directives/form_control_directive.ts index 0e3952c177..055cfa163d 100644 --- a/modules/@angular/forms/src/directives/reactive_directives/form_control_directive.ts +++ b/modules/@angular/forms/src/directives/reactive_directives/form_control_directive.ts @@ -12,9 +12,9 @@ import {EventEmitter} from '../../facade/async'; import {StringMapWrapper} from '../../facade/collection'; import {FormControl} from '../../model'; import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../../validators'; - import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '../control_value_accessor'; import {NgControl} from '../ng_control'; +import {ReactiveErrors} from '../reactive_errors'; import {composeAsyncValidators, composeValidators, isPropertyUpdated, selectValueAccessor, setUpControl} from '../shared'; import {AsyncValidatorFn, ValidatorFn} from '../validators'; @@ -81,6 +81,9 @@ export class FormControlDirective extends NgControl implements OnChanges { @Input('ngModel') model: any; @Output('ngModelChange') update = new EventEmitter(); + @Input('disabled') + set disabled(isDisabled: boolean) { ReactiveErrors.disabledAttrWarning(); } + constructor(@Optional() @Self() @Inject(NG_VALIDATORS) private _validators: /* Array */ any[], @Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) private _asyncValidators: @@ -94,6 +97,7 @@ export class FormControlDirective extends NgControl implements OnChanges { ngOnChanges(changes: SimpleChanges): void { if (this._isControlChanged(changes)) { setUpControl(this.form, this); + if (this.control.disabled) this.valueAccessor.setDisabledState(true); this.form.updateValueAndValidity({emitEvent: false}); } if (isPropertyUpdated(changes, this.viewModel)) { diff --git a/modules/@angular/forms/src/directives/reactive_directives/form_control_name.ts b/modules/@angular/forms/src/directives/reactive_directives/form_control_name.ts index 62788fba40..f4d6027b9c 100644 --- a/modules/@angular/forms/src/directives/reactive_directives/form_control_name.ts +++ b/modules/@angular/forms/src/directives/reactive_directives/form_control_name.ts @@ -106,57 +106,58 @@ export class FormControlName extends NgControl implements OnChanges, OnDestroy { @Input('ngModel') model: any; @Output('ngModelChange') update = new EventEmitter(); - constructor(@Optional() @Host() @SkipSelf() private _parent: ControlContainer, - @Optional() @Self() @Inject(NG_VALIDATORS) private _validators: - /* Array */ any[], - @Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) private _asyncValidators: - /* Array */ any[], - @Optional() @Self() @Inject(NG_VALUE_ACCESSOR) - valueAccessors: ControlValueAccessor[]) { - super(); - this.valueAccessor = selectValueAccessor(this, valueAccessors); - } + @Input('disabled') + set disabled(isDisabled: boolean) { ReactiveErrors.disabledAttrWarning(); } - ngOnChanges(changes: SimpleChanges) { - if (!this._added) { - this._checkParentType(); - this.formDirective.addControl(this); - this._added = true; - } - if (isPropertyUpdated(changes, this.viewModel)) { - this.viewModel = this.model; - this.formDirective.updateModel(this, this.model); - } - } + constructor( + @Optional() @Host() @SkipSelf() private _parent: ControlContainer, + @Optional() @Self() @Inject(NG_VALIDATORS) private _validators: + /* Array */ any[], + @Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) private _asyncValidators: + /* Array */ any[], + @Optional() @Self() @Inject(NG_VALUE_ACCESSOR) valueAccessors: ControlValueAccessor[]) { + super(); + this.valueAccessor = selectValueAccessor(this, valueAccessors); + } - ngOnDestroy(): void { this.formDirective.removeControl(this); } + ngOnChanges(changes: SimpleChanges) { + if (!this._added) { + this._checkParentType(); + this.formDirective.addControl(this); + if (this.control.disabled) this.valueAccessor.setDisabledState(true); + this._added = true; + } + if (isPropertyUpdated(changes, this.viewModel)) { + this.viewModel = this.model; + this.formDirective.updateModel(this, this.model); + } + } - viewToModelUpdate(newValue: any): void { - this.viewModel = newValue; - this.update.emit(newValue); - } + ngOnDestroy(): void { this.formDirective.removeControl(this); } - get path(): string[] { return controlPath(this.name, this._parent); } + viewToModelUpdate(newValue: any): void { + this.viewModel = newValue; + this.update.emit(newValue); + } - get formDirective(): any { return this._parent.formDirective; } + get path(): string[] { return controlPath(this.name, this._parent); } - get validator(): ValidatorFn { return composeValidators(this._validators); } + get formDirective(): any { return this._parent.formDirective; } - get asyncValidator(): AsyncValidatorFn { - return composeAsyncValidators(this._asyncValidators); - } + get validator(): ValidatorFn { return composeValidators(this._validators); } - get control(): FormControl { return this.formDirective.getControl(this); } + get asyncValidator(): AsyncValidatorFn { return composeAsyncValidators(this._asyncValidators); } - private _checkParentType(): void { - if (!(this._parent instanceof FormGroupName) && - this._parent instanceof AbstractFormGroupDirective) { - ReactiveErrors.ngModelGroupException(); - } else if ( - !(this._parent instanceof FormGroupName) && - !(this._parent instanceof FormGroupDirective) && - !(this._parent instanceof FormArrayName)) { - ReactiveErrors.controlParentException(); - } - } + get control(): FormControl { return this.formDirective.getControl(this); } + + private _checkParentType(): void { + if (!(this._parent instanceof FormGroupName) && + this._parent instanceof AbstractFormGroupDirective) { + ReactiveErrors.ngModelGroupException(); + } else if ( + !(this._parent instanceof FormGroupName) && !(this._parent instanceof FormGroupDirective) && + !(this._parent instanceof FormArrayName)) { + ReactiveErrors.controlParentException(); + } + } } diff --git a/modules/@angular/forms/src/directives/reactive_errors.ts b/modules/@angular/forms/src/directives/reactive_errors.ts index a7c2eff802..1f73f963c4 100644 --- a/modules/@angular/forms/src/directives/reactive_errors.ts +++ b/modules/@angular/forms/src/directives/reactive_errors.ts @@ -61,4 +61,18 @@ export class ReactiveErrors { ${Examples.formArrayName}`); } + + static disabledAttrWarning(): void { + console.warn(` + It looks like you're using the disabled attribute with a reactive form directive. If you set disabled to true + when you set up this control in your component class, the disabled attribute will actually be set in the DOM for + you. We recommend using this approach to avoid 'changed after checked' errors. + + Example: + form = new FormGroup({ + first: new FormControl({value: 'Nancy', disabled: true}, Validators.required), + last: new FormControl('Drew', Validators.required) + }); + `); + } } diff --git a/modules/@angular/forms/src/directives/select_control_value_accessor.ts b/modules/@angular/forms/src/directives/select_control_value_accessor.ts index 226f4e3b90..043c88ce89 100644 --- a/modules/@angular/forms/src/directives/select_control_value_accessor.ts +++ b/modules/@angular/forms/src/directives/select_control_value_accessor.ts @@ -71,6 +71,10 @@ export class SelectControlValueAccessor implements ControlValueAccessor { } registerOnTouched(fn: () => any): void { this.onTouched = fn; } + setDisabledState(isDisabled: boolean): void { + this._renderer.setElementProperty(this._elementRef.nativeElement, 'disabled', isDisabled); + } + /** @internal */ _registerOption(): string { return (this._idCounter++).toString(); } diff --git a/modules/@angular/forms/src/directives/select_multiple_control_value_accessor.ts b/modules/@angular/forms/src/directives/select_multiple_control_value_accessor.ts index f8df57d43c..18ed1b5c5f 100644 --- a/modules/@angular/forms/src/directives/select_multiple_control_value_accessor.ts +++ b/modules/@angular/forms/src/directives/select_multiple_control_value_accessor.ts @@ -63,7 +63,7 @@ export class SelectMultipleControlValueAccessor implements ControlValueAccessor onChange = (_: any) => {}; onTouched = () => {}; - constructor() {} + constructor(private _renderer: Renderer, private _elementRef: ElementRef) {} writeValue(value: any): void { this.value = value; @@ -101,6 +101,10 @@ export class SelectMultipleControlValueAccessor implements ControlValueAccessor } registerOnTouched(fn: () => any): void { this.onTouched = fn; } + setDisabledState(isDisabled: boolean): void { + this._renderer.setElementProperty(this._elementRef.nativeElement, 'disabled', isDisabled); + } + /** @internal */ _registerOption(value: NgSelectMultipleOption): string { let id: string = (this._idCounter++).toString(); diff --git a/modules/@angular/forms/src/directives/shared.ts b/modules/@angular/forms/src/directives/shared.ts index aa76f6f39d..2db9e34b6e 100644 --- a/modules/@angular/forms/src/directives/shared.ts +++ b/modules/@angular/forms/src/directives/shared.ts @@ -58,6 +58,11 @@ export function setUpControl(control: FormControl, dir: NgControl): void { if (emitModelEvent) dir.viewToModelUpdate(newValue); }); + if (dir.valueAccessor.setDisabledState) { + control.registerOnDisabledChange( + (isDisabled: boolean) => { dir.valueAccessor.setDisabledState(isDisabled); }); + } + // touched dir.valueAccessor.registerOnTouched(() => control.markAsTouched()); } @@ -99,6 +104,15 @@ export function isPropertyUpdated(changes: {[key: string]: any}, viewModel: any) return !looseIdentical(viewModel, change.currentValue); } +export function isBuiltInAccessor(valueAccessor: ControlValueAccessor): boolean { + return ( + hasConstructor(valueAccessor, CheckboxControlValueAccessor) || + hasConstructor(valueAccessor, NumberValueAccessor) || + hasConstructor(valueAccessor, SelectControlValueAccessor) || + hasConstructor(valueAccessor, SelectMultipleControlValueAccessor) || + hasConstructor(valueAccessor, RadioControlValueAccessor)); +} + // TODO: vsavkin remove it once https://github.com/angular/angular/issues/3011 is implemented export function selectValueAccessor( dir: NgControl, valueAccessors: ControlValueAccessor[]): ControlValueAccessor { @@ -111,11 +125,7 @@ export function selectValueAccessor( if (hasConstructor(v, DefaultValueAccessor)) { defaultAccessor = v; - } else if ( - hasConstructor(v, CheckboxControlValueAccessor) || hasConstructor(v, NumberValueAccessor) || - hasConstructor(v, SelectControlValueAccessor) || - hasConstructor(v, SelectMultipleControlValueAccessor) || - hasConstructor(v, RadioControlValueAccessor)) { + } else if (isBuiltInAccessor(v)) { if (isPresent(builtinAccessor)) _throwError(dir, 'More than one built-in value accessor matches form control with'); builtinAccessor = v; diff --git a/modules/@angular/forms/src/form_builder.ts b/modules/@angular/forms/src/form_builder.ts index e7f936053b..6bc3d93da5 100644 --- a/modules/@angular/forms/src/form_builder.ts +++ b/modules/@angular/forms/src/form_builder.ts @@ -64,21 +64,21 @@ export class FormBuilder { * See the {@link FormGroup} constructor for more details. */ group(controlsConfig: {[key: string]: any}, extra: {[key: string]: any} = null): FormGroup { - var controls = this._reduceControls(controlsConfig); - var optionals = <{[key: string]: boolean}>( - isPresent(extra) ? StringMapWrapper.get(extra, 'optionals') : null); - var validator: ValidatorFn = isPresent(extra) ? StringMapWrapper.get(extra, 'validator') : null; - var asyncValidator: AsyncValidatorFn = + const controls = this._reduceControls(controlsConfig); + const validator: ValidatorFn = + isPresent(extra) ? StringMapWrapper.get(extra, 'validator') : null; + const asyncValidator: AsyncValidatorFn = isPresent(extra) ? StringMapWrapper.get(extra, 'asyncValidator') : null; - return new FormGroup(controls, optionals, validator, asyncValidator); + return new FormGroup(controls, validator, asyncValidator); } /** - * Construct a new {@link FormControl} with the given `value`,`validator`, and `asyncValidator`. + * Construct a new {@link FormControl} with the given `formState`,`validator`, and + * `asyncValidator`. */ control( - value: Object, validator: ValidatorFn|ValidatorFn[] = null, + formState: Object, validator: ValidatorFn|ValidatorFn[] = null, asyncValidator: AsyncValidatorFn|AsyncValidatorFn[] = null): FormControl { - return new FormControl(value, validator, asyncValidator); + return new FormControl(formState, validator, asyncValidator); } /** diff --git a/modules/@angular/forms/src/model.ts b/modules/@angular/forms/src/model.ts index 22d31319a0..6ac18f9f87 100644 --- a/modules/@angular/forms/src/model.ts +++ b/modules/@angular/forms/src/model.ts @@ -13,7 +13,7 @@ import {composeAsyncValidators, composeValidators} from './directives/shared'; import {AsyncValidatorFn, ValidatorFn} from './directives/validators'; import {EventEmitter, Observable} from './facade/async'; import {ListWrapper, StringMapWrapper} from './facade/collection'; -import {isBlank, isPresent, isPromise, normalizeBool} from './facade/lang'; +import {isBlank, isPresent, isPromise, isStringMap, normalizeBool} from './facade/lang'; @@ -33,6 +33,12 @@ export const INVALID = 'INVALID'; */ export const PENDING = 'PENDING'; +/** + * Indicates that a FormControl is disabled, i.e. that the control is exempt from ancestor + * calculations of validity or value. + */ +export const DISABLED = 'DISABLED'; + export function isControl(control: Object): boolean { return control instanceof AbstractControl; } @@ -115,6 +121,10 @@ export abstract class AbstractControl { get pending(): boolean { return this._status == PENDING; } + get disabled(): boolean { return this._status === DISABLED; } + + get enabled(): boolean { return this._status !== DISABLED; } + setAsyncValidators(newValidator: AsyncValidatorFn|AsyncValidatorFn[]): void { this.asyncValidator = coerceToAsyncValidator(newValidator); } @@ -175,6 +185,39 @@ export abstract class AbstractControl { } } + disable({onlySelf, emitEvent}: {onlySelf?: boolean, emitEvent?: boolean} = {}): void { + emitEvent = isPresent(emitEvent) ? emitEvent : true; + + this._status = DISABLED; + this._forEachChild((control: AbstractControl) => { control.disable({onlySelf: true}); }); + this._updateValue(); + + if (emitEvent) { + this._valueChanges.emit(this._value); + this._statusChanges.emit(this._status); + } + + this._updateAncestors(onlySelf); + this._onDisabledChange(true); + } + + enable({onlySelf, emitEvent}: {onlySelf?: boolean, emitEvent?: boolean} = {}): void { + this._status = VALID; + this._forEachChild((control: AbstractControl) => { control.enable({onlySelf: true}); }); + this.updateValueAndValidity({onlySelf: true, emitEvent: emitEvent}); + + this._updateAncestors(onlySelf); + this._onDisabledChange(false); + } + + private _updateAncestors(onlySelf: boolean) { + if (isPresent(this._parent) && !onlySelf) { + this._parent.updateValueAndValidity(); + this._parent._updatePristine(); + this._parent._updateTouched(); + } + } + setParent(parent: FormGroup|FormArray): void { this._parent = parent; } abstract setValue(value: any, options?: Object): void; @@ -189,14 +232,18 @@ export abstract class AbstractControl { emitEvent = isPresent(emitEvent) ? emitEvent : true; this._updateValue(); - this._errors = this._runValidator(); + const originalStatus = this._status; this._status = this._calculateStatus(); if (this._status == VALID || this._status == PENDING) { this._runAsyncValidator(emitEvent); } + if (this._disabledChanged(originalStatus)) { + this._updateValue(); + } + if (emitEvent) { this._valueChanges.emit(this._value); this._statusChanges.emit(this._status); @@ -227,6 +274,11 @@ export abstract class AbstractControl { } } + private _disabledChanged(originalStatus: string): boolean { + return this._status !== originalStatus && + (this._status === DISABLED || originalStatus === DISABLED); + } + /** * Sets errors on a form control. * @@ -306,6 +358,7 @@ export abstract class AbstractControl { if (isPresent(this._errors)) return INVALID; if (this._anyControlsHaveStatus(PENDING)) return PENDING; if (this._anyControlsHaveStatus(INVALID)) return INVALID; + if (this._allControlsDisabled()) return DISABLED; return VALID; } @@ -318,6 +371,9 @@ export abstract class AbstractControl { /** @internal */ abstract _anyControls(condition: Function): boolean; + /** @internal */ + abstract _allControlsDisabled(): boolean; + /** @internal */ _anyControlsHaveStatus(status: string): boolean { return this._anyControls((control: AbstractControl) => control.status == status); @@ -350,6 +406,15 @@ export abstract class AbstractControl { this._parent._updateTouched({onlySelf: onlySelf}); } } + + /** @internal */ + _onDisabledChange(isDisabled: boolean): void {} + + /** @internal */ + _isBoxedValue(formState: any): boolean { + return isStringMap(formState) && Object.keys(formState).length === 2 && 'value' in formState && + 'disabled' in formState; + } } /** @@ -375,10 +440,10 @@ export class FormControl extends AbstractControl { _onChange: Function[] = []; constructor( - value: any = null, validator: ValidatorFn|ValidatorFn[] = null, + formState: any = null, validator: ValidatorFn|ValidatorFn[] = null, asyncValidator: AsyncValidatorFn|AsyncValidatorFn[] = null) { super(coerceToValidator(validator), coerceToAsyncValidator(asyncValidator)); - this._value = value; + this._applyFormState(formState); this.updateValueAndValidity({onlySelf: true, emitEvent: false}); this._initObservables(); } @@ -427,10 +492,11 @@ export class FormControl extends AbstractControl { this.setValue(value, options); } - reset(value: any = null, {onlySelf}: {onlySelf?: boolean} = {}): void { - this.markAsPristine({onlySelf: onlySelf}); - this.markAsUntouched({onlySelf: onlySelf}); - this.setValue(value, {onlySelf: onlySelf}); + reset(formState: any = null, {onlySelf}: {onlySelf?: boolean} = {}): void { + this._applyFormState(formState); + this.markAsPristine({onlySelf}); + this.markAsUntouched({onlySelf}); + this.setValue(this._value, {onlySelf}); } /** @@ -443,15 +509,35 @@ export class FormControl extends AbstractControl { */ _anyControls(condition: Function): boolean { return false; } + /** + * @internal + */ + _allControlsDisabled(): boolean { return this.disabled; } + /** * Register a listener for change events. */ registerOnChange(fn: Function): void { this._onChange.push(fn); } + /** + * Register a listener for disabled events. + */ + registerOnDisabledChange(fn: (isDisabled: boolean) => void): void { this._onDisabledChange = fn; } + /** * @internal */ _forEachChild(cb: Function): void {} + + private _applyFormState(formState: any) { + if (this._isBoxedValue(formState)) { + this._value = formState.value; + formState.disabled ? this.disable({onlySelf: true, emitEvent: false}) : + this.enable({onlySelf: true, emitEvent: false}); + } else { + this._value = formState; + } + } } /** @@ -470,14 +556,10 @@ export class FormControl extends AbstractControl { * @stable */ export class FormGroup extends AbstractControl { - private _optionals: {[key: string]: boolean}; - constructor( - public controls: {[key: string]: AbstractControl}, - /* @deprecated */ optionals: {[key: string]: boolean} = null, validator: ValidatorFn = null, + public controls: {[key: string]: AbstractControl}, validator: ValidatorFn = null, asyncValidator: AsyncValidatorFn = null) { super(validator, asyncValidator); - this._optionals = isPresent(optionals) ? optionals : {}; this._initObservables(); this._setParentForControls(); this.updateValueAndValidity({onlySelf: true, emitEvent: false}); @@ -509,30 +591,12 @@ export class FormGroup extends AbstractControl { this.updateValueAndValidity(); } - /** - * Mark the named control as non-optional. - * @deprecated - */ - include(controlName: string): void { - StringMapWrapper.set(this._optionals, controlName, true); - this.updateValueAndValidity(); - } - - /** - * Mark the named control as optional. - * @deprecated - */ - exclude(controlName: string): void { - StringMapWrapper.set(this._optionals, controlName, false); - this.updateValueAndValidity(); - } - /** * Check whether there is a control with the given name in the group. */ contains(controlName: string): boolean { - var c = StringMapWrapper.contains(this.controls, controlName); - return c && this._included(controlName); + const c = StringMapWrapper.contains(this.controls, controlName); + return c && this.get(controlName).enabled; } setValue(value: {[key: string]: any}, {onlySelf}: {onlySelf?: boolean} = {}): void { @@ -562,6 +626,14 @@ export class FormGroup extends AbstractControl { this._updateTouched({onlySelf: onlySelf}); } + getRawValue(): Object { + return this._reduceChildren( + {}, (acc: {[k: string]: AbstractControl}, control: AbstractControl, name: string) => { + acc[name] = control.value; + return acc; + }); + } + /** @internal */ _throwIfControlMissing(name: string): void { if (!Object.keys(this.controls).length) { @@ -601,7 +673,9 @@ export class FormGroup extends AbstractControl { _reduceValue() { return this._reduceChildren( {}, (acc: {[k: string]: AbstractControl}, control: AbstractControl, name: string) => { - acc[name] = control.value; + if (control.enabled || this.disabled) { + acc[name] = control.value; + } return acc; }); } @@ -609,18 +683,19 @@ export class FormGroup extends AbstractControl { /** @internal */ _reduceChildren(initValue: any, fn: Function) { var res = initValue; - this._forEachChild((control: AbstractControl, name: string) => { - if (this._included(name)) { - res = fn(res, control, name); - } - }); + this._forEachChild( + (control: AbstractControl, name: string) => { res = fn(res, control, name); }); return res; } /** @internal */ - _included(controlName: string): boolean { - var isOptional = StringMapWrapper.contains(this._optionals, controlName); - return !isOptional || StringMapWrapper.get(this._optionals, controlName); + _allControlsDisabled(): boolean { + for (let controlName of Object.keys(this.controls)) { + if (this.controls[controlName].enabled) { + return false; + } + } + return !StringMapWrapper.isEmpty(this.controls); } /** @internal */ @@ -729,6 +804,8 @@ export class FormArray extends AbstractControl { this._updateTouched({onlySelf: onlySelf}); } + getRawValue(): any[] { return this.controls.map((control) => control.value); } + /** @internal */ _throwIfControlMissing(index: number): void { if (!this.controls.length) { @@ -748,11 +825,14 @@ export class FormArray extends AbstractControl { } /** @internal */ - _updateValue(): void { this._value = this.controls.map((control) => control.value); } + _updateValue(): void { + this._value = this.controls.filter((control) => control.enabled || this.disabled) + .map((control) => control.value); + } /** @internal */ _anyControls(condition: Function): boolean { - return this.controls.some((control: AbstractControl) => condition(control)); + return this.controls.some((control: AbstractControl) => control.enabled && condition(control)); } /** @internal */ @@ -768,4 +848,12 @@ export class FormArray extends AbstractControl { } }); } + + /** @internal */ + _allControlsDisabled(): boolean { + for (let control of this.controls) { + if (control.enabled) return false; + } + return !!this.controls.length; + } } diff --git a/modules/@angular/forms/test/directives_spec.ts b/modules/@angular/forms/test/directives_spec.ts index 87406afee6..9f17cd48b4 100644 --- a/modules/@angular/forms/test/directives_spec.ts +++ b/modules/@angular/forms/test/directives_spec.ts @@ -74,7 +74,7 @@ export function main() { }); it('should return select multiple accessor when provided', () => { - const selectMultipleAccessor = new SelectMultipleControlValueAccessor(); + const selectMultipleAccessor = new SelectMultipleControlValueAccessor(null, null); expect(selectValueAccessor(dir, [ defaultAccessor, selectMultipleAccessor ])).toEqual(selectMultipleAccessor); @@ -95,7 +95,7 @@ export function main() { it('should return custom accessor when provided with select multiple', () => { const customAccessor = new SpyValueAccessor(); - const selectMultipleAccessor = new SelectMultipleControlValueAccessor(); + const selectMultipleAccessor = new SelectMultipleControlValueAccessor(null, null); expect(selectValueAccessor( dir, [defaultAccessor, customAccessor, selectMultipleAccessor])) .toEqual(customAccessor); @@ -124,9 +124,9 @@ export function main() { }); describe('formGroup', () => { - var form: any /** TODO #9100 */; - var formModel: FormGroup; - var loginControlDir: any /** TODO #9100 */; + let form: FormGroupDirective; + let formModel: FormGroup; + let loginControlDir: FormControlName; beforeEach(() => { form = new FormGroupDirective([], []); @@ -160,7 +160,7 @@ export function main() { describe('addControl', () => { it('should throw when no control found', () => { - var dir = new FormControlName(form, null, null, [defaultAccessor]); + const dir = new FormControlName(form, null, null, [defaultAccessor]); dir.name = 'invalidName'; expect(() => form.addControl(dir)) diff --git a/modules/@angular/forms/test/form_array_spec.ts b/modules/@angular/forms/test/form_array_spec.ts index dfc1f615ec..b33b870dea 100644 --- a/modules/@angular/forms/test/form_array_spec.ts +++ b/modules/@angular/forms/test/form_array_spec.ts @@ -8,9 +8,10 @@ import {fakeAsync, tick} from '@angular/core/testing'; import {AsyncTestCompleter, beforeEach, ddescribe, describe, iit, inject, it, xit} from '@angular/core/testing/testing_internal'; -import {FormArray, FormControl, FormGroup} from '@angular/forms'; +import {AbstractControl, FormArray, FormControl, FormGroup} from '@angular/forms'; import {isPresent} from '../src/facade/lang'; +import {Validators} from '../src/validators'; export function main() { function asyncValidator(expected: any /** TODO #9100 */, timeouts = {}) { @@ -34,8 +35,8 @@ export function main() { describe('FormArray', () => { describe('adding/removing', () => { - var a: FormArray; - var c1: any /** TODO #9100 */, c2: any /** TODO #9100 */, c3: any /** TODO #9100 */; + let a: FormArray; + let c1: FormControl, c2: FormControl, c3: FormControl; beforeEach(() => { a = new FormArray([]); @@ -72,12 +73,12 @@ export function main() { describe('value', () => { it('should be the reduced value of the child controls', () => { - var a = new FormArray([new FormControl(1), new FormControl(2)]); + 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', () => { - var a = new FormArray([]); + const a = new FormArray([]); expect(a.value).toEqual([]); }); }); @@ -102,6 +103,22 @@ export function main() { 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']); @@ -119,6 +136,12 @@ export function main() { ])).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'])) @@ -176,6 +199,22 @@ export function main() { 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']); @@ -254,6 +293,12 @@ export function main() { expect(a.value).toEqual(['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']); @@ -378,6 +423,22 @@ export function main() { 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[]; @@ -413,7 +474,7 @@ export function main() { describe('errors', () => { it('should run the validator when the value changes', () => { - var simpleValidator = (c: any /** TODO #9100 */) => + const simpleValidator = (c: FormArray) => c.controls[0].value != 'correct' ? {'broken': true} : null; var c = new FormControl(null); @@ -433,8 +494,8 @@ export function main() { describe('dirty', () => { - var c: FormControl; - var a: FormArray; + let c: FormControl; + let a: FormArray; beforeEach(() => { c = new FormControl('value'); @@ -451,8 +512,8 @@ export function main() { }); describe('touched', () => { - var c: FormControl; - var a: FormArray; + let c: FormControl; + let a: FormArray; beforeEach(() => { c = new FormControl('value'); @@ -470,8 +531,8 @@ export function main() { describe('pending', () => { - var c: FormControl; - var a: FormArray; + let c: FormControl; + let a: FormArray; beforeEach(() => { c = new FormControl('value'); @@ -499,8 +560,8 @@ export function main() { }); describe('valueChanges', () => { - var a: FormArray; - var c1: any /** TODO #9100 */, c2: any /** TODO #9100 */; + let a: FormArray; + let c1: any /** TODO #9100 */, c2: any /** TODO #9100 */; beforeEach(() => { c1 = new FormControl('old1'); @@ -566,22 +627,22 @@ export function main() { describe('get', () => { it('should return null when path is null', () => { - var g = new FormGroup({}); + const g = new FormGroup({}); expect(g.get(null)).toEqual(null); }); it('should return null when path is empty', () => { - var g = new FormGroup({}); + const g = new FormGroup({}); expect(g.get([])).toEqual(null); }); it('should return null when path is invalid', () => { - var g = new FormGroup({}); + const g = new FormGroup({}); expect(g.get('invalid')).toEqual(null); }); it('should return a child of a control group', () => { - var g = new FormGroup({ + const g = new FormGroup({ 'one': new FormControl('111'), 'nested': new FormGroup({'two': new FormControl('222')}) }); @@ -593,7 +654,7 @@ export function main() { }); it('should return an element of an array', () => { - var g = new FormGroup({'array': new FormArray([new FormControl('111')])}); + const g = new FormGroup({'array': new FormArray([new FormControl('111')])}); expect(g.get(['array', 0]).value).toEqual('111'); }); @@ -601,8 +662,8 @@ export function main() { describe('asyncValidator', () => { it('should run the async validator', fakeAsync(() => { - var c = new FormControl('value'); - var g = new FormArray([c], null, asyncValidator('expected')); + const c = new FormControl('value'); + const g = new FormArray([c], null, asyncValidator('expected')); expect(g.pending).toEqual(true); @@ -612,5 +673,139 @@ export function main() { 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); + }); + + 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']); + }); + + }); + + }); }); } diff --git a/modules/@angular/forms/test/form_builder_spec.ts b/modules/@angular/forms/test/form_builder_spec.ts index 699771f4b8..5e931506bf 100644 --- a/modules/@angular/forms/test/form_builder_spec.ts +++ b/modules/@angular/forms/test/form_builder_spec.ts @@ -14,7 +14,7 @@ export function main() { function asyncValidator(_: any /** TODO #9100 */) { return Promise.resolve(null); } describe('Form Builder', () => { - var b: any /** TODO #9100 */; + let b: FormBuilder; beforeEach(() => { b = new FormBuilder(); }); @@ -24,6 +24,13 @@ export function main() { 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', () => { var g = b.group( {'login': ['some value'], 'password': ['some value', syncValidator, asyncValidator]}); @@ -42,12 +49,6 @@ export function main() { expect(g.controls['login'].asyncValidator).toBe(asyncValidator); }); - it('should create groups with optional controls', () => { - var g = b.group({'login': 'some value'}, {'optionals': {'login': false}}); - - expect(g.contains('login')).toEqual(false); - }); - it('should create groups with a custom validator', () => { var g = b.group( {'login': 'some value'}, {'validator': syncValidator, 'asyncValidator': asyncValidator}); diff --git a/modules/@angular/forms/test/form_control_spec.ts b/modules/@angular/forms/test/form_control_spec.ts index cdab3f5fe8..4e371d8bc1 100644 --- a/modules/@angular/forms/test/form_control_spec.ts +++ b/modules/@angular/forms/test/form_control_spec.ts @@ -12,6 +12,7 @@ import {FormControl, FormGroup, Validators} from '@angular/forms'; import {EventEmitter} from '../src/facade/async'; import {isPresent} from '../src/facade/lang'; +import {FormArray} from '../src/model'; export function main() { function asyncValidator(expected: any /** TODO #9100 */, timeouts = {}) { @@ -43,18 +44,40 @@ export function main() { describe('FormControl', () => { it('should default the value to null', () => { - var c = new FormControl(); + const c = new FormControl(); expect(c.value).toBe(null); }); + 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 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); + }); + + }); + describe('validator', () => { it('should run validator with the initial value', () => { - var c = new FormControl('value', Validators.required); + const c = new FormControl('value', Validators.required); expect(c.valid).toEqual(true); }); it('should rerun the validator when the value changes', () => { - var c = new FormControl('value', Validators.required); + const c = new FormControl('value', Validators.required); c.setValue(null); expect(c.valid).toEqual(false); }); @@ -69,12 +92,12 @@ export function main() { }); it('should return errors', () => { - var c = new FormControl(null, Validators.required); + const c = new FormControl(null, Validators.required); expect(c.errors).toEqual({'required': true}); }); it('should set single validator', () => { - var c = new FormControl(null); + const c = new FormControl(null); expect(c.valid).toEqual(true); c.setValidators(Validators.required); @@ -87,7 +110,7 @@ export function main() { }); it('should set multiple validators from array', () => { - var c = new FormControl(''); + const c = new FormControl(''); expect(c.valid).toEqual(true); c.setValidators([Validators.minLength(5), Validators.required]); @@ -103,7 +126,7 @@ export function main() { }); it('should clear validators', () => { - var c = new FormControl('', Validators.required); + const c = new FormControl('', Validators.required); expect(c.valid).toEqual(false); c.clearValidators(); @@ -114,7 +137,7 @@ export function main() { }); it('should add after clearing', () => { - var c = new FormControl('', Validators.required); + const c = new FormControl('', Validators.required); expect(c.valid).toEqual(false); c.clearValidators(); @@ -127,7 +150,7 @@ export function main() { describe('asyncValidator', () => { it('should run validator with the initial value', fakeAsync(() => { - var c = new FormControl('value', null, asyncValidator('expected')); + const c = new FormControl('value', null, asyncValidator('expected')); tick(); expect(c.valid).toEqual(false); @@ -135,7 +158,7 @@ export function main() { })); it('should support validators returning observables', fakeAsync(() => { - var c = new FormControl('value', null, asyncValidatorReturningObservable); + const c = new FormControl('value', null, asyncValidatorReturningObservable); tick(); expect(c.valid).toEqual(false); @@ -143,7 +166,7 @@ export function main() { })); it('should rerun the validator when the value changes', fakeAsync(() => { - var c = new FormControl('value', null, asyncValidator('expected')); + const c = new FormControl('value', null, asyncValidator('expected')); c.setValue('expected'); tick(); @@ -152,7 +175,7 @@ export function main() { })); it('should run the async validator only when the sync validator passes', fakeAsync(() => { - var c = new FormControl('', Validators.required, asyncValidator('expected')); + const c = new FormControl('', Validators.required, asyncValidator('expected')); tick(); expect(c.errors).toEqual({'required': true}); @@ -164,7 +187,7 @@ export function main() { })); it('should mark the control as pending while running the async validation', fakeAsync(() => { - var c = new FormControl('', null, asyncValidator('expected')); + const c = new FormControl('', null, asyncValidator('expected')); expect(c.pending).toEqual(true); @@ -174,7 +197,7 @@ export function main() { })); it('should only use the latest async validation run', fakeAsync(() => { - var c = new FormControl( + const c = new FormControl( '', null, asyncValidator('expected', {'long': 200, 'expected': 100})); c.setValue('long'); @@ -194,7 +217,7 @@ export function main() { })); it('should add single async validator', fakeAsync(() => { - var c = new FormControl('value', null); + const c = new FormControl('value', null); c.setAsyncValidators(asyncValidator('expected')); expect(c.asyncValidator).not.toEqual(null); @@ -206,7 +229,7 @@ export function main() { })); it('should add async validator from array', fakeAsync(() => { - var c = new FormControl('value', null); + const c = new FormControl('value', null); c.setAsyncValidators([asyncValidator('expected')]); expect(c.asyncValidator).not.toEqual(null); @@ -218,12 +241,20 @@ export function main() { })); it('should clear async validators', fakeAsync(() => { - var c = new FormControl('value', [asyncValidator('expected'), otherAsyncValidator]); + 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', () => { @@ -305,6 +336,14 @@ export function main() { 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', () => { @@ -361,6 +400,15 @@ export function main() { 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()', () => { @@ -368,7 +416,7 @@ export function main() { beforeEach(() => { c = new FormControl('initial value'); }); - it('should restore the initial value of the control if passed', () => { + it('should reset to a specific value if passed', () => { c.setValue('new value'); expect(c.value).toBe('new value'); @@ -376,6 +424,14 @@ export function main() { expect(c.value).toBe('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'); @@ -402,7 +458,6 @@ export function main() { expect(g.value).toEqual({'one': null}); }); - it('should mark the control as pristine', () => { c.markAsDirty(); expect(c.pristine).toBe(false); @@ -457,6 +512,20 @@ export function main() { 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[]; @@ -483,6 +552,15 @@ export function main() { 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']); + }); }); }); @@ -572,7 +650,7 @@ export function main() { describe('setErrors', () => { it('should set errors on a control', () => { - var c = new FormControl('someValue'); + const c = new FormControl('someValue'); c.setErrors({'someError': true}); @@ -581,7 +659,7 @@ export function main() { }); it('should reset the errors and validity when the value changes', () => { - var c = new FormControl('someValue', Validators.required); + const c = new FormControl('someValue', Validators.required); c.setErrors({'someError': true}); c.setValue(''); @@ -590,8 +668,8 @@ export function main() { }); it('should update the parent group\'s validity', () => { - var c = new FormControl('someValue'); - var g = new FormGroup({'one': c}); + const c = new FormControl('someValue'); + const g = new FormGroup({'one': c}); expect(g.valid).toEqual(true); @@ -601,8 +679,8 @@ export function main() { }); it('should not reset parent\'s errors', () => { - var c = new FormControl('someValue'); - var g = new FormGroup({'one': c}); + const c = new FormControl('someValue'); + const g = new FormGroup({'one': c}); g.setErrors({'someGroupError': true}); c.setErrors({'someError': true}); @@ -611,8 +689,8 @@ export function main() { }); it('should reset errors when updating a value', () => { - var c = new FormControl('oldValue'); - var g = new FormGroup({'one': c}); + const c = new FormControl('oldValue'); + const g = new FormGroup({'one': c}); g.setErrors({'someGroupError': true}); c.setErrors({'someError': true}); @@ -623,5 +701,221 @@ export function main() { 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 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 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); + }); + + 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']); + }); + + }); + }); }); } diff --git a/modules/@angular/forms/test/form_group_spec.ts b/modules/@angular/forms/test/form_group_spec.ts index affb21705d..4d9f6d326c 100644 --- a/modules/@angular/forms/test/form_group_spec.ts +++ b/modules/@angular/forms/test/form_group_spec.ts @@ -42,17 +42,17 @@ export function main() { describe('FormGroup', () => { describe('value', () => { it('should be the reduced value of the child controls', () => { - var g = new FormGroup({'one': new FormControl('111'), 'two': new FormControl('222')}); + 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', () => { - var g = new FormGroup({}); + const g = new FormGroup({}); expect(g.value).toEqual({}); }); it('should support nested groups', () => { - var g = new FormGroup({ + const g = new FormGroup({ 'one': new FormControl('111'), 'nested': new FormGroup({'two': new FormControl('222')}) }); @@ -66,7 +66,7 @@ export function main() { describe('adding and removing controls', () => { it('should update value and validity when control is added', () => { - var g = new FormGroup({'one': new FormControl('1')}); + const g = new FormGroup({'one': new FormControl('1')}); expect(g.value).toEqual({'one': '1'}); expect(g.valid).toBe(true); @@ -77,7 +77,7 @@ export function main() { }); it('should update value and validity when control is removed', () => { - var g = new FormGroup( + 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); @@ -91,11 +91,11 @@ export function main() { describe('errors', () => { it('should run the validator when the value changes', () => { - var simpleValidator = (c: any /** TODO #9100 */) => + const simpleValidator = (c: FormGroup) => c.controls['one'].value != 'correct' ? {'broken': true} : null; var c = new FormControl(null); - var g = new FormGroup({'one': c}, null, simpleValidator); + var g = new FormGroup({'one': c}, simpleValidator); c.setValue('correct'); @@ -110,7 +110,7 @@ export function main() { }); describe('dirty', () => { - var c: FormControl, g: FormGroup; + let c: FormControl, g: FormGroup; beforeEach(() => { c = new FormControl('value'); @@ -128,7 +128,7 @@ export function main() { describe('touched', () => { - var c: FormControl, g: FormGroup; + let c: FormControl, g: FormGroup; beforeEach(() => { c = new FormControl('value'); @@ -164,6 +164,23 @@ export function main() { 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'}); @@ -181,6 +198,13 @@ export function main() { .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({ @@ -239,6 +263,22 @@ export function main() { 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'}); @@ -317,6 +357,13 @@ export function main() { 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'}); @@ -440,6 +487,21 @@ export function main() { 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[]; @@ -470,159 +532,48 @@ export function main() { 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']); + }); + }); }); - describe('optional components', () => { - describe('contains', () => { - var group: any /** TODO #9100 */; - - beforeEach(() => { - group = new FormGroup( - { - 'required': new FormControl('requiredValue'), - 'optional': new FormControl('optionalValue') - }, - {'optional': false}); - }); - - // rename contains into has - it('should return false when the component is not included', - () => { 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 included', () => { - expect(group.contains('required')).toEqual(true); - - group.include('optional'); - - expect(group.contains('optional')).toEqual(true); - }); - }); - - it('should not include an inactive component into the group value', () => { - var group = new FormGroup( - { - 'required': new FormControl('requiredValue'), - 'optional': new FormControl('optionalValue') - }, - {'optional': false}); - - expect(group.value).toEqual({'required': 'requiredValue'}); - - group.include('optional'); - - expect(group.value).toEqual({'required': 'requiredValue', 'optional': 'optionalValue'}); - }); - - it('should not run Validators on an inactive component', () => { - var group = new FormGroup( - { - 'required': new FormControl('requiredValue', Validators.required), - 'optional': new FormControl('', Validators.required) - }, - {'optional': false}); - - expect(group.valid).toEqual(true); - - group.include('optional'); - - expect(group.valid).toEqual(false); - }); - }); - - describe('valueChanges', () => { - var g: FormGroup, c1: FormControl, c2: FormControl; + describe('contains', () => { + let group: FormGroup; beforeEach(() => { - c1 = new FormControl('old1'); - c2 = new FormControl('old2'); - g = new FormGroup({'one': c1, 'two': c2}, {'two': true}); + group = new FormGroup({ + 'required': new FormControl('requiredValue'), + 'optional': new FormControl({value: 'disabled value', disabled: true}) + }); }); - it('should fire an event after the value has been updated', - inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { - g.valueChanges.subscribe({ - next: (value: any) => { - expect(g.value).toEqual({'one': 'new1', 'two': 'old2'}); - expect(value).toEqual({'one': 'new1', 'two': 'old2'}); - async.done(); - } - }); - c1.setValue('new1'); - })); + it('should return false when the component is disabled', + () => { expect(group.contains('optional')).toEqual(false); }); - it('should fire an event after the control\'s observable fired an event', - inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { - var controlCallbackIsCalled = 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); - c1.valueChanges.subscribe({next: (value: any) => { controlCallbackIsCalled = true; }}); + group.enable('optional'); - g.valueChanges.subscribe({ - next: (value: any) => { - expect(controlCallbackIsCalled).toBe(true); - async.done(); - } - }); - - c1.setValue('new1'); - })); - - it('should fire an event when a control is excluded', - inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { - g.valueChanges.subscribe({ - next: (value: any) => { - expect(value).toEqual({'one': 'old1'}); - async.done(); - } - }); - - g.exclude('two'); - })); - - it('should fire an event when a control is included', - inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { - g.exclude('two'); - - g.valueChanges.subscribe({ - next: (value: any) => { - expect(value).toEqual({'one': 'old1', 'two': 'old2'}); - async.done(); - } - }); - - g.include('two'); - })); - - it('should fire an event every time a control is updated', - inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { - var loggedValues: any[] /** TODO #9100 */ = []; - - g.valueChanges.subscribe({ - next: (value: any) => { - loggedValues.push(value); - - if (loggedValues.length == 2) { - expect(loggedValues).toEqual([ - {'one': 'new1', 'two': 'old2'}, {'one': 'new1', 'two': 'new2'} - ]); - async.done(); - } - } - }); - - c1.setValue('new1'); - c2.setValue('new2'); - })); - - // hard to test without hacking zones - // xit('should not fire an event when an excluded control is updated', () => null); + expect(group.contains('optional')).toEqual(true); + }); }); + describe('statusChanges', () => { let control: FormControl; let group: FormGroup; @@ -652,15 +603,15 @@ export function main() { describe('getError', () => { it('should return the error when it is present', () => { - var c = new FormControl('', Validators.required); - var g = new FormGroup({'one': c}); + 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', () => { - var c = new FormControl('not empty', Validators.required); - var g = new FormGroup({'one': c}); + 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); @@ -669,8 +620,8 @@ export function main() { describe('asyncValidator', () => { it('should run the async validator', fakeAsync(() => { - var c = new FormControl('value'); - var g = new FormGroup({'one': c}, null, null, asyncValidator('expected')); + const c = new FormControl('value'); + const g = new FormGroup({'one': c}, null, asyncValidator('expected')); expect(g.pending).toEqual(true); @@ -681,8 +632,8 @@ export function main() { })); it('should set the parent group\'s status to pending', fakeAsync(() => { - var c = new FormControl('value', null, asyncValidator('expected')); - var g = new FormGroup({'one': c}); + const c = new FormControl('value', null, asyncValidator('expected')); + const g = new FormGroup({'one': c}); expect(g.pending).toEqual(true); @@ -693,8 +644,8 @@ export function main() { it('should run the parent group\'s async validator when children are pending', fakeAsync(() => { - var c = new FormControl('value', null, asyncValidator('expected')); - var g = new FormGroup({'one': c}, null, null, asyncValidator('expected')); + const c = new FormControl('value', null, asyncValidator('expected')); + const g = new FormGroup({'one': c}, null, asyncValidator('expected')); tick(1); @@ -702,5 +653,161 @@ export function main() { 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); + }); + + 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']); + }); + + }); + + }); }); } diff --git a/modules/@angular/forms/test/reactive_integration_spec.ts b/modules/@angular/forms/test/reactive_integration_spec.ts index d8a61feceb..9a27d2330d 100644 --- a/modules/@angular/forms/test/reactive_integration_spec.ts +++ b/modules/@angular/forms/test/reactive_integration_spec.ts @@ -161,7 +161,7 @@ export function main() { }); describe('programmatic changes', () => { - it('should update the value in the DOM when setValue is called', () => { + it('should update the value in the DOM when setValue() is called', () => { const fixture = TestBed.createComponent(FormGroupComp); const login = new FormControl('oldValue'); const form = new FormGroup({'login': login}); @@ -175,6 +175,89 @@ export function main() { expect(input.nativeElement.value).toEqual('newValue'); }); + describe('disabled controls', () => { + it('should add disabled attribute to an individual control when instantiated as disabled', + () => { + const fixture = TestBed.createComponent(FormControlComp); + const control = new FormControl({value: 'some value', disabled: true}); + fixture.debugElement.componentInstance.control = control; + fixture.detectChanges(); + + const input = fixture.debugElement.query(By.css('input')); + expect(input.nativeElement.disabled).toBe(true); + + control.enable(); + fixture.detectChanges(); + expect(input.nativeElement.disabled).toBe(false); + }); + + it('should add disabled attribute to formControlName when instantiated as disabled', () => { + const fixture = TestBed.createComponent(FormGroupComp); + const control = new FormControl({value: 'some value', disabled: true}); + fixture.debugElement.componentInstance.form = new FormGroup({login: control}); + fixture.debugElement.componentInstance.control = control; + fixture.detectChanges(); + + const input = fixture.debugElement.query(By.css('input')); + expect(input.nativeElement.disabled).toBe(true); + + control.enable(); + fixture.detectChanges(); + expect(input.nativeElement.disabled).toBe(false); + }); + + it('should add disabled attribute to an individual control when disable() is called', + () => { + const fixture = TestBed.createComponent(FormControlComp); + const control = new FormControl('some value'); + fixture.debugElement.componentInstance.control = control; + fixture.detectChanges(); + + control.disable(); + fixture.detectChanges(); + + const input = fixture.debugElement.query(By.css('input')); + expect(input.nativeElement.disabled).toBe(true); + + control.enable(); + fixture.detectChanges(); + expect(input.nativeElement.disabled).toBe(false); + }); + + it('should add disabled attribute to child controls when disable() is called on group', + () => { + const fixture = TestBed.createComponent(FormGroupComp); + const form = new FormGroup({'login': new FormControl('login')}); + fixture.debugElement.componentInstance.form = form; + fixture.detectChanges(); + + form.disable(); + fixture.detectChanges(); + + const inputs = fixture.debugElement.queryAll(By.css('input')); + expect(inputs[0].nativeElement.disabled).toBe(true); + + form.enable(); + fixture.detectChanges(); + expect(inputs[0].nativeElement.disabled).toBe(false); + }); + + + it('should not add disabled attribute to custom controls when disable() is called', () => { + const fixture = TestBed.createComponent(MyInputForm); + const control = new FormControl('some value'); + fixture.debugElement.componentInstance.form = new FormGroup({login: control}); + fixture.detectChanges(); + + control.disable(); + fixture.detectChanges(); + + const input = fixture.debugElement.query(By.css('my-input')); + expect(input.nativeElement.getAttribute('disabled')).toBe(null); + }); + + }); + }); describe('user input', () => { @@ -1203,7 +1286,7 @@ class WrappedValueForm { template: `
-
+ ` }) class MyInputForm { diff --git a/modules/@angular/forms/test/template_integration_spec.ts b/modules/@angular/forms/test/template_integration_spec.ts index 952b2ecadb..4f205e9fbb 100644 --- a/modules/@angular/forms/test/template_integration_spec.ts +++ b/modules/@angular/forms/test/template_integration_spec.ts @@ -7,13 +7,14 @@ */ import {NgFor, NgIf} from '@angular/common'; -import {Component} from '@angular/core'; -import {TestBed, fakeAsync, tick} from '@angular/core/testing'; +import {Component, Input} from '@angular/core'; +import {TestBed, async, fakeAsync, tick} from '@angular/core/testing'; import {beforeEach, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '@angular/core/testing/testing_internal'; -import {FormsModule, NgForm} from '@angular/forms'; +import {ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR, NgForm} from '@angular/forms'; import {By} from '@angular/platform-browser/src/dom/debug/by'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {dispatchEvent} from '@angular/platform-browser/testing/browser_util'; + import {ListWrapper} from '../src/facade/collection'; export function main() { @@ -24,7 +25,7 @@ export function main() { declarations: [ StandaloneNgModel, NgModelForm, NgModelGroupForm, NgModelValidBinding, NgModelNgIfForm, NgModelRadioForm, NgModelSelectForm, NgNoFormComp, InvalidNgModelNoName, - NgModelOptionsStandalone + NgModelOptionsStandalone, NgModelCustomComp, NgModelCustomWrapper ], imports: [FormsModule] }); @@ -364,6 +365,66 @@ export function main() { })); }); + describe('disabled controls', () => { + it('should not consider disabled controls in value or validation', fakeAsync(() => { + const fixture = TestBed.createComponent(NgModelGroupForm); + fixture.debugElement.componentInstance.isDisabled = false; + fixture.debugElement.componentInstance.first = ''; + fixture.debugElement.componentInstance.last = 'Drew'; + fixture.debugElement.componentInstance.email = 'some email'; + fixture.detectChanges(); + tick(); + + 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); + + fixture.componentInstance.isDisabled = true; + fixture.detectChanges(); + tick(); + + expect(form.value).toEqual({name: {last: 'Drew'}, email: 'some email'}); + expect(form.valid).toBe(true); + expect(form.control.get('name.first').disabled).toBe(true); + })); + + it('should add disabled attribute in the UI if disable() is called programmatically', + fakeAsync(() => { + const fixture = TestBed.createComponent(NgModelGroupForm); + fixture.debugElement.componentInstance.isDisabled = false; + fixture.debugElement.componentInstance.first = 'Nancy'; + fixture.detectChanges(); + tick(); + + const form = fixture.debugElement.children[0].injector.get(NgForm); + form.control.get('name.first').disable(); + fixture.detectChanges(); + tick(); + + const input = fixture.debugElement.query(By.css(`[name="first"]`)); + expect(input.nativeElement.disabled).toBe(true); + })); + + it('should disable a custom control if disabled attr is added', async(() => { + const fixture = TestBed.createComponent(NgModelCustomWrapper); + fixture.debugElement.componentInstance.name = 'Nancy'; + fixture.debugElement.componentInstance.isDisabled = true; + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + fixture.whenStable().then(() => { + const form = fixture.debugElement.children[0].injector.get(NgForm); + expect(form.control.get('name').disabled).toBe(true); + + const customInput = fixture.debugElement.query(By.css('[name="custom"]')); + expect(customInput.nativeElement.disabled).toEqual(true); + }); + }); + })); + + }); + describe('radio controls', () => { it('should support ', fakeAsync(() => { const fixture = TestBed.createComponent(NgModelRadioForm); @@ -488,6 +549,30 @@ export function main() { })); }); + describe('custom value accessors', () => { + it('should support standard writing to view and model', async(() => { + const fixture = TestBed.createComponent(NgModelCustomWrapper); + fixture.debugElement.componentInstance.name = 'Nancy'; + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + fixture.whenStable().then(() => { + // model -> view + const customInput = fixture.debugElement.query(By.css('[name="custom"]')); + expect(customInput.nativeElement.value).toEqual('Nancy'); + + customInput.nativeElement.value = 'Carson'; + dispatchEvent(customInput.nativeElement, 'input'); + fixture.detectChanges(); + + // view -> model + expect(fixture.debugElement.componentInstance.name).toEqual('Carson'); + }); + }); + })); + + }); + describe('ngModel corner cases', () => { it('should update the view when the model is set back to what used to be in the view', fakeAsync(() => { @@ -541,7 +626,7 @@ class StandaloneNgModel { @Component({ selector: 'ng-model-form', template: ` -
+
` @@ -549,6 +634,8 @@ class StandaloneNgModel { class NgModelForm { name: string; options = {}; + + onReset() {} } @Component({ @@ -556,7 +643,7 @@ class NgModelForm { template: `
- +
@@ -567,10 +654,11 @@ class NgModelGroupForm { first: string; last: string; email: string; + isDisabled: boolean; } @Component({ - selector: 'ng-model-group-form', + selector: 'ng-model-valid-binding', template: `
@@ -668,6 +756,40 @@ class NgModelSelectForm { cities: any[] = []; } +@Component({ + selector: 'ng-model-custom-comp', + template: ` + + `, + providers: [{provide: NG_VALUE_ACCESSOR, multi: true, useExisting: NgModelCustomComp}] +}) +class NgModelCustomComp implements ControlValueAccessor { + model: string; + @Input('disabled') isDisabled: boolean = false; + changeFn: (value: any) => void; + + writeValue(value: any) { this.model = value; } + + registerOnChange(fn: (value: any) => void) { this.changeFn = fn; } + + registerOnTouched() {} + + setDisabledState(isDisabled: boolean) { this.isDisabled = isDisabled; } +} + +@Component({ + selector: 'ng-model-custom-wrapper', + template: ` + + + + ` +}) +class NgModelCustomWrapper { + name: string; + isDisabled = false; +} + function sortedClassList(el: HTMLElement) { var l = getDOM().classList(el); ListWrapper.sort(l); diff --git a/tools/public_api_guard/forms/index.d.ts b/tools/public_api_guard/forms/index.d.ts index 12696fff9b..c1e2db6cde 100644 --- a/tools/public_api_guard/forms/index.d.ts +++ b/tools/public_api_guard/forms/index.d.ts @@ -2,6 +2,8 @@ export declare abstract class AbstractControl { asyncValidator: AsyncValidatorFn; dirty: boolean; + disabled: boolean; + enabled: boolean; errors: { [key: string]: any; }; @@ -20,6 +22,14 @@ export declare abstract class AbstractControl { constructor(validator: ValidatorFn, asyncValidator: AsyncValidatorFn); clearAsyncValidators(): void; clearValidators(): void; + disable({onlySelf, emitEvent}?: { + onlySelf?: boolean; + emitEvent?: boolean; + }): void; + enable({onlySelf, emitEvent}?: { + onlySelf?: boolean; + emitEvent?: boolean; + }): void; get(path: Array | string): AbstractControl; getError(errorCode: string, path?: string[]): any; hasError(errorCode: string, path?: string[]): boolean; @@ -59,6 +69,8 @@ export declare abstract class AbstractControl { export declare abstract class AbstractControlDirective { control: AbstractControl; dirty: boolean; + disabled: boolean; + enabled: boolean; errors: { [key: string]: any; }; @@ -98,6 +110,7 @@ export declare class CheckboxControlValueAccessor implements ControlValueAccesso constructor(_renderer: Renderer, _elementRef: ElementRef); registerOnChange(fn: (_: any) => {}): void; registerOnTouched(fn: () => {}): void; + setDisabledState(isDisabled: boolean): void; writeValue(value: any): void; } @@ -112,6 +125,7 @@ export declare class ControlContainer extends AbstractControlDirective { export interface ControlValueAccessor { registerOnChange(fn: any): void; registerOnTouched(fn: any): void; + setDisabledState?(isDisabled: boolean): void; writeValue(obj: any): void; } @@ -122,6 +136,7 @@ export declare class DefaultValueAccessor implements ControlValueAccessor { constructor(_renderer: Renderer, _elementRef: ElementRef); registerOnChange(fn: (_: any) => void): void; registerOnTouched(fn: () => void): void; + setDisabledState(isDisabled: boolean): void; writeValue(value: any): void; } @@ -148,6 +163,7 @@ export declare class FormArray extends AbstractControl { length: number; constructor(controls: AbstractControl[], validator?: ValidatorFn, asyncValidator?: AsyncValidatorFn); at(index: number): AbstractControl; + getRawValue(): any[]; insert(index: number, control: AbstractControl): void; patchValue(value: any[], {onlySelf}?: { onlySelf?: boolean; @@ -178,7 +194,7 @@ export declare class FormArrayName extends ControlContainer implements OnInit, O /** @stable */ export declare class FormBuilder { array(controlsConfig: any[], validator?: ValidatorFn, asyncValidator?: AsyncValidatorFn): FormArray; - control(value: Object, validator?: ValidatorFn | ValidatorFn[], asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[]): FormControl; + control(formState: Object, validator?: ValidatorFn | ValidatorFn[], asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[]): FormControl; group(controlsConfig: { [key: string]: any; }, extra?: { @@ -188,7 +204,7 @@ export declare class FormBuilder { /** @stable */ export declare class FormControl extends AbstractControl { - constructor(value?: any, validator?: ValidatorFn | ValidatorFn[], asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[]); + constructor(formState?: any, validator?: ValidatorFn | ValidatorFn[], asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[]); patchValue(value: any, options?: { onlySelf?: boolean; emitEvent?: boolean; @@ -196,7 +212,8 @@ export declare class FormControl extends AbstractControl { emitViewToModelChange?: boolean; }): void; registerOnChange(fn: Function): void; - reset(value?: any, {onlySelf}?: { + registerOnDisabledChange(fn: (isDisabled: boolean) => void): void; + reset(formState?: any, {onlySelf}?: { onlySelf?: boolean; }): void; setValue(value: any, {onlySelf, emitEvent, emitModelToViewChange, emitViewToModelChange}?: { @@ -211,6 +228,7 @@ export declare class FormControl extends AbstractControl { export declare class FormControlDirective extends NgControl implements OnChanges { asyncValidator: AsyncValidatorFn; control: FormControl; + disabled: boolean; form: FormControl; model: any; path: string[]; @@ -226,6 +244,7 @@ export declare class FormControlDirective extends NgControl implements OnChanges export declare class FormControlName extends NgControl implements OnChanges, OnDestroy { asyncValidator: AsyncValidatorFn; control: FormControl; + disabled: boolean; formDirective: any; model: any; name: string; @@ -245,13 +264,10 @@ export declare class FormGroup extends AbstractControl { }; constructor(controls: { [key: string]: AbstractControl; - }, optionals?: { - [key: string]: boolean; }, validator?: ValidatorFn, asyncValidator?: AsyncValidatorFn); addControl(name: string, control: AbstractControl): void; contains(controlName: string): boolean; - /** @deprecated */ exclude(controlName: string): void; - /** @deprecated */ include(controlName: string): void; + getRawValue(): Object; patchValue(value: { [key: string]: any; }, {onlySelf}?: { @@ -380,6 +396,7 @@ export declare class NgForm extends ControlContainer implements Form { export declare class NgModel extends NgControl implements OnChanges, OnDestroy { asyncValidator: AsyncValidatorFn; control: FormControl; + disabled: boolean; formDirective: any; model: any; name: string; @@ -442,6 +459,7 @@ export declare class SelectControlValueAccessor implements ControlValueAccessor constructor(_renderer: Renderer, _elementRef: ElementRef); registerOnChange(fn: (value: any) => any): void; registerOnTouched(fn: () => any): void; + setDisabledState(isDisabled: boolean): void; writeValue(value: any): void; } @@ -450,9 +468,10 @@ export declare class SelectMultipleControlValueAccessor implements ControlValueA onChange: (_: any) => void; onTouched: () => void; value: any; - constructor(); + constructor(_renderer: Renderer, _elementRef: ElementRef); registerOnChange(fn: (value: any) => any): void; registerOnTouched(fn: () => any): void; + setDisabledState(isDisabled: boolean): void; writeValue(value: any): void; }