angular icon indicating copy to clipboard operation
angular copied to clipboard

FormGroup.disable() and FormGroup.enable() do not allow resetting disabled state.

Open We-St opened this issue 8 years ago • 16 comments

I'm submitting a...


[ ] Regression (a behavior that used to work and stopped working in a new release)
[x] Bug report
[ ] Feature request
[ ] Documentation issue or request
[ ] Support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question

Current behavior

When dealing with an ngForm, I want to disable all included controls so users cannot modify them, e.g. during submit.

<form (ngSubmit)="onSubmit()" #form="ngForm">
   <input [disabled]="disabled">
   <input disabled="true"> <!-- always disabled -->
</form>

I can access the above form from within the component controller to overwrite the formGroup states:

this.form.form.disable();
this.form.form.enable();

When doing this, first all controls get disabled, then all get enabled. Even the second form which has disabled="true" hard-coded gets enabled and there is no way to get it back to its self-governing state.

Expected behavior

There could be multiple solutions to this:

  • the most specific attribute ([disabled] on the input) could take precedence.
  • a "force" option in disable() and enable()
  • a method on AbstractControl to reset the disabled state.

Minimal reproduction of the problem with instructions

http://plnkr.co/edit/M71eHXdG8DcNJOYZDuhe The plnkr has two inputs: one "regular" and one is always disabled. Click the submit button -> both inputs get disabled. After the timeout -> both inputs get enabled. The "always disabled" input cannot be reset.

What is the motivation / use case for changing the behavior?

If we can revert to the input disabled value, we can build forms that enforce a state during a certain process (e.g. async update) and then reset the disabled states after the async operation is completed.

Environment

Angular version: 4.4.1

Browser:

  • [x] Chrome (desktop) version XX
  • [ ] Chrome (Android) version XX
  • [ ] Chrome (iOS) version XX
  • [ ] Firefox version XX
  • [ ] Safari (desktop) version XX
  • [ ] Safari (iOS) version XX
  • [ ] IE version XX
  • [ ] Edge version XX

We-St avatar Sep 18 '17 15:09 We-St

I am getting the same issue. I have used form builder and set the disabled state on there so I would have thought enabling the form would set it back to the original state as defined on creation of the form group perhaps?

Sorry not helpful in that I'm not actually providing a fix or workaround, just a suggestion on the expected behaviour

debslord avatar Nov 03 '17 14:11 debslord

In theory the AbstractControl.enable() and AbstractControl.disable() methods have a "selfOnly" boolean option that would look like what you're looking for. Unfortunately, after looking at the source code it seems it's being ignored.

hardsetting avatar Mar 20 '18 11:03 hardsetting

Does somebody have a solution?

cp-michal avatar Apr 27 '18 13:04 cp-michal

this works... sorry for the workaround, but i need it now x.X

          ` setTimeout(() => {
		this.formGroup.disable();
	}, 100);`

dontboyle avatar Sep 24 '18 18:09 dontboyle

Within Angular 4.3.2 the formGroup disabled still does not have precedence over inner control's disabled state.

hidegh avatar Oct 15 '18 13:10 hidegh

Any update on the issue? I also need to disable the entire form and then enable it back to initial state.

britvik avatar Jan 30 '19 07:01 britvik

I also would like to hear about update on this issue. Meanwhile I found this hack: If you embed you form controls into fieldset, for example:

  <fieldset [attr.disabled]="disablestatus">
    .... controls here
  </fieldset>

Then you set disablestatus to either true to disable it or to null to enable it. Unfortunately it will require additional HTML tag and some HTML changes but perhaps better than going thru all controls in the form, and I have some nested forms so it requires recursive function to go thru all controls. Also fieldset is not supported on IE but in my case I dropped support for IE so its not an issue for me.

Hope it helps.

olegkap avatar Mar 06 '19 01:03 olegkap

I'm on Angular 7 now, and it appears disable() will disable all form controls, then unfortunately enable() will enable them all, even if they were previously disabled at the control level. I think normally you will already have or need a function that sets the state of dependent controls on the form based on form/model values and/or other external conditions. Best I have found is to disable the form group when I need to, e.g. while saving, then enable the form group, then immediately call my form UI state function to put everything back where it needs to be. It all happens in the same update cycle, so there is no flicker of incorrect state.

I would like to see angular maintain the form and control enable/disable state individually, disable the control if either the control or its form are disabled, enable it if both are enabled. That's the behavior I expected.

rweads0520 avatar Mar 06 '19 14:03 rweads0520

I too have a form that enables/disables FormControls based on the value of other FormControls (seems like a pretty common use case). While a working enable/disable({onlySelf: true}) would be ideal, just setting the value of the parent FormGroup to its existing value fixes the enabled/disabled state of the logic controlled FormControls.

Something like:

this.formGroup.enable({ onlySelf: true, emitEvent: false }); // Hopefully will work one day
this.formGroup.setValue(this.formGroup.value); // Workaround

jzgoda avatar Jun 04 '19 14:06 jzgoda

this is a usable workaround: https://stackoverflow.com/questions/40494968/reactive-forms-disabled-attribute in your html: <formly-form [attr.disabled]="formsDisabledVariable">

in your .ts file: formsDisabledVariable: boolean = false;

and then conditional disable with: this.formsDisabledVariable = true;

re-enable with: this.formsDisabledVariable = null; // nulled not false

jalex13 avatar Oct 04 '19 17:10 jalex13

Just found this guide to be useful for a rather clean way of achieving this.

zzantares avatar Jan 12 '20 02:01 zzantares

any news on this? I have the issue on custom form controls, so I can't add fieldsets everywhere... I've just upgraded to v 7.2.16 but still not working with

this._disabled ? this.groupCtrl.disable({onlySelf: !0, emitEvent: !1}) : this.groupCtrl.enable({onlySelf: !0, emitEvent: !1});

tonysamperi avatar Jan 14 '20 16:01 tonysamperi

this works... sorry for the workaround, but i need it now x.X

          ` setTimeout(() => {
		this.formGroup.disable();
	}, 100);`

This works for me. Any idea why it does?

aninaslyan avatar Nov 02 '20 07:11 aninaslyan

Here is my workaround. It's in typescript, but it's vanilla. They're not elegant, but they work.

I made these 2 functions:

export function mapDisabledState(form: FormGroup | FormArray) {
  const result = { rootFormDisabledStatus: form.disabled };
  Object.keys(form.controls).forEach(key => {
    const abstractControl = form.controls[key];
    if (abstractControl instanceof FormGroup || abstractControl instanceof FormArray) {
      result[key] = mapDisabledState(abstractControl);
    } else {
      result[key] = abstractControl.disabled;
    }
  });
  return result;
}

export function restoreFormDisabledState(form: FormGroup | FormArray, keymap: { [pathKey: string]: any }) {
  Object.keys(keymap).forEach(function (key) {
    if (key === 'rootFormDisabledStatus') {
      if (keymap[key]) {
        form.disable({ onlySelf: true, emitEvent: false });
      }
    } else {
      const abstractControl: FormGroup = form.controls[key];
      const result = keymap[key];
      if (typeof result === 'object') {
        restoreFormDisabledState(abstractControl, keymap[key]);
      } else if (result) {
        abstractControl.disable({ onlySelf: true, emitEvent: false });
      }
    }
  });
}

Then right before form submission I do this so the user can't touch it while it's sending:

        this.disabledControlsState = mapDisabledState(this.detailsForm);
        this.detailsForm.disable();

and then after the async call completes I do this

        this.detailsForm.enable();
        restoreFormDisabledState(this.detailsForm, this.disabledControlsState);

I could probably have skipped the last enable and just had the form only re-enable controls that were false but it worked so I moved on.

bdavis252 avatar Aug 06 '21 13:08 bdavis252

+1

soori-reddy avatar Nov 03 '21 19:11 soori-reddy

@bdavis252 's workaround works for me :+1:

Another workaround (more effort, but also a bit cleaner maybe) would be to use https://github.com/ngneat/reactive-forms (a wrapper to angular's form classes) and implement all of this reactively (using disabledWhile() on each field).

floAtEbcont avatar Mar 07 '23 18:03 floAtEbcont

Here is my workaround. It's in typescript, but it's vanilla. They're not elegant, but they work.

I made these 2 functions:

This is actually a good workaround if the form is small then you can use the upper solution passively with this.

let disbaledState = null
this.f.statusChanges.subscribe(()=> {
      if(this.f.disabled){
        restoreFormDisabledState(this.f, disbaledState);
      }
      else {
        disbaledState = mapDisabledState(this.f)
      }

now this.f.enable(); , this.f.disable(); will work while preserving the disable state

and a small improvement in restoreFormDisabledState

export function mapDisabledState(form: FormGroup | FormArray) {
  const result = { rootFormDisabledStatus: form.disabled };
  Object.keys(form.controls).forEach(key => {
    const abstractControl = form.controls[key];
    if (abstractControl instanceof FormGroup || abstractControl instanceof FormArray) {
      result[key] = mapDisabledState(abstractControl);
    } else {
      result[key] = abstractControl.disabled;
    }
  });
  return result;
}

export function restoreFormDisabledState(form: FormGroup | FormArray, keymap: { [pathKey: string]: any }) {
  if(!keymap) return; 
  Object.keys(keymap).forEach(function (key) {
    if (key === 'rootFormDisabledStatus') {
      if (keymap[key]) {
        form.disable({ onlySelf: true, emitEvent: false });
      }
    } else {
      const abstractControl: FormGroup = form.controls[key];
      const result = keymap[key];
      if (typeof result === 'object') {
        restoreFormDisabledState(abstractControl, keymap[key]);
      } else if (result) {
        abstractControl.disable({ onlySelf: true, emitEvent: false });
      }
    }
  });
}

aammfe avatar Oct 20 '23 16:10 aammfe

AFAIK this has been "fixed" in v15. with the following breaking change https://angular.io/guide/update-to-version-15#setdisabledstate-is-always-called-when-a-controlvalueaccessor-is-attached

JeanMeche avatar Apr 08 '24 12:04 JeanMeche

This issue has been automatically locked due to inactivity. Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.