Reading input
Read typed dates, numbers and times back with the same grammar that writes them.
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#
classify 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.
- status
- valid
- value
- 1234.5
- number
- 1234.5
- rejection
- (none)
// 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> );}valueis the canonical text form (plain dot decimal);numberis itsnumber, ornull.Typed or pasted. While typing (
mode: 'typing', the default) a grouping separator is refused (grouping-):while- typing 1.234in Dutch could be a slip. The same text is valid when pasted (mode: 'paste'). Try1.234,56with both settings above.A refused text names its
rejection, for examplecross-(a separator from another notation),convention grouping-orposition precision-.exceeded formatwrites a canonical value back as editable text, andNumber For Input(value, presentation, decimals?) getshows the notation itself.Number Input Placeholder(presentation, decimals?)
Dates#
get: the numeric pattern with a four-digit year, such asDate Input Pattern(presentation) dd-(nl),MM- yyyy dd.(de) orMM. yyyy dd/(en).MM/ yyyy classify:Date Input(text, presentation) validwith the ISO calendar day, or why not.parse: strict; impossible dates and short years areIso Date From Input(text, presentation) null.format: an ISO day as input text.Iso Date For Input(iso Date, presentation)
- status
- valid
- value
- 2028-02-29
- read as
- Dienstag, 29. Februar 2028
// 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#
classify reads a wall-clock time in the presentation's
clock and separator. A valid value is a Local (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
assert checks a constraint set once, for example where a field
is configured, and throws for an inconsistent one.