Support Directive Composition (`hostDirectives`) for `FormField`
- Dominant language
- TypeScript
- Stars
- 101k
- Forks
- 27.5k
- Avg merge
- 1d 19h
- Merged PRs (30d)
- 288
Description
### Which @angular/* package(s) are relevant/related to the feature request?
forms
### Description
I'd like to use Angular's [Directive Composition API](https://angular.dev/guide/directives/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:
```ts
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 {
readonly formFieldAria = input.required>();
readonly fieldDescriptionId = input();
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:
```html
```
### 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:
```html
```
This works but is verbose and error-prone, especially in larger forms.
## Minimal Reproduction
[StackBlitz](https://stackblitz.com/edit/signal-forms-directive-composition-demo?file=src%2Fapp%2Fform-field-aria.ts)
### Steps to reproduce
1. Open the StackBlitz link above
2. The `FormFieldAria` directive attempts to use `hostDirectives` with `FormField`
3. The form field binding does not take effect — the input is not registered with the form
## Environment
```text
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
Contributor guide
Research direction
Start by running the linked StackBlitz reproduction and reading src/app/form-field-aria.ts, then trace how FormField is applied through hostDirectives. Done means the composed FormField registers the input and provides the same two-way binding as direct use, while the aliased binding works with the single formFieldAria directive.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- angular, typescript
- Domain
- frontend
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100