Skip to main content

Forms

The @exodra/forms package provides runtime helpers for two-way data binding in forms. Each helper takes a writable bindable and returns a { bindables, handlers } pair that you spread onto an element — the bindables side pushes the model into the DOM, and the handlers side writes user input back with .setValue(...).

These helpers are also the runtime of the bind: directive: when using @exodra/babel-plugin-jsx, the compiler picks the right helper per element from bind:value / bind:checked and folds its result into the element.

npm install @exodra/forms @exodra/reactivity

Exports: bindText, bindNumber, bindChecked, bindSelect, and the TExoBindAttrs type.

The helpers

Each helper returns a TExoBindAttrs — an object with exactly two buckets:

interface TExoBindAttrs {
bindables: Record<string, unknown>;
handlers: Record<string, (event: Event) => void>;
}

function bindText(model: TExoWritableBindable<string>): TExoBindAttrs;
function bindSelect(model: TExoWritableBindable<string>): TExoBindAttrs;
function bindChecked(model: TExoWritableBindable<boolean>): TExoBindAttrs;
function bindNumber(model: TExoWritableBindable<number>): TExoBindAttrs;
Helperbindables keyhandlerreads from eventFor
bindTextvalueonInputtarget.valuetext inputs / textareas
bindSelectvalueonChangetarget.value<select> elements
bindCheckedcheckedonChangetarget.checkedcheckboxes / radios
bindNumbervalueonInputtarget.valueAsNumbernumber / range inputs

For example, bindText(model) returns:

{
bindables: { value: model },
handlers: { onInput: (e) => model.setValue(e.target.value) },
}

The bindable object is passed straight into bindables (never a thunk, never .value). The handler writes back with model.setValue(...). To read the model elsewhere, use model.getValue().

Manual usage

Spread a helper's result onto an h() call. Keep your own attributes in the static bucket:

import { bindText, bindNumber, bindChecked } from '@exodra/forms';
import { h } from '@exodra/core';
import { bindable } from '@exodra/reactivity';

const name = bindable('');
const age = bindable(0);
const agreed = bindable(false);

// Text input
h('input', {
...bindText(name),
static: { placeholder: 'Name' },
});

// Number input
h('input', {
...bindNumber(age),
static: { type: 'number' },
});

// Checkbox
h('input', {
...bindChecked(agreed),
static: { type: 'checkbox' },
});

With the bind: directive (JSX)

With @exodra/babel-plugin-jsx, use the bind: directive and let the compiler choose the helper. All non-directive attributes still go in the typed static bucket — flat React-style props are a compile error in Exodra JSX.

import { bindable } from '@exodra/reactivity';

function Form() {
const name = bindable('');
const email = bindable('');
const age = bindable(0);
const agreed = bindable(false);

return (
<form>
<input bind:value={name} static={{ placeholder: 'Name' }} />
<input bind:value={email} static={{ type: 'email' }} />
<input bind:value={age} static={{ type: 'number' }} />
<input bind:checked={agreed} static={{ type: 'checkbox' }} />

<button static={{ type: 'submit', children: 'Submit' }} />
</form>
);
}

TypeScript

The helpers infer from the bindable's value type:

import { bindText } from '@exodra/forms';
import { bindable } from '@exodra/reactivity';

const name = bindable<string>('');
const boundProps = bindText(name); // Fully typed TExoBindAttrs