feat(forms): add support for disabled controls (#10994)
This commit is contained in:
@ -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;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user