Skip to content

Instantly share code, notes, and snippets.

@envynoiz
Last active March 23, 2025 18:54
Show Gist options
  • Save envynoiz/6212c6007fb474ee60b270897a36c496 to your computer and use it in GitHub Desktop.
Save envynoiz/6212c6007fb474ee60b270897a36c496 to your computer and use it in GitHub Desktop.
MatFormField Readonly Directive
import { BooleanInput, coerceBooleanProperty } from '@angular/cdk/coercion';
import { afterRenderEffect, AfterViewInit, DestroyRef, Directive, inject, input } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatFormField } from '@angular/material/form-field';
import { MatInput } from '@angular/material/input';
import { fromEvent, filter } from 'rxjs';
@Directive({
selector: 'mat-form-field[readonlyField]'
})
export class ReadonlyFieldDirective implements AfterViewInit {
// By default enabled once the directive gets attached to the host element
// but can be handled externally using this input binding of course.
public isReadonly = input<boolean, BooleanInput>(true, {
alias: 'readonlyField',
transform: coerceBooleanProperty
});
private readonly MATERIAL_DISABLED_CLASSNAME = 'mdc-text-field--disabled';
private destroyRef = inject(DestroyRef);
private matFormField = inject(MatFormField);
private readonlyAfterRenderEffect = afterRenderEffect({
write: () => {
const readonlyValue = this.isReadonly();
const textFieldContainerElm = this.matFormField._textField.nativeElement;
textFieldContainerElm.classList.toggle(this.MATERIAL_DISABLED_CLASSNAME, readonlyValue);
this.materialInput.readonly = readonlyValue;
}
});
public get materialInput(): MatInput {
return this.matFormField._control as MatInput;
}
public ngAfterViewInit(): void {
// Not a fancy way to get the native element from matInput instance :)
// --->> this.materialInput['_elementRef'].nativeElement
// Let's try to follow some "good practices" to avoid the previous literal accessor.
const inputElm = this.matFormField._textField.nativeElement.querySelector('[matInput]') as Element;
// Native HTML "readonly" state does not have the same behavior like the "disabled" one.
// Readonly by default preserves the focus behavior and I guess that's one of the reason why Material
// does not handle this. Yes this is intrusive but will avoid the focus in our matInput element.
fromEvent(inputElm, 'focusin')
.pipe(
filter(() => this.isReadonly()),
takeUntilDestroyed(this.destroyRef)
)
.subscribe(() => this.materialInput._focusChanged(false));
}
}
@envynoiz
Copy link
Author

Usage examples:

<!-- Enabled by default after attaching the directive -->
<p>
  <mat-form-field appearance="fill" readonlyField>
    <mat-label>Fill form field</mat-label>
    <input matInput placeholder="Placeholder">
    <mat-icon matSuffix>sentiment_very_satisfied</mat-icon>
    <mat-hint>Hint</mat-hint>
  </mat-form-field>
</p>
<!-- Based on a value given on the input binding -->
<p>
  <mat-form-field appearance="outline" [readonlyField]="readonlyState()">
    <mat-label>Outline form field</mat-label>
    <input matInput placeholder="Placeholder">
    <mat-icon matSuffix>sentiment_very_satisfied</mat-icon>
    <mat-hint>Hint</mat-hint>
  </mat-form-field>
</p>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment