With Angular, simple forms may use [(ngModel)]
and the Angular FormsModule.
But any serious form implementing severe business logic will need to pick up Angular Reactive Forms from ReactiveFormsModule
or parts of it.
Also with ReactiveFormsModule
it is sometimes necessary to disable an input depending on other other input values state.
For example: the value of a "FormControl" with name status
shall be selectable only after a "FormControl" name
has been supplied.
The traditional approach is to monitor form value changes in the component code an set form control status
to be enabled or disabled depending on value and state of form control name
in TS-code of the component.
You may ignore all this and define HTML template code as:
<form [formGroup]="form">
<input formControlName="name" required>
<input formControlName="status" [disabled]="!form.control.name.valid">
</form>
This is what we would expect, right? But if we do so, the Angular will warn us:
It looks like you're using the disabled attribute with a reactive form directive. ...
To make a long story short: It will not work. Template properties are running out of sync with component based form properties.
Traditional solution is to switch between enabled and disabled in component code using form.controls.status.enable()
or form.controls.status.disable()
.
This is - especially on larger forms - a real effort.
"disabledState"-Directive helps out
In general we aim to make templates as explicit as possible and try to avoid complex component code to steer simple things, like e.g. switching form control state between enabled and disabled.
import { Directive, effect, inject, input, OnInit } from '@angular/core';
import { NgControl } from '@angular/forms';
/**
* Directive for Angular reactive forms steering the disabled state of a form control
* based on boolean signal input. The formControl name can be extracted from sibling formControlName directive.
*
* Usage:
* <input formControlName="fieldName" [disabledState]="isDisabled">
*
* Where isDisabled is a boolean signal that controls the disabled state of the form control.
*/
@Directive({
selector: '[disabledState]',
standalone: true
})
export class DisabledStateDirective implements OnInit {
/**
* Boolean signal input that determines whether the form control should be disabled
*/
disabledState = input.required<boolean>();
/**
* Injected NgControl to access the form control
*/
private ngControl = inject(NgControl, { optional: true });
/**
* Effect to watch for changes in the disabled state signal
*/
private disabledEffect = effect(() => {
if (this.ngControl?.control) {
const shouldDisable = this.disabledState();
if (shouldDisable && this.ngControl.control.enabled) {
this.ngControl.control.disable();
} else if (!shouldDisable && this.ngControl.control.disabled) {
this.ngControl.control.enable();
}
}
});
ngOnInit(): void {
if (!this.ngControl) {
console.warn('DisabledStateDirective: No NgControl found. Make sure this directive is used with formControlName or formControl.');
}
}
}
With disabledState
-Directive the HTML template will look like this:
<form [formGroup]="form">
<input formControlName="name" required>
<input formControlName="status" [disabledState]="!form.control.name.valid">
</form>
Benefits:
- no further warnings
- just working
"disabledState"-Directive explanation and documentation
DisabledStateDirective
A directive for Angular reactive forms that controls the disabled state of a form control based on a boolean signal input.
Features
- Signal-based: Uses Angular signals for reactive state management
- Automatic detection: Automatically finds and controls the associated FormControl
- Type-safe: Written in TypeScript with proper type definitions
- Standalone: Can be used as a standalone directive
- Performance optimized: Uses Angular effects for efficient change detection
Usage
Basic Usage
import { Component, signal } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { DisabledStateDirective } from './path/to/disabled-state.directive';
@Component({
selector: 'app-example',
standalone: true,
imports: [ReactiveFormsModule, DisabledStateDirective],
template: `
<form [formGroup]="form">
<input formControlName="fieldName" [disabledState]="isFieldDisabled">
<button (click)="toggleDisabled()">Toggle Disabled</button>
</form>`
})
export class ExampleComponent {
isFieldDisabled = signal(false);
form = this.fb.group({
fieldName: ['initial value']
});
constructor(private fb: FormBuilder) {}
toggleDisabled() {
this.isFieldDisabled.update(current => !current);
}
}
With Material Form Fields
<mat-form-field>
<mat-label>Username</mat-label>
<input matInput formControlName="username" [disabledState]="isUsernameDisabled">
</mat-form-field>
Conditional Disabling
// Disable field based on computed signal
isFieldDisabled = computed(() => this.userRole() === 'readonly' || this.isLoading());
API
Inputs
Inputs
- Name: disabledState
- Type:
boolean
(signal) - Description: Controls whether the form control should be disabled
Requirements
- Must be used with
formControlName
orformControl
directive - The boolean input must be a signal
How it Works
- The directive injects the
NgControl
from the same element - Uses Angular's
effect()
to watch for changes in thedisabledState
signal - Automatically calls
enable()
ordisable()
on the form control when the signal changes - Provides warning if no
NgControl
is found
Benefits
- Declarative: Control disabled state directly in the template
- Reactive: Automatically responds to signal changes
- Clean: Keeps form control logic out of component code
- Reusable: Works with any form control type
- Performance: Only updates when the signal actually changes\n\n## Error Handling\n\nThe directive will log a warning to the console if it cannot find an associated
NgControl
. This helps with debugging during development.
Browser Support
Requires Angular 17+ for signal support.
Thanks for reading.