---
id: "packages/active-format/input"
title: "Reading input"
description: "Read typed dates, numbers and times back with the same grammar that writes them."
url: "/packages/active-format/input"
locale: "en"
revision: "a0d3489022e083dd"
---

# Reading input

Display and parsing come from one source, so what a field shows and what it accepts cannot drift
apart. Every reader returns a **status** instead of guessing:

| Status | Meaning |
| --- | --- |
| `empty` | nothing typed |
| `incomplete` | a prefix of a valid value: keep typing |
| `invalid` | cannot become a valid value; numbers also say why (`rejection`) |
| `valid` | a value, in its canonical form |

## Numbers

`classifyNumberInput(text, presentation, contract?)` reads a number in the reader's notation. The
contract can limit decimals, refuse negatives, check a currency or allow a percent sign; `mode`
distinguishes typing from pasting.

**Example: Reading a typed number**

Client: Runs entirely in your browser. Source: `examples/format/number-input.tsx`.

```tsx
// apps/docs/examples/format/number-input.tsx
'use client';

import {useId, useState} from 'react';

import {
  classifyNumberInput,
  getNumberInputPlaceholder,
} from '@workspace/active-format';
import {
  DescriptionItem,
  DescriptionList,
} from '@workspace/active-ui/components/composed/description-list';
import {
  Field,
  FieldLabel,
} from '@workspace/active-ui/components/primitives/field';
import {Input} from '@workspace/active-ui/components/primitives/input';
import {
  NativeSelect,
  NativeSelectOption,
} from '@workspace/active-ui/components/primitives/native-select';

/**
 * Reading a typed number back with the same grammar that writes it. The
 * result says whether the text is empty, incomplete, invalid or valid, and
 * why a text is refused. Grouping separators are refused while typing and
 * accepted in pasted text.
 */
export default function NumberInput() {
  const inputId = useId();
  const languageId = useId();
  const modeId = useId();
  const [language, setLanguage] = useState('nl');
  const [mode, setMode] = useState<'typing' | 'paste'>('typing');
  const [text, setText] = useState('1234,5');
  const result = classifyNumberInput(text, language, {mode, maxDecimals: 2});
  return (
    <div className="flex flex-col gap-4">
      <div className="grid gap-3 sm:grid-cols-3">
        <Field>
          <FieldLabel htmlFor={languageId}>Language</FieldLabel>
          <NativeSelect
            id={languageId}
            value={language}
            onChange={(event) => setLanguage(event.target.value)}
          >
            {['nl', 'en', 'de', 'fr'].map((option) => (
              <NativeSelectOption key={option} value={option}>
                {option}
              </NativeSelectOption>
            ))}
          </NativeSelect>
        </Field>
        <Field>
          <FieldLabel htmlFor={modeId}>Input</FieldLabel>
          <NativeSelect
            id={modeId}
            value={mode}
            onChange={(event) =>
              setMode(event.target.value === 'paste' ? 'paste' : 'typing')
            }
          >
            <NativeSelectOption value="typing">Typed</NativeSelectOption>
            <NativeSelectOption value="paste">Pasted</NativeSelectOption>
          </NativeSelect>
        </Field>
        <Field>
          <FieldLabel htmlFor={inputId}>
            Amount (at most two decimals)
          </FieldLabel>
          <Input
            id={inputId}
            value={text}
            placeholder={getNumberInputPlaceholder(language, 2)}
            onChange={(event) => setText(event.target.value)}
            inputMode="decimal"
          />
        </Field>
      </div>
      <DescriptionList orientation="inline" columns={1}>
        <DescriptionItem label="status">
          <span className="font-mono" data-testid="number-status">
            {result.status}
          </span>
        </DescriptionItem>
        <DescriptionItem label="value">
          <span className="font-mono">{result.value || '(none)'}</span>
        </DescriptionItem>
        <DescriptionItem label="number">
          <span className="font-mono">{result.number ?? 'null'}</span>
        </DescriptionItem>
        <DescriptionItem label="rejection">
          <span className="font-mono">{result.rejection ?? '(none)'}</span>
        </DescriptionItem>
      </DescriptionList>
    </div>
  );
}
```

- `value` is the canonical text form (plain dot decimal); `number` is its `number`, or `null`.
- **Typed or pasted.** While typing (`mode: 'typing'`, the default) a grouping separator is
  refused (`grouping-while-typing`): `1.234` in Dutch could be a slip. The same text is valid when
  pasted (`mode: 'paste'`). Try `1.234,56` with both settings above.
- A refused text names its `rejection`, for example `cross-convention` (a separator from another
  notation), `grouping-position` or `precision-exceeded`.
- `formatNumberForInput(value, presentation, decimals?)` writes a canonical value back as editable
  text, and `getNumberInputPlaceholder(presentation, decimals?)` shows the notation itself.

## Dates

- `getDateInputPattern(presentation)`: the numeric pattern with a four-digit year, such as
  `dd-MM-yyyy` (nl), `dd.MM.yyyy` (de) or `dd/MM/yyyy` (en).
- `classifyDateInput(text, presentation)`: `valid` with the ISO calendar day, or why not.
- `parseIsoDateFromInput(text, presentation)`: strict; impossible dates and short years are `null`.
- `formatIsoDateForInput(isoDate, presentation)`: an ISO day as input text.

**Example: Reading a typed date**

Client: Runs entirely in your browser. Source: `examples/format/date-input.tsx`.

```tsx
// apps/docs/examples/format/date-input.tsx
'use client';

import {useId, useState} from 'react';

import {
  classifyDateInput,
  formatDateOnly,
  getDateInputPattern,
} from '@workspace/active-format';
import {
  DescriptionItem,
  DescriptionList,
} from '@workspace/active-ui/components/composed/description-list';
import {
  Field,
  FieldLabel,
} from '@workspace/active-ui/components/primitives/field';
import {Input} from '@workspace/active-ui/components/primitives/input';
import {
  NativeSelect,
  NativeSelectOption,
} from '@workspace/active-ui/components/primitives/native-select';

/**
 * A typed date in the reader's notation, read back as an ISO calendar day.
 * Impossible dates are invalid, never moved to the next valid day.
 */
export default function DateInput() {
  const inputId = useId();
  const languageId = useId();
  const [language, setLanguage] = useState('de');
  const [text, setText] = useState('29.02.2028');
  const pattern = getDateInputPattern(language);
  const result = classifyDateInput(text, language);
  return (
    <div className="flex flex-col gap-4">
      <div className="grid gap-3 sm:grid-cols-2">
        <Field>
          <FieldLabel htmlFor={languageId}>Language</FieldLabel>
          <NativeSelect
            id={languageId}
            value={language}
            onChange={(event) => setLanguage(event.target.value)}
          >
            {['de', 'nl', 'en', 'fr'].map((option) => (
              <NativeSelectOption key={option} value={option}>
                {option}
              </NativeSelectOption>
            ))}
          </NativeSelect>
        </Field>
        <Field>
          <FieldLabel htmlFor={inputId}>Date ({pattern})</FieldLabel>
          <Input
            id={inputId}
            value={text}
            placeholder={pattern}
            onChange={(event) => setText(event.target.value)}
          />
        </Field>
      </div>
      <DescriptionList orientation="inline" columns={1}>
        <DescriptionItem label="status">
          <span className="font-mono" data-testid="date-status">
            {result.status}
          </span>
        </DescriptionItem>
        <DescriptionItem label="value">
          <span className="font-mono">{result.value || '(none)'}</span>
        </DescriptionItem>
        <DescriptionItem label="read as">
          <span suppressHydrationWarning>
            {result.status === 'valid'
              ? formatDateOnly(result.value, 'full', language)
              : '(none)'}
          </span>
        </DescriptionItem>
      </DescriptionList>
    </div>
  );
}
```

Try `29.02.2027`: there is no 29 February in 2027, so the text is invalid. It is never moved to
1 March.

## Times of day

`classifyTimeInput(text, presentation, constraints?)` reads a wall-clock time in the presentation's
clock and separator. A valid value is a `LocalTimeString` (`HH:mm` or `HH:mm:ss`): a clock reading,
never a moment. Constraints set the precision, a step grid and inclusive bounds; a time off the step
grid is `invalid`, never rounded.

Fields of `TimeInputConstraints`:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `precision` | `TimeInputPrecision` | no | Default `minute`. |
| `step` | `TimeInputStep` | no | The allowed grid; without it, every minute (or second) is allowed. |
| `min` | `LocalTimeString` | no | The earliest allowed time, inclusive. |
| `max` | `LocalTimeString` | no | The latest allowed time, inclusive. |

`assertTimeInputConstraints(constraints)` checks a constraint set once, for example where a field
is configured, and throws for an inconsistent one.
