forty-cdk
llms.txt
Guides

Wrapping form primitives

Design systems built on forty-cdk usually don't expose the raw primitives. They wrap each form control in a styled component with the system's selector and classes. The wrapper must re-expose the primitive's full API by exact public name: the value model (value / checked), the touched model, the touch output, and the shared form-state inputs (disabled, readonly, required, invalid, pending, dirty, name, errors), plus every control-specific member. Any omission fails silently: an unbound name falls back to a native DOM property and [formField] discovery degrades.

Two patterns are supported. Both keep the Signal Forms contract intact, so a wrapper still auto-wires with [formField].

Pattern 1 — hostDirectives with the exported name tuples

Every primitive implementing FormValueControl / FormCheckboxControl exports two as const tuples from the main entry point: FOR_<PRIMITIVE>_HOST_DIRECTIVE_INPUTS and FOR_<PRIMITIVE>_HOST_DIRECTIVE_OUTPUTS. They hold the exact public names of every input (models included) and every output (the models' *Change emitters and the touch output). Spread them into a hostDirectives entry:

import { ChangeDetectionStrategy, Component } from '@angular/core';
import {
  FOR_INPUT_HOST_DIRECTIVE_INPUTS,
  FOR_INPUT_HOST_DIRECTIVE_OUTPUTS,
  ForInput,
} from 'forty-cdk/input';

@Component({
  selector: 'input[myInput]',
  template: '',
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: { class: 'my-input' },
  hostDirectives: [
    {
      directive: ForInput,
      inputs: [...FOR_INPUT_HOST_DIRECTIVE_INPUTS],
      outputs: [...FOR_INPUT_HOST_DIRECTIVE_OUTPUTS],
    },
  ],
})
export class MyInput {}

The wrapper now accepts every ForInput binding by its original name ([(value)], [(touched)], [disabled], (touch), …) and works under [formField] exactly like the bare primitive:

<input myInput [formField]="profile.name" />

An anti-drift spec in the library fails whenever a tuple stops matching the directive's actual inputs/outputs, so the lists stay trustworthy across releases.

Always spread into an inline object literal

Angular resolves hostDirectives statically at compile time. When your app compiles against the published package, the compiler can evaluate the name tuples (their literal types are preserved in the .d.ts), but it cannot evaluate a pre-built { directive, inputs, outputs } object imported from the package, and fails with NG1010: Host directive reference must be a class. That is why forty-cdk ships name tuples instead of ready-made config objects: spread them into an object literal written directly inside the hostDirectives array, as shown above.

Composite primitives keep working

The root directive's providers (its FOR_<PRIMITIVE>_CONTEXT token) come along with the host directive, so the child pieces a consumer projects into the wrapper still resolve their context:

import { ChangeDetectionStrategy, Component } from '@angular/core';
import {
  FOR_LISTBOX_HOST_DIRECTIVE_INPUTS,
  FOR_LISTBOX_HOST_DIRECTIVE_OUTPUTS,
  ForListbox,
} from 'forty-cdk/listbox';

@Component({
  selector: 'ul[myListbox]',
  template: '<ng-content />',
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: { class: 'my-listbox' },
  hostDirectives: [
    {
      directive: ForListbox,
      inputs: [...FOR_LISTBOX_HOST_DIRECTIVE_INPUTS],
      outputs: [...FOR_LISTBOX_HOST_DIRECTIVE_OUTPUTS],
    },
  ],
})
export class MyListbox {}
<ul myListbox [(value)]="picked" ariaLabel="Fruit">
  <li><button type="button" forListboxOption value="apple">Apple</button></li>
</ul>

Exposing only part of the surface

Because hostDirectives is resolved statically, the compiler cannot evaluate computed expressions over the tuples. Calling .filter(...), .map(...), and friends fails with NG1010: Value could not be determined statically. A wrapper that wants to withhold some inputs lists its subset literally instead of spreading:

inputs: ['value', 'disabled', 'touched'],

The withheld names are then no longer bindable from the outside; the wrapper binds the underlying directive itself (e.g. via host or by injecting it). Note that a hand-written subset opts out of the anti-drift guarantee (future API additions won't flow through automatically), so prefer spreading the full tuple unless hiding a member is a hard requirement.

Pattern 2 — subclassing

A subclass with its own decorator inherits the primitive's inputs, outputs, host bindings, and listeners, and stays a FormValueControl / FormCheckboxControl, so [formField] keeps working with no re-exposed names to maintain:

import { Directive } from '@angular/core';
import { ForInput } from 'forty-cdk/input';

@Directive({
  selector: 'input[myInput]',
  host: { class: 'my-input' },
})
export class MyInput extends ForInput {}

Decorator providers are not inherited

Angular inherits the parent's compiled metadata (inputs, outputs, host bindings) through the class hierarchy, but each decorator declares its own providers. A primitive that shares a context token through providers: [{ provide: FOR_X_CONTEXT, useExisting: ForX }] loses that registration in the subclass, so projected child pieces ([forListboxOption], [forSelectTrigger], …) can no longer resolve their context and throw the primitive's orphan error. The subclass must re-provide the token, pointing useExisting at itself:

import { Directive } from '@angular/core';
import { FOR_LISTBOX_CONTEXT, ForListbox } from 'forty-cdk/listbox';

@Directive({
  selector: 'ul[myListbox]',
  host: { class: 'my-listbox' },
  providers: [{ provide: FOR_LISTBOX_CONTEXT, useExisting: MyListbox }],
})
export class MyListbox extends ForListbox {}

Primitives whose root provides a context token (and therefore needs the re-provide): ForCombobox, ForDateField, ForDatePicker, ForDateRangeField, ForDateRangePicker, ForListbox, ForOtpInput, ForRadioGroup, ForSelect, ForSlider, ForTimeField, ForTimeRangeField, ForToggleGroup. The pure leaf controls (ForInput, ForTextarea, ForSwitch, ForToggle, ForNumberInput) declare no providers, so a bare subclass is enough.

A wrapper that also wants inject(ForSelect) to resolve adds { provide: ForSelect, useExisting: MySelect } alongside the context re-provide.

A split context does not change the re-provide

ForSelect, ForCombobox, ForListbox, ForTimePicker and ForRadioGroup split their coordination surface in two: the public FOR_<PRIMITIVE>_CONTEXT an advanced consumer injects, and an internal interface carrying the members only the primitive's own pieces call (the piece-registration protocol, or in Listbox's and TimePicker's case the pointer-highlight channel). That interface is deliberately not exported (#1399, #1524, #1781, #1784). Since #1593 both are typed views of the same token on the same object, the one-line re-provide above is the whole provider set. There is no second provider for a wrapper to name:

import { Directive } from '@angular/core';
import { FOR_SELECT_CONTEXT, ForSelect } from 'forty-cdk/select';

@Directive({
  selector: '[mySelect]',
  exportAs: 'mySelect',
  providers: [{ provide: FOR_SELECT_CONTEXT, useExisting: MySelect }],
})
export class MySelect<T> extends ForSelect<T> {}

useExisting pointing at the root's class (or a subclass of it) is a precondition rather than a style, because the pieces read the token at the internal interface's type. A useValue carrying your own object satisfies the token's declared type and resolves, but has none of the registration protocol behind it; in dev mode the first piece to resolve rejects it with a [forty-cdk/<primitive>]-prefixed error naming this shape (#1669). The same check covers Select's and Combobox's explicit trigger reference ([forSelectTrigger]="root" / [forComboboxTrigger]="root"), which resolves the root without DI.

ForTable is the one exception. It is not a form primitive, but it splits its context the same way and provides an internal registry its own constructor injects, so a subclass without provideForTable(MyTable) fails to construct at all (NG0201). See Wrapping non-form roots.

Indicator parent parts also self-provide a token

A second family of parts provides a self-token so their optional indicator resolves the parent without importing the concrete class. Subclassing one of these parts and projecting its indicator needs the same one-line re-provide, pointing useExisting at the subclass:

import { Directive } from '@angular/core';
import { FOR_SELECT_OPTION, ForSelectOption } from 'forty-cdk/select';

@Directive({
  selector: 'button[mySelectOption]',
  host: { class: 'my-select-option' },
  providers: [{ provide: FOR_SELECT_OPTION, useExisting: MySelectOption }],
})
export class MySelectOption extends ForSelectOption {}
Subclassed partIndicator that resolves itToken to re-provide
ForSelectOption[forSelectIndicator]FOR_SELECT_OPTION
ForComboboxOption[forComboboxIndicator]FOR_COMBOBOX_OPTION
ForListboxOption[forListboxOptionIndicator]FOR_LISTBOX_OPTION
ForCheckbox[forCheckboxIndicator]FOR_CHECKBOX
ForRadio[forRadioIndicator]FOR_RADIO
ForMenuCheckboxItem[forMenuItemIndicator]FOR_MENU_CHECKBOX_ITEM
ForMenuRadioItem[forMenuItemIndicator]FOR_MENU_RADIO_ITEM

ForCheckbox is the one part that is both a leaf form control and an indicator parent: a bare MyCheckbox extends ForCheckbox is enough on its own, and the re-provide is only needed when the wrapper projects [forCheckboxIndicator] into it. The other parts always carry their indicator inside the same wrapper, so re-provide the token whenever you subclass them.

Projected time sources self-provide a bridge token

ForDatePicker builds a date-time picker by querying its projected time control through contentChild(FOR_TIME_VALUE_SOURCE). Both ForTimeField and ForTimePicker satisfy that bridge by providing the token from their own decorator ({ provide: FOR_TIME_VALUE_SOURCE, useExisting: ForTimeField | ForTimePicker }). A subclass wrapper projected into a date-time ForDatePicker must re-provide the token, pointing useExisting at itself, or the bridge finds no time source and the time component is silently dropped:

import { Directive } from '@angular/core';
import { FOR_TIME_VALUE_SOURCE } from 'forty-cdk/core';
import { ForTimeField } from 'forty-cdk/time-field';

@Directive({
  selector: '[myTimeField]',
  exportAs: 'myTimeField',
  providers: [{ provide: FOR_TIME_VALUE_SOURCE, useExisting: MyTimeField }],
})
export class MyTimeField<D> extends ForTimeField<D> {}
Subclassed primitiveBridge that resolves itToken to re-provide
ForTimeFieldForDatePicker's contentChild time bridgeFOR_TIME_VALUE_SOURCE
ForTimePickerForDatePicker's contentChild time bridgeFOR_TIME_VALUE_SOURCE

For ForTimeField this is in addition to the FOR_TIME_FIELD_CONTEXT re-provide from the table above. Its decorator provides both tokens, and a subclass that projects the time field's own segment pieces and feeds a date-time picker re-provides each. A subclass that only feeds the date-picker bridge (no projected child pieces) re-provides FOR_TIME_VALUE_SOURCE alone.

A picker composed with anatomy="field" is found the other way round: the projected ForDateField / ForTimeField injects FOR_DATE_FIELD_HOST / FOR_TIME_FIELD_HOST, which ForDatePicker / ForTimePicker provide from their own decorator. A subclassed picker used in that anatomy re-provides its token the same way, or no field is adopted and opening the picker throws FORCDK-DATE-PICKER-007 / FORCDK-TIME-PICKER-004 in dev mode:

import { Directive } from '@angular/core';
import { FOR_CALENDAR_HOST, FOR_DATE_FIELD_HOST } from 'forty-cdk/core';
import { FOR_DATE_PICKER_CONTEXT, ForDatePicker } from 'forty-cdk/date-picker';

@Directive({
  selector: '[myDatePicker]',
  exportAs: 'myDatePicker',
  providers: [
    { provide: FOR_DATE_PICKER_CONTEXT, useExisting: MyDatePicker },
    { provide: FOR_DATE_FIELD_HOST, useExisting: MyDatePicker },
    { provide: FOR_CALENDAR_HOST, useExisting: MyDatePicker },
  ],
})
export class MyDatePicker<D> extends ForDatePicker<D> {}

The picker finds its calendar through contentChild(FOR_CALENDAR_CONTEXT), the token a ForCalendar subclass re-provides anyway (see Wrapping non-form roots). The other direction is FOR_CALENDAR_HOST, which ForDatePicker and ForDateRangePicker provide so the projected calendar takes the picker's readonly and disabled. A subclassed picker re-provides it as above, in either anatomy, or a read-only picker's calendar accepts picks again.

Choosing a pattern

  • hostDirectives composes without touching the class hierarchy: the wrapper is a component that owns its template and can layer extra structure, and several host directives can stack on one host. Bindings forward by name, so the exported tuples (plus the anti-drift spec behind them) are what keeps the surface complete.
  • Subclassing is the lighter option when the wrapper only adds styling hooks or overrides behavior: nothing to re-expose, but remember the providers caveat above and that the subclass inherits future API additions automatically (including ones your design system may not want to expose).

Exported tuples

PrimitiveInputs tupleOutputs tuple
ForCheckboxFOR_CHECKBOX_HOST_DIRECTIVE_INPUTSFOR_CHECKBOX_HOST_DIRECTIVE_OUTPUTS
ForComboboxFOR_COMBOBOX_HOST_DIRECTIVE_INPUTSFOR_COMBOBOX_HOST_DIRECTIVE_OUTPUTS
ForDateFieldFOR_DATE_FIELD_HOST_DIRECTIVE_INPUTSFOR_DATE_FIELD_HOST_DIRECTIVE_OUTPUTS
ForDatePickerFOR_DATE_PICKER_HOST_DIRECTIVE_INPUTSFOR_DATE_PICKER_HOST_DIRECTIVE_OUTPUTS
ForDateRangeFieldFOR_DATE_RANGE_FIELD_HOST_DIRECTIVE_INPUTSFOR_DATE_RANGE_FIELD_HOST_DIRECTIVE_OUTPUTS
ForDateRangePickerFOR_DATE_RANGE_PICKER_HOST_DIRECTIVE_INPUTSFOR_DATE_RANGE_PICKER_HOST_DIRECTIVE_OUTPUTS
ForInputFOR_INPUT_HOST_DIRECTIVE_INPUTSFOR_INPUT_HOST_DIRECTIVE_OUTPUTS
ForListboxFOR_LISTBOX_HOST_DIRECTIVE_INPUTSFOR_LISTBOX_HOST_DIRECTIVE_OUTPUTS
ForNumberInputFOR_NUMBER_INPUT_HOST_DIRECTIVE_INPUTSFOR_NUMBER_INPUT_HOST_DIRECTIVE_OUTPUTS
ForOtpInputFOR_OTP_INPUT_HOST_DIRECTIVE_INPUTSFOR_OTP_INPUT_HOST_DIRECTIVE_OUTPUTS
ForRadioGroupFOR_RADIO_GROUP_HOST_DIRECTIVE_INPUTSFOR_RADIO_GROUP_HOST_DIRECTIVE_OUTPUTS
ForSelectFOR_SELECT_HOST_DIRECTIVE_INPUTSFOR_SELECT_HOST_DIRECTIVE_OUTPUTS
ForSliderFOR_SLIDER_HOST_DIRECTIVE_INPUTSFOR_SLIDER_HOST_DIRECTIVE_OUTPUTS
ForSwitchFOR_SWITCH_HOST_DIRECTIVE_INPUTSFOR_SWITCH_HOST_DIRECTIVE_OUTPUTS
ForTextareaFOR_TEXTAREA_HOST_DIRECTIVE_INPUTSFOR_TEXTAREA_HOST_DIRECTIVE_OUTPUTS
ForTimeFieldFOR_TIME_FIELD_HOST_DIRECTIVE_INPUTSFOR_TIME_FIELD_HOST_DIRECTIVE_OUTPUTS
ForTimeRangeFieldFOR_TIME_RANGE_FIELD_HOST_DIRECTIVE_INPUTSFOR_TIME_RANGE_FIELD_HOST_DIRECTIVE_OUTPUTS
ForToggleFOR_TOGGLE_HOST_DIRECTIVE_INPUTSFOR_TOGGLE_HOST_DIRECTIVE_OUTPUTS
ForToggleGroupFOR_TOGGLE_GROUP_HOST_DIRECTIVE_INPUTSFOR_TOGGLE_GROUP_HOST_DIRECTIVE_OUTPUTS

Binding a single-valued field to a selection primitive

The selection primitives (ForSelect, ForListbox, ForCombobox) model their value as readonly T[], with single mode keeping the array at length ≤ 1 (the selection value-type contract). That uniform array shape is the FormValueControl<readonly T[]> backing the [formField] directive auto-wires to, and it is deliberately the same for single and multi selection.

Model the form field with the same shape, and bind it directly. A single-select field is a readonly T[] you keep at length ≤ 1. There is no adapter, no wrapper directive, and nothing to re-plumb: [formField] pushes disabled / readonly / required / invalid / errors / touched into the control and routes focus() to the primitive's real focus target, exactly as it does for a multi-select field.

import { Component, signal } from '@angular/core';
import { form, FormField } from '@angular/forms/signals';
import { ForSelect } from 'forty-cdk/select';

@Component({
  selector: 'app-country-picker',
  imports: [ForSelect, FormField],
  template: `<div forSelect [formField]="profile.country">…</div>`,
})
export class CountryPicker {
  // Single-select: the array never exceeds one entry, `[]` means nothing picked.
  private readonly model = signal({ country: [] as readonly string[] });
  protected readonly profile = form(this.model);
}

Read the picked value off the primitive's selected / selectedItem accessor for display, and map to a T | null shape at the edge that needs it (a request payload, a persisted record) rather than in the binding. A FieldTree<T | null> cannot bind to these controls: Angular's template type-checker adds a two-way [value] binding between the field and every directive on the host that owns a value model, so the value types have to line up exactly, in both directions.

Do not reach for a hand-written FieldTree view to bridge the gap. The library shipped one (forSingleValueField, retired in #1579) and it could only be expressed as reflection over @angular/forms/signals internals: Angular exposes no writable-computed primitive, so a two-way mapped value signal means mutating a computed after creation, and FieldTree<readonly T[]> is additionally an array-like of per-element subfield trees that no hand-built view carries. Every one of those bets fails silently on a dependency bump, which is why the shipped answer is to match the shape instead.