JavaScript Formify

@lemonadejs/formify · ✓ 8 contract checks · framework-agnostic · zero dependencies

<Formify /> — v6 port of v5’s “pico smart HTML forms” (@lemonadejs/formify).

v5 is NOT a field generator: it is a smart <form> wrapper around YOUR markup. Any child carrying a name attribute participates; Formify collects them into one data object and applies data objects back — including nested names: name=“address[city]” ⇄ { address: { city } }.

v5 → v6 mapping: get()/set() → api.get/api.set, PLUS bind=”${state}”: the whole form as one two-way data object (user edits flow out, external writes flow in) load(url) → api.load(url) — now returns the promise — plus a url prop that loads on mount; onload(data) fires (declared but unused in v5) save(url, cb) → api.save(url, cb) — same wire format as v5 (POST, JSON body, X-Requested-With header) .val() protocol → still honored on custom children; v6 web components are covered through their value property and any inner [name] native inputs

Deliberate fixes over v5 (the v5 code, not its intent):

  • radio groups: the checked value wins regardless of DOM order (v5 let a later unchecked sibling erase it)
  • multiple <select>: runtime selections are read (v5 queried the selected ATTRIBUTE, so user picks were invisible)
  • get() returns NESTED data for bracket names, symmetric with set() (v5 returned flat ‘a[b]’ keys but consumed nested data)
  • bare checkboxes are booleans; value-carrying ones submit their value (” when off) — v5 returned the browser default ‘on’

Validation and submit stay native: required/pattern/etc on the children block submission by constraint validation; a declared onsubmit receives (data, event) with the default prevented.

Example

live
import { html } from 'lemonadejs';
import Formify from '@lemonadejs/formify';

const App = (props, { state }) => {
    const order = state({ name: 'Maria Silva', plan: 'team', address: { city: 'Lisbon' } });
    const sent = state('');

    return html`<div style="max-width:420px">
        <${Formify} bind="${order}" onsubmit="${(data) => (sent.value = JSON.stringify(data))}">
            <div class="lm-formify-row">
                <div class="lm-formify-group"><label>Name</label><input type="text" name="name" required /></div>
                <div class="lm-formify-group"><label>City</label><input type="text" name="address[city]" /></div>
            </div>
            <div class="lm-formify-group"><label>Plan</label>
                <select name="plan">
                    <option value="solo">Solo</option>
                    <option value="team">Team</option>
                    <option value="enterprise">Enterprise</option>
                </select>
            </div>
            <label class="lm-formify-choice"><input type="checkbox" name="invoice" /> Send a monthly invoice</label>
            <button type="submit">Place order</button>
        </${Formify}>
        <pre style="font-size:12px">${() => JSON.stringify(order.value, null, 2)}</pre>
        ${() => (sent.value ? html`<p>Submitted: <code>${sent.value}</code></p>` : '')}
    </div>`;
};

Installation

npm install @lemonadejs/formify
import Formify from '@lemonadejs/formify';
import '@lemonadejs/formify/style.css';

Three deployment forms, one component:

html`<${Formify} />`                       // by value (no registration)
setComponents({ Formify });               // then <Formify /> by name anywhere
createWebComponent(Formify);              // <lm-formify> in plain HTML/any framework

Props

Every declared prop arrives as a live state — pass a value for a snapshot or a state for a two-way live wire. Attribute strings are coerced to the declared type.

PropTypeDefaultDescription
bindobjectTwo-way bound value. .set() fires onchange; plain assignment is silent. the whole form as one two-way data object (v5: get/set only)
urlstring''remote JSON applied to the form on mount (v5: load())

Events

All event names are lowercase (the platform convention — LJS-305 warns otherwise).

  • onchange — (data, previous) after any user edit
  • onsubmit — (data, event) — declaring it intercepts native submit
  • onload — (data) after url / api.load() data lands

API

import { ref } from 'lemonadejs';
const formify = ref();
html`<${Formify} ref="${formify}" />`;
// formify.current.get(...)  ·  formify.current.set(...)  ·  formify.current.load(...)  ·  formify.current.save(...)
  • get()
  • set()
  • load()
  • save()

Styling

All classes follow the lm-formify-* convention; visual variants are data-* attributes on the root. Override freely — there is no styling engine to fight.

Contract

The machine-readable schema ships with the package:

import contract from '@lemonadejs/formify/contract.json';

verify.json carries the conformance proof produced by verify(Formify).

Looking for the v5 plugin? See the archived v5 documentation.