When working with Reactive Forms, you might probably have seen the below warning.
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)
});
When using reactive forms control, Angular want you only to interact with form control via its instance on the component class, not on the template side. To trigger this warning, you need to set a disabled
input property on a reactive form control. If you familiar with Template driven form, that’s how we disable a control. Angular is complaining because we are mixing between Reactive Form and Template driven form.
export class ReactiveFormWarningComponent implements OnInit {
disabledName = false
form: FormGroup
constructor(private fb: FormBuilder) {}
ngOnInit() {
this.form = this.fb.group({
name: [''],
})
}
}
<button (click)="disabledName = !disabledName">Toggle name state</button>
<form [formGroup]="form">
<input
class="form-control"
type="text"
formControlName="name"
[disabled]="disabledName"
/>
</form>
Obviously, the input will not be disabled and the UI won’t update accordingly to the boolean flag that you passed into it.
There are two approaches to handle the above warning.
We followed the warning error message and set the disabled
this.form = this.fb.group({
name: [{ value: '', disabled: false }],
})
this.form.get('name').enable()
this.form.get('name').disable()
The two approaches above will work pretty well.
But if you want to write something as below, to disable state of a FormControl on template, we need to find another way around that.
<input
class="form-control"
type="text"
formControlName="name"
[disabled]="disabledName"
/>
Using the custom directive approach is more declarative. You declare how a control will be disabled (by a flag on the template). By calling
disable()
orenable()
based on the value of the flag, it’s imperative because you have to track the changes of the flag and call the methods.
So how to handle that? The answer is to write a Directive.
I will write a DisabledControlDirective. This directive is applying in combination with formControlName
directive or formControl
directive. When you want to disable FormControl
on the template, you use [disableControl]
instead of the built-in [disabled]
.
import { Directive, Input } from '@angular/core'
import { NgControl } from '@angular/forms'
@Directive({
selector: '([formControlName], [formControl])[disabledControl]',
})
export class DisabledControlDirective {
@Input() set disabledControl(state: boolean) {
const action = state ? 'disable' : 'enable'
this.ngControl.control[action]()
}
constructor(private readonly ngControl: NgControl) {}
}
That’s how it looks on the template side.
<form [formGroup]="form">
<input type="text" formControlName="name" [disabledControl]="disabledName" />
</form>
That’s the end result
If a control is disabled, its value will be excluded when accessing FormGroup.value
. If you want to get all the controls value, no matter what their disabled state, please use FormGroup.getRawValue()
instead.
Thanks Chau Tran for the original Vietnamese version.