Wanna see something cool? Check out Angular Spotify 🎧

Disable a reactive form control using custom directive

Reactive Forms warning

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>

DisabledControlDirective to disable Reactive Form control

Obviously, the input will not be disabled and the UI won’t update accordingly to the boolean flag that you passed into it.

Solution

There are two approaches to handle the above warning.

1. Setup the formControl with disabled state

We followed the warning error message and set the disabled

this.form = this.fb.group({
  name: [{ value: '', disabled: false }],
})

2. Use FormControl.enable() or FormControl.disable()

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"
/>

Create a custom directive

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() or enable() 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

DisabledControlDirective to disable Reactive Form control

Notes

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.

Source code

https://stackblitz.com/edit/angular-disable-reactive-form-control-directive?file=src/app/disabled-control.directive.ts

Thanks Chau Tran for the original Vietnamese version.

Published 12 Dec 2020

Read more

 — Angular Jira Clone Part 08 - Create placeholder loading (like Facebook's cards loading)
 — Get the last items of an array using array.slice()
 — Angular Jira Clone Part 07 - Build a rich text editor
 — How to iterate over objects in TypeScript
 — How to copy an object from the Chrome inspector console as code