Below is a simple form with two fields and a button. We will use this setup to illustrate the different ways to disable form controls.
Here’s how we create this form in our application’s code:
Form component class
@Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) export class AppComponent implements OnInit { form: FormGroup; constructor(private formBuilder: FormBuilder) { } ngOnInit(): void { this.form = this.formBuilder.group({ firstName: [{ value: 'Foo', disabled: true }, [Validators.required]], lastName: ['Bar'] }); } onSubmit(): void { } get firstName(): FormControl { return this.form.controls.firstName as FormControl; } get lastName(): FormControl { return this.form.controls.lastName as FormControl; } }
Note that in the component class I have defined accessors to the two form controls for the ease of access.
Form template
<form [formGroup]="form">
<h1>Angular Reactive Form</h1>
<input formControlName="firstName" placeholder="First Name" />
<input formControlName="lastName" placeholder="Last Name" />
<button (click)="onSubmit()">Submit</button>
</form>
I’m using formGroup
and formControlName
directives from Reactive Forms API here.
Using the FormControl’s disable() instance method
This method will disable both the UI control and the FormControl’s instance. Let us see how we’d use this method to disable a form control. In the ngOnInit function, after creating the form instance, we can disable the lastName form control like below.
ngOnInit(): void { this.form = this.formBuilder.group({ firstName: [{ value: 'Foo', disabled: true }, [Validators.required]], lastName: ['Bar'] }); this.lastName.disable(); }
If we console.log the control lastName we’ll see its instance will be disabled.
This way is quick and straightforward.
When would this way of disabling FormControls be a disadvantage?
When we depend on a FormGroup’s validity status to make some decisions. Let’s see how, let’s change our ngOnInit code a bit by adding console logs.
ngOnInit(): void { this.form = this.formBuilder.group({ firstName: [{ value: 'Foo', disabled: true }, [Validators.required]], lastName: ['Bar'] }); this.lastName.disable(); console.log(this.lastName); console.log('Form:::', this.form); }
We’ve given the form controls default values, disabled both the controls and required the firstName control by having a required validator on its configuration. Note that the firstName control is disabled by default.
The last console.log’s output is below.
Something worth noting is that when we have all form controls disabled, irrespective of their validity status', the controls' instance status properties will be set to "DISABLED" hence the FormGroup's status will also be set to "DISABLED".
In the log snippet above, can you also see that the form’s valid and invalid properties are both false, but the controls have values and are valid? Can you see the confusion this way of disabling controls might cause when the form’s validity is a dependency in some decisions we have to make?
For example, let's say we have a big form implementation and we are aware that some form fields will need to be disabled.
For instance, say we have a second form in a separate page that requires the firstName and lastName the user had entered in the first form. We would, in the second form, disable and pre-populate the firstName and lastName inputs; now let’s say we want to propagate the form’s value to some state store when the form value or status changes but when the form is valid.
Let’s consider a case where we have a listener to the form value changes and we have some controls whose values are changed by an external source.
this.form.valueChanges .pipe( distinctUntilChanged() ) .subscribe( (status) => { if (this.form.status === 'VALID') {// `this.form.status` is "DISABLED" // set some state value } } ); this.lastName.setValue('Baz');
We set an observer to the form’s value changes and after we change the value of the lastName control.
Our form’s value will not sync with our application state store because when the observer executes, it will find that the form’s status is DISABLED even though the form value is valid which might cause confusion and discrepancies in our code especially when the state store has observers to it also.
That could be an issue with this mechanism of disabling form controls.
Another way to disable form controls is the one we use to disable the firstName control when instantiating the form itself.
this.form = this.formBuilder.group({ firstName: [{ value: 'Foo', disabled: true }, [Validators.required]], lastName: ['Bar'] });
This has the same effect described above when a control is disabled using the .disable()
control method.
Using a template attribute to disable FormControls
Yet another way is to add disabled=true
attribute to HTML in the template like this:
<form [formGroup]="form"> <h1>Angular Reactive Form</h1> <input formControlName="firstName" placeholder="First Name"> <input formControlName="lastName" disabled="true" placeholder="Last Name"> <button (click)="onSubmit()">Submit</button> </form>
Which gives the below warning
This warning is thrown to warn you about the potential errors regarding "changed after checked" that might be thrown in result of using the disabled attribute directive with a reactive form.
What this means is that, if the disabled attribute would expect a dynamic expression like [disabled]="isDisabled"
(resulting in true or false, not just a static true for example) resulting in a binding eligible for being checked during change detection, it would make the binding susceptible to the "changed after checked" error, that is, the expression could be false
when change detection is run and then true
when the change detection verification phase runs.
A deep dive into the ‘ExpressionChangedAfterItHasBeenCheckedError’ error can be found here .
So to avoid the warning, you can use the [attr.*]
binding like this:
<form [formGroup]="form"> <h1>Angular Reactive Form</h1> <input formControlName="firstName" placeholder="First Name"> <input formControlName="lastName" [attr.disabled]="true" placeholder="Last Name"> <button (click)="onSubmit()">Submit</button> </form>
There’s no warning, but it is worth noting that this will add the HTML disabled attribute which cannot be toggled using just true and false (as false also results in a disabled field), null and undefined can be used to enable the field. Another way to enable a field with this attribute with value set to true, false or any truthy value is by removing it from the field.
Both approaches have the same outcome, a form with a status of VALID, which is what we want to offset the limitation that comes with using the first approach described above (Using the FormControl’s disable() instance method).
However, it is important to note the form’s value does not have the firstName’s control value because it is disabled, this happens when some of the form controls are disabled (Using the FormControl’s disable() instance method or from the form configuration like for the firstName control) and some are enabled or disabled using the [disabled] template directive.
To circumvent this issue, we can use a form’s instance method to get the raw form value that will include values of disabled controls on the form.
this.form.getRawValue()
console.log(this.form.value); console.log(this.form.getRawValue()); // The logs will have
With the second approach (Using a template attribute to disable FormControls) we can expect what we see on the form UI to mirror the value of the form’s instance without affecting the controls instances.
Another way similar to the latter ([attr.disabled]) is to use the readonly attribute.
<form [formGroup]="form"> <h1>Angular Reactive Form</h1> <input formControlName="firstName" placeholder="First Name"> <input formControlName="lastName" readonly="true" placeholder="Last Name"> <button (click)="onSubmit()">Submit</button> </form>
This one has the same effect as using the disabled attribute but does not apply disabled visuals to the control, this will disable the input control but to the eye it will look like the control is enabled.
You can simply style the input like below.
input { margin-bottom: 8px; width: 250px; height: 45px; border-radius: 3px; padding-left: 10px; border: 1px solid rgba(0, 0, 0, 0.4); &:read-only { color: rgb(84, 84, 84); cursor: default; background-color: rgba(239, 239, 239, 0.3); &:focus { outline: none; } } }
Those are the differences I have experienced with the different ways of disabled form controls.
In conclusion
We have seen the different ways we can use to disable form control. They are the following:
- Using the FormControl’s disable() instance method
- Disabling a FormControl from the form configuration
- Using a template attribute to disable FormControls like disable, [attr.disable] and readonly
We have also seen how the first two approaches can affect the form’s status value (Which could possibly affect the application state if it is synced with the form’s state).
Depending on your requirements and use case, it is beneficial to consider beforehand the approach that will not limit your flexibility when working with reactive forms.
No comments:
Post a Comment