Skip to content

Support Directive Composition (hostDirectives) for FormField #67982

@d-koppenhagen

Description

@d-koppenhagen

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

StackBlitz

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

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: formsgemini-triagedLabel noting that an issue has been triaged by gemini

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions