Skip to content

Concepts

Every node in a Kin Form tree — a leaf input, a nested object, a nested array, or the form itself — is the same class, FieldApi:

BaseApi → FieldApi<TValue, TParentValue> → FormApi<TValue>
ClassDescription
BaseApisubscribe / notify — the pub/sub primitive everything else builds on
FieldApi<TValue, TParentValue>value / error / touched / validating / dirty / validators, handleBlur / handleChange, child registry, array helpers
FormApi<TValue>root: parent=null, name="", + submit/reset

This mirrors the DOM's own EventTarget → Node → Document shape: one node type with optional children, not a separate class per leaf/container. Whether an object/array-valued field is treated as one atomic leaf (bind handleChange straight to a custom control) or decomposed into children (call field() per sub-path) is entirely up to you — nothing in the engine forces either. FormApi is just the FieldApi at the tree's root, with parent null and name "", plus submitting/reset/resetField/handleSubmit added on top.

Shared state

Every node — leaf, nested field, or form — exposes:

PropertyTypeMeaning
valueTValueThe node's current value. Setting it bubbles up into the parent's value; setting it on a node with registered children cascades down to each of them.
errorstring | nullThe result of the most recently settled validation run. null if valid.
schemaErrorstring | nullThis node's own slice of its parent's schemaErrorMap map, if any.
schemaErrorMapPartial<Record<...>> | nullA flat, dot-joined path -> message map produced by this node's own schemaValidator, if any.
A child reads its slice via its own schemaError.
invalidbooleantrue if error or schemaError is set. Also true if any registered child is invalid, or this node's own schemaErrorMap is non-empty.
touchedbooleanWhether the user has blurred this node. Also true if any registered child is touched.
validatingbooleanWhether a validation run is currently pending/in-flight, for this node or any registered child.
dirtybooleanWhether value differs from the reset baseline — see Dirty Tracking & Reset.
namestringThis node's path relative to its parent ("" at the form root).
idnumberA stable identity for this instance, independent of name — survives array re-keying.

Every node also has handleBlur/handleChange, convenience handlers for binding to a single control — meaningful whether that node is a leaf input or a whole nested object/array edited atomically as one control (see Nested Objects).

Getting a field

field is called relative to the node it's called on, not the whole form, and is idempotent — calling it again with the same name returns the same instance, applying the given options instead of creating a new one:

ts
const form = new FormApi({
  initialValue: { email: "", address: { line1: "" }, items: [{ code: "" }] },
});

const email = form.field("email");
const address = form.field("address");
const line1 = address.field("line1");
const firstItemCode = form.field("items.0.code");

It's fully typed against the form's value: DeepKey<T> computes every dot-joined path into T (objects and arrays alike) as a literal string union, and DeepValue<T, Key> resolves the value type at that path. A typo'd path is a compile error, not a silent undefined at runtime.

What's next

Released under the MIT License.
Copyright © 2026-present Man Hoang