-
Notifications
You must be signed in to change notification settings - Fork 27.1k
Support Directive Composition (hostDirectives) for FormField #67982
Description
Which @angular/* package(s) are relevant/related to the feature request?
forms
Description
I'd like to use Angular's Directive Composition API (hostDirectives) to compose FormField into a custom directive. Since FormField is a standalone directive with a standard InputSignal, this seems like it should be a supported use case — but it currently doesn't work.
Use case
A common pattern when building accessible forms is to create a reusable directive that manages ARIA attributes (aria-invalid, aria-busy, aria-describedby, aria-errormessage) based on the field's validation state. Ideally, this directive would compose FormField via hostDirectives so that consumers only need a single binding per input element instead of two.
Desired behavior
The following directive composes FormField via hostDirectives and forwards its input through an alias:
import { computed, Directive, input } from '@angular/core';
import { FieldTree, FormField } from '@angular/forms/signals';
@Directive({
selector: '[formFieldAria]',
host: {
'[aria-invalid]': 'ariaInvalid()',
'[aria-busy]': 'ariaBusy()',
'[aria-describedby]': 'ariaDescribedBy()',
'[aria-errormessage]': 'ariaErrorMessage()',
},
hostDirectives: [
{ directive: FormField, inputs: ['formField: formFieldAria'] },
],
})
export class FormFieldAria<T> {
readonly formFieldAria = input.required<FieldTree<T>>();
readonly fieldDescriptionId = input<string>();
readonly ariaInvalid = computed(() => {
const state = this.formFieldAria()();
return state.touched() && !state.pending()
? state.errors().length > 0
: undefined;
});
readonly ariaBusy = computed(() => {
const state = this.formFieldAria()();
return state.pending();
});
readonly ariaDescribedBy = computed(() => {
const id = this.fieldDescriptionId();
return !id || this.ariaInvalid() ? null : id;
});
readonly ariaErrorMessage = computed(() => {
const id = this.fieldDescriptionId();
return !id || !this.ariaInvalid() ? null : id;
});
}This would allow a clean single-binding usage in templates:
<input
type="text"
fieldDescriptionId="username-info"
[formFieldAria]="myForm.username"
/>Current behavior
The composition does not work. FormField is not correctly applied to the host element when used via hostDirectives, so the form control registration and two-way binding that FormField normally provides do not take effect.
Current workaround
Both directives must be applied separately, which means duplicating the field binding on every input:
<input
type="text"
fieldDescriptionId="username-info"
[formField]="myForm.username"
[formFieldAria]="myForm.username"
/>This works but is verbose and error-prone, especially in larger forms.
Minimal Reproduction
Steps to reproduce
- Open the StackBlitz link above
- The
FormFieldAriadirective attempts to usehostDirectiveswithFormField - The form field binding does not take effect — the input is not registered with the form
Environment
Angular version: 22.0.0-next.6
Additional context
I understand that Signal Forms is still experimental. FormField is standalone and uses a standard InputSignal, so from the outside it looks like it should be compatible with hostDirectives.
Supporting this would make it much easier to build reusable, accessible form field wrappers — which feels like a natural fit for the Directive Composition API.
Proposed solution
It should be possible to compose the FormField with a custom directive, so I only have to apply one directive and pass the form field input binding.
Alternatives considered
none