angular / angular

Deliver web apps with confidence 🚀

Home Page:https://angular.dev

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

RC6 Forms: disabled attribute cannot be set dynamically anymore

0cv opened this issue · comments

I'm submitting a bug/feature request (check one with "x")

[x] bug report => search github for a similar issue or PR before submitting
[x] feature request
[ ] support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question

Current behavior
Have a look at that, (demo: http://plnkr.co/edit/P25dYPC5ChRxpyxpL0Lj?p=preview):

@Component({
  selector: 'my-app',
  providers: [],
  template: `
    <div [formGroup]="form">
      <input formControlName="first" [disabled]="isDisabled">
    </div>
  `,
  directives: []
})
export class App {
  isDisabled = true
  form = new FormGroup({
    'first': new FormControl('Hello')
  })
}

There is a warning message asking to refactor the code, but more importantly, the input is unfortunately not disabled. Refactoring the code to what is suggested is not helping either, i.e. doing this will not work, (demo: http://plnkr.co/edit/Gf7FGR42UXkBh6e75cm2?p=preview):

@Component({
  selector: 'my-app',
  providers: [],
  template: `
    <div [formGroup]="form">
      <input formControlName="first">
    </div>
  `,
  directives: []
})
export class App {
  isDisabled = false
  form = new FormGroup({
    'first': new FormControl({value: 'hello', disabled: this.isDisabled})
  })

  constructor() {
    setTimeout(() {
      this.isDisabled = true  
    }, 10)
  }
}

Expected/desired behavior
After the callback of the setTimeout is executed, the input should be disabled, but it is not.

Reproduction of the problem
Yes, please look here: http://plnkr.co/edit/P25dYPC5ChRxpyxpL0Lj?p=preview

What is the motivation / use case for changing the behavior?
To have the same behaviour, or similar one than in RC5. At least, we shall have a possibility, even if it's a breaking change, to set dynamically the disabled attribute. As of now, it does not seem possible anymore.

Please tell us about your environment:

  • Angular version: 2.0.0-rc.6
  • Browser: [ Chrome 52 ]
  • Language: [ TypeScript ]
commented

@Krisa If you want to set the disabled state programmatically, you'll need to call disable(). Changing this.isDisabled will never have an effect because it is passed by value.

export class App {
  isDisabled = false
  form = new FormGroup({
    'first': new FormControl({value: 'hello', disabled: false})
  })

  constructor() {
    setTimeout(() => {
      this.form.get('first').disable();
    }, 10)
  }
}

See example here: http://plnkr.co/edit/UXNiM8Pz73Go27nDRD7M?p=preview

Re the first example, we deliberately haven't hooked up the template-driven entry point to reactive forms. That's why we throw the warning. If we did hook it up, users would immediately run into 'changed after checked' errors because the validation of reactive forms occurs synchronously. We generally don't suggest mixing reactive and template-driven patterns for this reason.

We considered throwing an error instead, but we were concerned that if someone had a custom form component that used disabled as an input for something unrelated, their code would break. Perhaps we can make the warning message a bit clearer. Let me know if you think the language could be changed. In any case, closing this as it works as designed.

@kara - I was aware of the disable method, but it's not the same, at least not in my case.

Let me take a bit more complicated example than what we can see above. I have a form composed of roughly 70 to 80 fields split across several components. Some part of the form is disabled/enabled, shown/hidden based on either some attributes of the record (on load, or on save), or depending on the user viewing this record, or even from where the record is being called (i.e. different route). I'm playing with the disabled and ngIf attribute / directive (required is so far always static in my case) but all are defined by, I would call that rules, at the attribute/directive level. This approach is quite consistent for me, ngIf like disabled are all in the html template.

Now, if I have to refactor this code based on what you suggest, I will probably need a central function in my class, and call disable() or enable() for each field, based on the value of the form and from external property (user profile, route params, etc.). This function shall subscribe to valueChanges, and also be called when the component is loaded, or when the save method is called. Definitely doable, but it feels terribly awkward, at least compared to the current solution, don't you think?

Either way, I will have to live with what is implemented. I have so far downgraded the form module to 0.3.0, hoping for a PR resolving the situation above, but based on your comment, I fear this spec is not going to change, is it? Could you please (re)confirm one or the other?

Thanks
Chris

@kara Thanks for your great work so far! Please reconsider disabled property bindings in reactive forms as this is an essential feature to any dynamic forms approach. Otherwise it is not possible to directly change the enabled/disabled state of a control by simply setting a corresponding business object model property on the outside. It would need native ES6 Proxies to intercept the disabled-setter call of that object model in order to call disable() or enable() on the FormControl as well. I myself building ng2 Dynamic Forms would really appreciate it!

If I call this.myForm.controls["code"].disable();, this.myForm.valid is always false:

this.myForm.patchValue({code: 17});
console.log(this.myForm.valid);
this.myForm.controls["code"].disable();
console.log(this.myForm.valid);

This outputs

true
false

Why is this the case? I want the control to be disabled, but the from itself to be still valid.

@capi Looks like a bug to my opinion, but it's also not related to the original issue I've reported, which is the ability to set the disabled attribute dynamically.

Please open a new issue for that to not mix up things

@capi I have the same behaviour like you. For me this is wrong....

@maku I filed #11379 for this, we should not mix the two issues, @Krisa is entirely right about that.

@udos86 @Krisa We are experiencing the same behavior, and it looks quite troublesome to solve: on one hand, we could code the dynamic enable/disable behavior by subscribing to valueChangesof FormControl, but it requires lot of code each time (and we have large forms with many fields).

On the other hand, I was thinking about a custom directive to replace disable, taking the FormControland an Observable, in order to keep the reactive approach in templates, but I need to experiment with it.

Is there any update on this? I mean, now you have a view framework that lets you work with declarative, one-way binding code for a load of attributes and for whole content items, but then for disabled attribute you're stuck to imperative code?

@kara @pkozlowski-opensource
Positive or negative, it would be great to have a feedback, even very quick, on what's coming soon, to not let us in the dark, potentially refactoring unnecessarily our code.

Thank you

@Krisa, i would recommend to avoid reactive forms. Reactive forms require a lot of coding if want interaction with other parts of template and offer very small benefits. You should be able to create configurable custom validator (or even validate your data model manually) and use it instead of required, also you could work with a set of forms instead of combining everything into one big form.

@Krisa On our project, we quick-solved this by using two custom utility methods:

import { FormGroup, FormControl, AbstractControl } from "@angular/forms";

export function includeInForm(form: FormGroup, controlName: string, includeChildren?: boolean) {
    const ctl = form.get(controlName);
    makeOptional(ctl, false, includeChildren);
}

export function excludeFromForm(form: FormGroup, controlName: string, excludeChildren?: boolean) {
    const ctl = form.get(controlName);
    makeOptional(ctl, true, excludeChildren);
}

function makeOptional(ctl: AbstractControl, isOptional: boolean, children?: boolean) {
    if (isOptional) {
        (<any>ctl).__validator = ctl.validator || (<any>ctl).__validator;
        ctl.clearValidators();
    } else {
        ctl.setValidators(ctl.validator || (<any>ctl).__validator);
    }

    if (children) {
        Object.keys((<FormGroup>ctl).controls).forEach((control) => {
            makeOptional(ctl.get(control), isOptional);
        });
    }

    ctl.updateValueAndValidity();
}

It might not be the most elegant - for sure - but it works and made the code work again with minimal changes.

I'm also looking for a simpler and less verbose solution to disable using form variables!

Why is this closed? There's still no clear way to declaratively disable a form control, nor workaround for it.

Seems that the angular team doesnt want to communicate on this...max be thés are still discussing alternatives

Ok, best workaround I could think of is to create a directive to do the dirty job, and "declaretively" test for a boolean condition and get a form/input/FormControl disabled without the need test for expressions that sometimes aren't observables where it's easy to know when something changed (i.e.: to depend on the change detection system).

You can even use it to disable other FormControl, or use another FormControl.value as a condition to disable itself.

@Directive({
  selector: '[disableFC][disableCond]'
})
export class DisableFCDirective {
  @Input() disableFC: FormControl;

  constructor() { }

  get disableCond(): boolean { // getter, not needed, but here only to completude
    return !!this.disableFC && this.disableFC.disabled;
  }

  @Input('disableCond') set disableCond(s: boolean) {
    if (!this.disableFC) return;
    else if (s) this.disableFC.disable();
    else this.disableFC.enable();
  }
}

And it can be used like:
<input type="text" [formControl]="textFC" [disableFC]="textFC" [disableCond]="anotherElement.value < 10" />

@andrevmatos
Hi there - have been grappling with this issue and tried to apply your suggested fix without success. Would you mind taking a look at this Plunk and clarifying where its going wrong?

@raisindetre
Before all, you need to set formControlName on the selects for it to use the controls inside the formGroup, or formControl to set the control directly. After that, you can use my directive to enable or disable dinamically the control. I changed your example to demonstrate my directive disabling the first select based on a boolean variable (toggled by the button), and the second to disable it based oh the value of the first.

Ah, got you - thanks! I was almost there at one point but I was wrapping the formControlName directive in square brackets. Still trying to get the hang of model/reactive driven forms.

@andrevmatos

I updated your directive to what I would consider a bit cleaner of an approach, sharing the formControl input passed from the html element that is already a formConrol.

@Directive({
    selector: '[formControl][disableCond]'
})
export class DisableFormControlDirective {
    @Input() formControl: FormControl;

    constructor() { }

    get disableCond(): boolean { // getter, not needed, but here only to completude
        return !!this.formControl && this.formControl.disabled;
    }

    @Input('disableCond') set disableCond(s: boolean) {
        if (!this.formControl) return;
        else if (s) this.formControl.disable();
        else this.formControl.enable();
    }
}

This allows us to remove the duplication from the template

<input type="text" [formControl]="textFC" [disableCond]="anotherElement.value < 10" />

@kara
Although I would really appreciate it if the Angular team could update the reactive forms component to track the formControl disabled property correctly.

@andrevmatos @PTC-JoshuaMatthews we've found also the version at this GH issue comment by @unsafecode to be quite interesting.

I have been facing the same issue - there isn't a clean way to disable portions of a reactive/model-driven form. The use case is pretty common: Consider a group radio buttons that each display a child form when selected, and each subform that is not selected needs to be disabled in order to make the entire form valid.

I really like @PTC-JoshuaMatthews 's DisableFormControlDirective, but then I realized that it defeats the purpose of using reactive forms... In other words, the disabling becomes template-driven (to test it, you need to parse the template). As far as I can tell, the best way to stay model-driven/reactive is to subscribe to valueChanges on a control and then call disable on the corresponding subform (which is very verbose/ugly). I'm really surprised there isn't a better way to achieve this.

@pmulac while @PTC-JoshuaMatthews directive can be definitely useful, I'd just like to point out that in our project in the end for many single controls (e.g. input, checkbox) that had to be disabled, we switched to binding on [readonly] in order to actually disable that input. If validation was somehow involved, e.g. a readonly field had to be ignored under some condition, we made custom validation that took condition into account.
On the other hand, if it was a matter of disabling whole forms, it looks to me that binding on [disabled] still works good, with no warning.
Just our 2¢

@BrainCrumbz

One issue with doing it with [readonly] or [disabled] is that the actual form control object will not be in a disabled state, and that will affect the functionality of the form. For instance any validations you have attached to the form control will still run, so if you have a required field that is disabled the form will be considered invalid. Using the directive I posted will properly disable the form control and skip validation on disabled fields.

EDIT

I figured out how to do it without using a directive, see my SO link below.

I just saw this warning in the console, so I am trying to solve this the RxJS way as per the comment from @pmulac - subscribe to ValueChanges.

In my case I need to enable a control that is disabled on init.

SO question here if anyone can shed some light on how to solve this, thanks.

@rmcsharry See #13570 for an alternate solution

@noamichael Thanks for the ref.

@kemsky (kemsky commented on Sep 22, 2016),

This actually seems like the best approach, IMO. I first realized Microsoft had a hand in the Angular 2 project when I saw how much Boilerplate gets implemented into projects when they use ReactiveForms. For some things, I'm thrilled about Microsoft's influence on Angular2, but this is one area where I'll be rejecting the MS-Boilerplating -- assuming its their hand in it.

There's a correct way to do things, and there's a right way. If you're throttling the Engineer's productivity so much without much benefit -- not to mention adding more Coupling in the system -- then that's not the "right way". Why don't we just make it so your application blows up if an input is not wrapped in a form?

Why ReactiveForms???
What benefits do you really get? Seems like validation (etc) is better done using a Visitor Pattern, Decorator, Mediator, Service or something else anyway.

That said -- before I throw the ugly baby out with the bathwater -- could you gives us some insight to any caveats you've encountered on the 'no-reactive-forms-boilerplate' bandwagon?

Much Appreciated!

You can solve it with a Directive. A directive named Canbedisabled and a poperty "blocked", for example. Write a setter for blocked and set it to nativelement.disabled property.

Code example:

@Directive({
  selector : ["canbedisabled"]
})
export class Canbedisabled{
  
    constructor(private el: ElementRef) {
       
    }
  
  @Input()
  set blocked(blocked : boolean){
    this.el.nativeElement.disabled = blocked;
  }
}

 <input formControlName="first" canbedisabled [blocked]="isDisabled">

I've been handling this scenario by tying into the ngDoCheck Lifecycle hook.

ngDoCheck() {
    if (this.childControl.disabled != this.isDisabled) {
        this.isDisabled ? this.childControl.disable() : this.childControl.enable();
    }
}

Don't know why, but when I call .enable() or .disable() twice in a row, it works !

  form.controls['partPerson'].enable();
  form.controls['partPerson'].enable();

That kind of restrictive approach take down the advantage of using Angular Reactive forms since disable and enable an element is a pretty much a basic feature, specially if you are building a dynamic form.

So if you need to build a complex dynamic form, and need total control on that form, right now it's better use the template-driven approach, even though it can means more work, such as validation, down the road this approach will be bather then end up with a complex code full of workarounds to solve basic things like enable and disable a form element.

Btw, there is nothing that says you can not use both approaches, reactive and template-driven, in the same application, just be aware of the advantages and limitations of each one.

https://angular.io/docs/ts/latest/guide/reactive-forms.html

commented

Erm... maybe I'm a bit off here, but I was able to get the disabled feature working with [attr.disabled]="isDisabled"

commented

This worked:

<input [attr.disabled]="disabled?'':null"/>
private disabled: boolean = false;

// disable input box
disableInput(): void {
   this.disabled = true;
}

// enable input box
enableInput(): void {
   this.disabled = false;
}

@kekeh Thanks, It works as expected

@Highspeed7 that's because you're using the attribute binding rather than the property binding. I'm sure there's some reason this is ineffective at some level, but it works for my needs, thanks for pointing it out!

@kekeh It works

@kekeh thanks! it works like a charm :)

does not work for me

commented

You can use form.getRawValue()

The problem could be solved the 'reactive' way, if the FormControl constructor accepted a function for the disabled property of the first parameter. Currently it only accepts a boolean value.
E.g.

  form = new FormGroup({
    'first': new FormControl(),
    'second': new FormControl({value: '', disabled: formValue => formValue.first === 'something'})
  })

Disabled, should definitely be allow to accept a function. Disabled state will often change and it is a mess to have helper methods and watchers just to check a condition.

Really appreciate the current 'hack-y' solutions provided while we wait for this to get changed

commented

@medeirosrich : you are good buddy !

Best solution is your !

Works like a charm:

ngDoCheck() {
    if (this.childControl.disabled != this.isDisabled) {
        this.isDisabled ? this.childControl.disable() : this.childControl.enable();
    }
}

Agree with the many others...disabled NEEDS to be dynamically controlled, otherwise it's fairly useless

@kara why is this closed?

@billfranklin The correct way is to bind to attr.disabled. Check the solution that kekeh posted.

@kekeh 's solution is great but it does not work with custom form controls. I am trying to conditionally disable form fields in angular2-json-form-schema. I am planning to do it by using a directive the way @PTC-JoshuaMatthews has suggested. Is there any better way to achieve the same? I know I can implement ngDoCheck in custom controls but it won't be an efficient solution.

any news on this?

Not sure why this is closed even though no clean solution/approach was given ...

@mkotsollaris Did you read the reply with the most thumbs up?

I ended up abandoning this approach altogether in my applications. Instead of setting isDisabled property on my component and expecting the form to track that properties changes, I instead call this.form.controls[“mycontrol”].disable() in the ngAfterViewInit hook. If the status changes I explicitly call enable() to reactive it.

This approach works fine as I’m already responding to some event to enable/disable the control so no problem doing it directly through the form api as opposed to attempting to bind the disabled status to a property. I suspect my desire to bind it comes from my angular 1 days.

Is it good/correct way of using below code for disabling the forms ???

<input [attr.disabled]="disabled?'':null"/>

I have tried with the suggestion provided by angular,
But still it was throwing the warning, and the field isn't disabled.
In component

  form = new FormGroup({
    firstName: new FormControl({value: 'Vishal', disabled: true}, Validators.required),
    lastName: new FormControl('Drew', Validators.required)
  });

Then, in html

<input type="text" formControlName="firstName" 
       [value]="form.controls['firstName'].value"
       [disabled]="form.controls['firstName'].disabled">

@Highspeed7 That works "partially" in that the element's disabled attribute value is bound to the expected value, but for some arcane HTML standard reason, the disabled attribute need only exist for it to disable the element, even if disabled="false".

@kekeh Added to this solution by binding the ternary isDisabled ? '' : null instead of just isDisabled. This is basically a hack to get Angular to clear the attribute entirely when the form is re-enabled.

So on the template, this would be the workaround:
[attr.disabled]="isDisabled ? '' : null"

What are the implications of setting [attr.disabled] vs [disabled]?

@sgronblo afaik there aren't any. I was under the impression that [disabled] is just shorthand for [attr.disabled] but I could be wrong.

commented

This worked:

<input [attr.disabled]="disabled?'':null"/>
private disabled: boolean = false;

// disable input box
disableInput(): void {
   this.disabled = true;
}

// enable input box
enableInput(): void {
   this.disabled = false;
}

It worked for me even by just having this.disabled = false; etc, wherever I needed it. The functions weren't necessary.

this.myForm.controls['id'].disable();

Is there a more proper solution for this case instead of binding to the disabled attribute (which is kept after the form is re-enabled) ?

@danzrou Does the answer directly above you not work for you? Just disable the form control that the input is tied to and the input will be dealt with automatically.

@danzrou yes, fwiw, afaik that is the defacto way to enable/disable form controls:
someFormGroup.controls['controlName'].disable()
and
someFormGroup.controls['controlName'].enable()

Keep in mind that reactive forms are not the standard form set. They are intended to be used programmatically, not through the dom, which is why (I suspect) they don't allow the bindings anymore.

My current situation is that I am passing a form control to a child component and the control is changing dynamically according to user selection. If I pass a control which has disabled=true, the input element is indeed disabled. Then I pass a a new control which has disabled=false (and I made sure it is not disabled) and yet the disabled attribute remains on the input field. This still happens in Angular 7.2.3

@danzrou can you elaborate on how are you passing the form control into the child component? My guess is that the child component isn’t seeing the new form control.

@PTC-JoshuaMatthews it passes it using standard input. Some more details:

  • Each time user clicks on some element, new form controls are generated for this request therefore the reference for them is not kept
  • This means a new control instance is being pass each time (not the same one with property changes)

Is there a more proper solution for this case instead of binding to the disabled attribute (which is kept after the form is re-enabled) ?

this.myForm.controls['id'].enable();

@danzrou I could see why that wouldn’t work, you’re swapping out the form control but not changing disabled state while it is bound to the input. Form controls aren’t really meant to be swapped out like that, there’s no event taking place for it to react to.

If you’re sure that you have to swap form controls, I would recommend using an input setter to detect when the form control changes and re-initializing the input when this happens.

https://stackoverflow.com/questions/36653678/angular2-input-to-a-property-with-get-set

As for how to reinitialize it a dirty solution would be by hiding it with an ngIf and then showing it in a timeout directly after. A cleaner solution might just be to call formControl.setDisabled(formControl.disabled). Just as long as you’re doing this while the form control is bound to the input. Might have to get creative with a timeout

please provide a flag in angular config to disable this warning

@sgentile I think the entire point of the warning is that you're using reactive forms, and therefore should not be using template-driven-form syntax to control form state. This could cause a lot of confusion when a developer sees reactive forms, tries to program in reactive forms, and unknowingly ends up fighting a template-driven form convention because it was implemented in conflict. The solution is to set/unset the disabled state in code just like you're handing the rest of your reactive form state (myForm.controls['someControl'].disable())

@sgentile Yeah sorry - wasn't attempting to assume but without more info you sound like most developers who complain about a feature w/o a justification. Framework and language devs have to make the most generic choices possible and a lot of times that gets overlooked. I think your case is a valid point. Ideally you'd submit a feature request for this since this is a closed ticket that is attempting to report a bug.

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.