angular / angular/angular

Support Directive Composition (`hostDirectives`) for `FormField`

Open
#67,982 8 comments 1 reaction 0 assignees View on GitHub
area: forms gemini-triaged
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.