---
title: "Forms and Form State in LemonadeJS"
description: "The form() helper: a typed tree of states mirroring your data shape, where the states ARE the storage, plus the Formify block for uncontrolled markup."
source: https://lemonadejs.com/docs/forms/
---

# Forms and form state

A form is a data shape with inputs attached. `form()` takes the shape and
returns a typed tree of states, ready for [bind](/docs/two-way-binding/),
with whole-form get and set:

```javascript
import { form } from 'lemonadejs';

const f = form({ name: '', address: { city: '' }, age: 0 });

html`<input bind="${f.name}" />
     <input bind="${f.address.city}" />
     <input type="number" bind="${f.age}" />`

f.$get();                    // { name, address: { city }, age }, plain data
f.$set({ name: 'Ana' });     // partial apply, nested supported
```

`form()` builds the state tree; the inputs bind to it; `$get()` and `$set()` move whole objects in and out.

<!--example-->

```js
import { html, form } from 'lemonadejs';

const f = form({ name: '', address: { city: '', zip: '' }, age: 0 });

const App = (props, { state }) => {
    const saved = state('');
    const submit = (e) => {
        e.preventDefault();
        saved.value = JSON.stringify(f.$get());               // plain data, nested
    };
    const load = () => f.$set({ name: 'Ana', address: { city: 'Lisbon' } });   // partial, nested

    return html`<form onsubmit="${submit}">
        <input bind="${f.name}" placeholder="name" required />
        <input bind="${f.address.city}" placeholder="city" />
        <input bind="${f.address.zip}" placeholder="zip" pattern="[0-9-]+" />
        <input type="number" bind="${f.age}" min="0" placeholder="age" style="width:5em" />
        <button>Save</button> <button type="button" onclick="${load}">$set() a server response</button>
        <p>live: <b>${f.name}</b> in <b>${f.address.city}</b> · saved: <code>${saved}</code></p>
    </form>`;
};
```

## State-tree model

`form()` builds the state tree from the shape, and then **the states are
the storage**. There is no backing values object to synchronize, because
there is nothing else holding the data:

```javascript
const f = form({
    name: '',
    address: { city: '', zip: '' },    // nested objects → nested groups, recursively
    tags: [] as string[],              // arrays (and other values) are leaf states
});

f.address.city;                        // State<string>, an ordinary state
```

- **`$get()`** reconstructs the plain object from the live states on
  demand (via `peek()`; reading a form never creates subscriptions).
  Typing in a bound input wrote the state; there is no second copy that
  could disagree.
- **`$set(values)`** applies a data object, **partial at every level**
  (`DeepPartial<T>`): `f.$set({ address: { city: 'Lisbon' } })` touches one
  state and nothing else. Unknown keys are ignored, not thrown, so a fat
  server response applies safely.
- **Fields are ordinary states.** Render them live (`${f.name}`),
  `subscribe()` to one field, feed one to [computed()](/docs/computed/) for
  a derived summary, pass one to a component prop. No `watch`, no
  `valueChanges`, no field-registration step.
- **The platform does validation.** `required`, `min`, `pattern`, `type`
  work on the bound inputs natively; number inputs round-trip real numbers
  through `bind` (`null` when empty), so `f.age` is a `State<number>`,
  never `"42"`.

No per-keystroke form re-render exists because no form render exists:
typing updates one state, which updates exactly the bindings that read it.

## In practice

**A profile form with server round-trip.** The whole lifecycle is three
calls:

```javascript
const f = form({ name: '', address: { city: '', zip: '' }, age: 0 });

const Profile = (props, { onMount }) => {
    onMount(() => {
        fetch('/api/profile').then((r) => r.json()).then((data) => f.$set(data));
    });
    return html`<form onsubmit="${(e) => {
        e.preventDefault();
        fetch('/api/profile', { method: 'POST', body: JSON.stringify(f.$get()) });
    }}">
        <input bind="${f.name}" required />
        <input bind="${f.address.city}" />
        <input bind="${f.address.zip}" pattern="[0-9-]+" />
        <input type="number" bind="${f.age}" min="0" />
        <button>Save</button>
    </form>`;
};
```

The fetch result applies partially and nested; the submit serializes from
the live states; native constraint validation gates the inputs.

**The other flavor: the Formify block.** `form()` assumes *you* render the
fields. The `<Formify />` block is the inverse: a smart `<form>` wrapper
around **uncontrolled markup you do not control**: server-rendered HTML, a
CMS fragment, designer-owned markup. Any child carrying a `name`
participates, including nested bracket names:

```javascript
html`<${Formify} bind="${data}" url="/api/profile"
    onsubmit="${(data, e) => save(data)}">
    <input name="name" required />
    <input name="address[city]" />          <!-- ⇄ { address: { city } } -->
    <select name="country">...</select>
</${Formify}>`
```

It collects every named field into one nested data object and applies data
back. `bind` carries the whole form as a single two-way object (user
edits flow out evented, external writes flow in silently, per the
[protocol](/docs/two-way-binding/)), and the api adds `get`/`set` plus
`load(url)`/`save(url)` server round-trips. Web-component children
participate through their `value` property, so a published Dropdown inside
a Formify just works. Submit stays native: declaring `onsubmit` intercepts
it with `(data, event)` after constraint validation passes.

**Choosing between them:** reach for `form()` when your component renders
the fields and you want a typed state per field; it composes with
`computed`, contracts and everything else in the [state](/docs/state/)
model. Reach for `<Formify />` when the markup already exists or arrives
from elsewhere and you want one data object in and out. They meet in the
middle: both speak `bind`, both produce plain nested data.

## Reference

```javascript
import { form } from 'lemonadejs';

const f = form(shape);          // Form<T>, states mirroring the shape
// nested plain objects → nested form groups (recursively)
// arrays and everything else → leaf State<T[K]>

f.field                         // State: bind it, subscribe it, render it
f.group.field                   // nested groups are Form<...> with $get/$set
f.$get()                        // T: plain data, reconstructed via peek()
f.$set(values)                  // DeepPartial<T>: nested partial apply,
                                // unknown keys ignored

type DeepPartial<T>             // partial at EVERY level (arrays kept whole)
```

`form()` has no component owner; fields are module-scope states (the same
box as [store()](/docs/store/)), so a form can outlive any one component
and be shared across several. For the wrapper flavor, the Formify contract
is `bind` (the whole form object), `url`, `onchange(data, previous)`,
`onsubmit(data, event)`, `onload(data)`, and
`api: { get, set, load, save }`.