feat(forms): add support for disabled controls (#10994)

This commit is contained in:
Kara 2016-08-24 16:58:43 -07:00 committed by Victor Berchet
parent 4f8f8cfc66
commit 2b313e4979
24 changed files with 1335 additions and 343 deletions

View File

@ -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<any> {
return isPresent(this.control) ? this.control.statusChanges : null;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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();

View File

@ -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<Validator|Function> */ 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)) {

View File

@ -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<Validator|Function> */ any[],
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) private _asyncValidators:
/* Array<Validator|Function> */ 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<Validator|Function> */ any[],
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) private _asyncValidators:
/* Array<Validator|Function> */ 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();
}
}
}

View File

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

View File

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

View File

@ -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();

View File

@ -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;

View File

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

View File

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

View File

@ -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, <any>[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))

View File

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

View File

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

View File

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

View File

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

View File

@ -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: `
<div [formGroup]="form">
<my-input formControlName="login"></my-input>
</div>
</div>
`
})
class MyInputForm {

View File

@ -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 <type=radio>', 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: `
<form (ngSubmit)="name='submitted'">
<form (ngSubmit)="name='submitted'" (reset)="onReset()">
<input name="name" [(ngModel)]="name" minlength="10" [ngModelOptions]="options">
</form>
`
@ -549,6 +634,8 @@ class StandaloneNgModel {
class NgModelForm {
name: string;
options = {};
onReset() {}
}
@Component({
@ -556,7 +643,7 @@ class NgModelForm {
template: `
<form>
<div ngModelGroup="name">
<input name="first" [(ngModel)]="first" required>
<input name="first" [(ngModel)]="first" required [disabled]="isDisabled">
<input name="last" [(ngModel)]="last">
</div>
<input name="email" [(ngModel)]="email">
@ -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: `
<form>
<div ngModelGroup="name" #group="ngModelGroup">
@ -668,6 +756,40 @@ class NgModelSelectForm {
cities: any[] = [];
}
@Component({
selector: 'ng-model-custom-comp',
template: `
<input name="custom" [(ngModel)]="model" (ngModelChange)="changeFn($event)" [disabled]="isDisabled">
`,
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: `
<form>
<ng-model-custom-comp name="name" [(ngModel)]="name" [disabled]="isDisabled"></ng-model-custom-comp>
</form>
`
})
class NgModelCustomWrapper {
name: string;
isDisabled = false;
}
function sortedClassList(el: HTMLElement) {
var l = getDOM().classList(el);
ListWrapper.sort(l);

View File

@ -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 | number> | 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;
}