forty-cdk
llms.txt

Primitives

Input

Attribute directives for single- and multi-line text: a string value() that auto-wires with Signal Forms and reflects every form state (empty, disabled, readonly, invalid …) as data-* / aria-* hooks.

forty-cdk/input

Type in the field and watch the [forInput] host: data-empty and data-touched follow what the user has actually done to it. data-dirty is a reflection of the dirty input, so it appears only when [formField] or a [dirty] binding supplies it, never from this standalone [(value)] binding.

∅ — 0 chars

ForInput and ForTextarea implement Angular's FormValueControl<string> from @angular/forms/signals, so they auto-wire with [formField] and auto-associate inside a [forField] (label, description, and error wiring) with zero extra markup. These are thin wrappers, not re-implementations: the native <input> / <textarea> keeps its own type, caret, IME composition, and native form submission. The directive only bridges the value to a signal and reflects validation state.

When to choose

  • Input / Textarea: the control. [forInput] / [forTextarea] sit on a native <input> / <textarea>, bridge its string value to a signal and reflect validation state; the element keeps its own type, caret and IME behaviour.
  • Field: the wiring around a control (label, description and error region tied to it by id), with no value of its own. It renders nothing and never replaces the input, so the two are used together.
  • When the value is not free text, reach for the control that models it: Number Input, Date Field, Select or Combobox.

Anatomy

<!-- Single-line, two-way bound value -->
<input forInput [(value)]="email" type="email" />

<!-- Multi-line; autosize grows the height to fit content -->
<textarea forTextarea autosize [(value)]="bio"></textarea>

<!-- Auto-associated inside a Field via Signal Forms -->
<div forField>
  <label forLabel>Full name</label>
  <input forInput [formField]="profile.name" />
</div>

Both expose the identical API below; [forTextarea] adds the optional autosize input.

Field composition

Drop the control inside a [forField] and it auto-associates with the label, description, and error region without any id / aria-* wiring by hand.

import { Component, signal } from '@angular/core';
import { form, required } from '@angular/forms/signals';
import { FormField } from '@angular/forms/signals';
import { ForField, ForFieldError, ForLabel } from 'forty-cdk/field';
import { ForInput } from 'forty-cdk/input';

@Component({
  selector: 'demo-signup',
  imports: [ForField, ForLabel, ForFieldError, ForInput, FormField],
  template: `
    <form>
      <div forField>
        <label forLabel>Full name</label>
        <input forInput class="input" [formField]="profile.name" />
        @if (err.shown()) {
          <p forFieldError #err="forFieldError">{{ err.messages().join(', ') }}</p>
        }
      </div>
    </form>
  `,
})
export class DemoSignup {
  readonly model = signal({ name: '' });
  readonly profile = form(this.model, (p) => {
    required(p.name, { message: 'Name is required' });
  });
}

[formField] detects the FormValueControl<string> interface and wires everything (value, disabled, required, invalid, errors, touched) without any glue.

Examples

States

One class and one directive, three states. disabled reflects native disabled plus data-disabled and drops out of submission; readonly keeps the field focusable but blocks edits and reflects data-readonly. The example's stylesheet keys on nothing else.

Auto-sizing textarea

autosize makes the textarea's height track its content: type a few lines and delete them, and it grows and shrinks with each edit. It also recomputes on programmatic value writes and when the width reflows. The directive sets only height, so pair it with resize: none; overflow: hidden keyed off the reflected data-autosize. Cap it with a max-height and overflowing() tells you when the content runs past the cap, on every edit including the ones that leave the box the same size, so a "Read more" toggle can lift the cap. The measurement is browser-only, so it is inert under SSR and hydrates without a layout jump.

Signal Forms validation

Bound through [formField], forInput auto-associates inside forField: the label adopts the control id, errors flow into aria-errormessage, and touched / invalid are reflected with no manual id plumbing. Type an invalid address and blur to surface the error.

API

ForInput

PropertyTypeDescription
value
model
Two-way bindable text value. Defaults to ''; reflected as data-empty while empty.
Default: —
disabled
input
Reflects native disabled + data-disabled (no aria-disabled).
Default: —
readonly
input
Reflects native readonly + aria-readonly="true" + data-readonly.
Default: —
required
input
Reflects aria-required="true".
Default: —
invalid
input
Reflects aria-invalid="true" + data-invalid.
Default: —
pending
input
Reflects aria-busy="true" + data-pending while async validation is in flight.
Default: —
dirty
input
Reflects data-dirty.
Default: —
name
input
Reflected on the native name attribute for form submission.
Default: —
errors
input
Validation errors fed by [formField]. The directive does not render them. That is consumer territory.
Default: —
touched
model
Set to true on blur. Two-way so the field can read it back.
Default: —
Data attributeValues
data-emptypresent (value is '') / absent
data-disabledpresent / absent
data-readonlypresent / absent
data-touchedpresent / absent
data-dirtypresent / absent
data-pendingpresent / absent
data-invalidpresent / absent

ForTextarea

PropertyTypeDescription
value
model
Two-way bindable text value. Defaults to ''; reflected as data-empty while empty.
Default: —
disabled
input
Reflects native disabled + data-disabled (no aria-disabled).
Default: —
readonly
input
Reflects native readonly + aria-readonly="true" + data-readonly.
Default: —
required
input
Reflects aria-required="true".
Default: —
invalid
input
Reflects aria-invalid="true" + data-invalid.
Default: —
pending
input
Reflects aria-busy="true" + data-pending while async validation is in flight.
Default: —
dirty
input
Reflects data-dirty.
Default: —
name
input
Reflected on the native name attribute for form submission.
Default: —
errors
input
Validation errors fed by [formField]. The directive does not render them. That is consumer territory.
Default: —
touched
model
Set to true on blur. Two-way so the field can read it back.
Default: —
autosize
input
Grows/shrinks the height to fit content; reflects data-autosize.
Default: false
overflowing
Signal
Whether the content is taller than the visible box, with or without autosize; a 1px difference counts as fitting. false under SSR. Reflects data-overflowing.
Default: —
Data attributeValues
data-emptypresent (value is '') / absent
data-disabledpresent / absent
data-readonlypresent / absent
data-touchedpresent / absent
data-dirtypresent / absent
data-pendingpresent / absent
data-invalidpresent / absent
data-autosizepresent (autosize on) / absent
data-overflowingpresent (content taller than the box) / absent

Accessibility

  • The native element is the control. It stays the focusable, submittable form field, so screen readers, mobile keyboards (type, inputmode), autofill, and native validation all behave exactly as they would on a bare <input> / <textarea>.
  • No hidden input. Because the visible element carries name and its .value is the form value, the browser serializes it natively, unlike ForSwitch (a <button>) or ForNumberInput (formatted display), which mount a hidden input. A disabled control is skipped by native serialization automatically.
  • Disabled reflects through one channel. The native disabled attribute already exposes the unavailable state through HTML-AAM, so no aria-disabled is emitted alongside it. Style the disabled state with :disabled or [data-disabled].
  • Falsy state styling selects on absence. aria-readonly / aria-required / aria-invalid / aria-busy are emitted only when truthy, so style the off state with :not([aria-invalid]), never [aria-invalid="false"].
  • @angular/forms is an optional peer. If you're not using Signal Forms, don't install it. The directive runs fine on a plain [(value)] binding (the only @angular/forms/signals reference is a type import, erased at build).

Styling

forty-cdk ships no styles: put your own class on each piece and key your CSS off the data-* attributes listed under API, not off the for* selectors (Styling forty-cdk explains why).

[forInput] and [forTextarea] reflect the identical set of attributes on their native host element.

.input[data-invalid] {
  border-color: red;
}

.input[data-empty]::placeholder {
  opacity: 0.5;
}

Wrapping in a design system

Wrapping form primitives documents both supported wrapper patterns: hostDirectives with the exported FOR_INPUT_HOST_DIRECTIVE_INPUTS / FOR_INPUT_HOST_DIRECTIVE_OUTPUTS and FOR_TEXTAREA_HOST_DIRECTIVE_INPUTS / FOR_TEXTAREA_HOST_DIRECTIVE_OUTPUTS name tuples, and subclassing.