Skip to content
lingo

Inputs

Turn any input into a natural-language field.

Prefer the live demos? Open this section in the interactive docs.

The DOM entry turns an input into a headless natural-language field. It ships no styles. It exposes state with ARIA and data attributes, adds a hidden input when name is supplied, canonicalizes on blur/Enter/submit, and supports programmatic set(), commit(), update(), and destroy().

import { lingoInput } from "@pascal-app/lingo/dom"

const field = lingoInput(document.querySelector("#height"), {
  kind: "length",
  unit: "m",
  name: "height_m",
  errorElement: "#height-error",
  hintElement: "#height-hint",
})

field.set("6ft")
field.commit()

The React entry is a small hook over the DOM field contract.

import { useLingoInput } from "@pascal-app/lingo/react"

function HeightField() {
  const field = useLingoInput({
    kind: "length",
    unit: "m",
    name: "height_m",
  })

  return <input ref={field.ref} placeholder="5'11\" or 180cm" />
}

React Native

useLingoTextInput() is a DOM-free hook for React Native TextInput. It returns controlled text handlers, canonical value and submit state, required/bounds issues, hints, and injected completion state without importing react-native.

import { Text, TextInput, View } from "react-native"
import { useLingoTextInput } from "@pascal-app/lingo/react-native"

function WeightField() {
  const field = useLingoTextInput({
    kind: "mass",
    unit: "kg",
    min: 0,
    max: "500 kg",
  })

  return (
    <View>
      <TextInput {...field.inputProps} placeholder="165 lb or 75 kg" />
      {field.errorMessage ? <Text>{field.errorMessage}</Text> : null}
    </View>
  )
}

Caption: Fields never rewrite while typing; commits canonicalize once.

Form demo rules:

  • React hook: One ref carries state, value, and programmatic control.
  • Vanilla controller: The DOM contract stays headless outside React.
  • Native validation: Native validation blocks submit after parser commit.
  • Display modes: Canonical rewrites; echo formats; preserve keeps user text.
  • Constraints: Bounds fail at commit with spans on original text.
  • Range vs single: A rejected range remains available as a candidate.
  • Bidirectional range slider: Text parses to thumbs; pointer and keyboard movement humanizes back to text.
  • Agent and hidden value: Automation sets visible text; FormData submits canonical value.

Deep link: /docs#forms-range-slider opens the bidirectional range slider directly.

The range slider field binds parseRange(text, { kind: "mass", unit: "kg" }) to a two-thumb kg slider (0 to 30 kg, 0.1 kg step). Valid text such as 8-12kg, 8 to 12 kg, around 10kg, 10kg +/-2, or a single 10kg moves the thumbs. Pointer and keyboard changes render humanized range text, but a focused input is never rewritten.

Web component

defineLingoInput() from @pascal-app/lingo/element registers a form-associated <lingo-input> custom element wrapping the same field through ElementInternals instead of a hidden input. FormData, native labels, and :invalid behave like any control in Vue, Svelte, Angular, or plain HTML.

DOM options documented by the public API include kind, unit, displayUnit, display, strictness, accept, tolerance, escalate, min, max, required, name, errorElement, hintElement, formatCandidate, validationBehavior, messages, and debounce.