Support Directive Composition (`hostDirectives`) for `FormField`
- 主要語言
- TypeScript
- 星號
- 101k
- 分支
- 27.5k
- 平均合併
- 1 天 19 小時
- 30 天內合併 PR
- 288
描述
### 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
貢獻指南
研究方向
先執行連結的 StackBlitz 重現並閱讀 src/app/form-field-aria.ts,接著追蹤 FormField 如何透過 hostDirectives 套用。完成的條件是組合後的 FormField 會註冊 input 並提供與直接使用相同的雙向繫結,同時別名繫結能與單一 formFieldAria 指令搭配運作。
由索引模型根據 Issue 內容生成。
評估
- 技術堆疊
- angular, typescript
- 領域
- frontend
- Issue 類型
- 功能
- 難度
- 4/5
- 預估耗時
- 3-5 天
- 活躍度
- 活躍
- 描述清晰度
- 基本清楚
- 新手友好度
- 48/100