@mobx-sentinel/react
v0.3.4
Published
MobX library for non-intrusive class-based model enhancement. Acting as a sentinel, it provides change detection, reactive validation, and form integration capabilities without contamination.
Maintainers
Readme
mobx-sentinel/react
Utility hooks and standard bindings for React applications using @mobx-sentinel/form.
Hooks
useFormAutoReset(form)
Automatically resets the form when the component mounts and unmounts. This is useful for cleaning up form state when navigating between pages or showing/hiding form components.
import { observer } from "mobx-react-lite";
import { useFormAutoReset } from "@mobx-sentinel/react";
const MyFormComponent = observer(({ model }) => {
const form = Form.get(model);
// Auto-resets form on mount/unmount
useFormAutoReset(form);
return <div>{/* your form fields */}</div>;
});useFormHandler(form, event, handler)
Adds a lifecycle handler to the form with automatic cleanup. The handler is automatically removed when the component unmounts. No need to memoize the handler - it's stored in a ref internally and always uses the latest version.
It's just a wrapper for form.addHandler().
import { observer } from "mobx-react-lite";
import { useFormHandler } from "@mobx-sentinel/react";
const MyFormComponent = observer(({ model, onSuccess }) => {
const form = Form.get(model);
// No need to memoize - the hook handles it
useFormHandler(form, "submit", async () => {
await saveToAPI(model);
return true; // Return true to indicate success
});
useFormHandler(form, "didSubmit", () => {
onSuccess(); // This always uses the latest onSuccess callback
});
return <div>{/* your form fields */}</div>;
});Standard Form Bindings
These pre-built bindings are designed for native HTML form elements (<input>, <select>, <button>, <label>) with accessibility in mind.
All binding methods are called on the form instance and automatically handle:
- Two-way data binding between model and UI
- Change tracking and touched state
- Error state management
- ARIA attributes for accessibility
Setup
To use the convenient bindInput(), bindCheckBox(), etc. methods, import the extension:
import "@mobx-sentinel/react/dist/extension";This extends the Form class with custom bind methods. Without the extension, you can still use the default bind() method with binding classes:
// With extension
form.bindInput("username", config)
// Without extension
form.bind("username", InputBinding, config)Text Input: form.bindInput(fieldName, config)
Binds text, number, or date input fields. Handles intermediate values during typing and auto-finalizes on blur.
String inputs:
import { observer } from "mobx-react-lite";
const MyForm = observer(({ model }) => {
const form = Form.get(model);
return (
<input
type="text"
{...form.bindInput("username", {
getter: () => model.username,
setter: (v) => (model.username = v),
})}
/>
);
});Number inputs:
<input
{...form.bindInput("age", {
type: "number", // Optional: auto-detected from valueAs
valueAs: "number",
getter: () => model.age,
setter: (v) => (model.age = v ?? 0),
})}
/>
// Optional number field
<input
{...form.bindInput("score", {
valueAs: "number",
getter: () => model.score,
setter: (v) => (model.score = v), // null when empty
})}
/>Date inputs:
<input
{...form.bindInput("birthday", {
type: "date", // Optional: auto-detected from valueAs
valueAs: "date",
// Getter must return formatted string
getter: () => model.birthday.toISOString().split("T")[0],
// Setter receives Date object
setter: (v) => (model.birthday = v ?? new Date(0)),
})}
/>
// Also supports: datetime-local, time, week, month
<input
{...form.bindInput("appointmentTime", {
type: "datetime-local",
valueAs: "date",
getter: () => model.time?.toISOString().slice(0, 16) ?? null,
setter: (v) => (model.time = v),
})}
/>Checkbox: form.bindCheckBox(fieldName, config)
Binds checkbox inputs for boolean values.
<input
type="checkbox"
{...form.bindCheckBox("subscribe", {
getter: () => model.subscribe,
setter: (v) => (model.subscribe = v),
})}
/>
// Optional boolean field
<input
type="checkbox"
{...form.bindCheckBox("agreedToTerms", {
getter: () => model.agreedToTerms ?? false,
setter: (v) => (model.agreedToTerms = v),
})}
/>Radio Buttons: form.bindRadioButton(fieldName, config)
Binds radio button groups. The binding function returns a function that takes the value for each button.
enum Role {
ADMIN = "ADMIN",
USER = "USER",
GUEST = "GUEST",
}
const MyForm = observer(({ model }) => {
const form = Form.get(model);
const bindRole = form.bindRadioButton("role", {
getter: () => model.role,
setter: (v) => (model.role = v as Role),
});
return (
<div>
{Object.values(Role).map((role) => (
<label key={role}>
<input {...bindRole(role)} />
{role}
</label>
))}
</div>
);
});
// With custom IDs for each button
<input {...bindRole(Role.ADMIN, { id: "role-admin" })} />
<input {...bindRole(Role.USER, { id: "role-user" })} />Select Box: form.bindSelectBox(fieldName, config)
Binds select elements for single or multiple selection.
Single selection:
<select
{...form.bindSelectBox("country", {
getter: () => model.country.code,
setter: (v) => (model.country = findCountry(v)),
})}
>
{countries.map((c) => (
<option key={c.code} value={c.code}>
{c.name}
</option>
))}
</select>Multiple selection:
<select
{...form.bindSelectBox("tags", {
multiple: true,
getter: () => model.tags.map((t) => t.id),
setter: (codes) => (model.tags = codes.map(findTag)),
})}
>
{availableTags.map((tag) => (
<option key={tag.id} value={tag.id}>
{tag.name}
</option>
))}
</select>Submit Button: form.bindSubmitButton(config?)
Binds submit buttons with automatic state management. The button is automatically disabled when the form is invalid, submitting, or validating.
<button {...form.bindSubmitButton()}>
Submit
</button>
// Shows errors on hover
<button {...form.bindSubmitButton()}>
{form.isSubmitting ? "Saving..." : "Save"}
</button>
// With custom click handler
<button
{...form.bindSubmitButton({
onClick: (e) => console.log("Submit clicked"),
})}
>
Submit
</button>ARIA attributes set:
aria-busy: true when submitting or validatingaria-invalid: true when form is invaliddisabled: true when form cannot be submitted
Label: form.bindLabel(fieldNames, config?)
Binds label elements to form fields with error state display. Can associate with single or multiple fields.
// Single field
<label {...form.bindLabel(["username"])}>
Username
</label>
// Multiple fields (shows first error)
<label {...form.bindLabel(["firstName", "lastName"])}>
Full Name
</label>
// Custom htmlFor
<label {...form.bindLabel(["email"], { htmlFor: "email-input" })}>
Email Address
</label>Extending Event Handlers
All bindings support extending the default event handlers:
<input
{...form.bindInput("email", {
getter: () => model.email,
setter: (v) => (model.email = v),
onChange: (e) => console.log("Changed:", e.currentTarget.value),
onFocus: (e) => console.log("Focused"),
onBlur: (e) => console.log("Blurred"),
})}
/>
<input
type="checkbox"
{...form.bindCheckBox("terms", {
getter: () => model.terms,
setter: (v) => (model.terms = v),
onChange: (e) => console.log("Checked:", e.currentTarget.checked),
})}
/>Custom IDs
By default, bindings use auto-generated field IDs. You can override them:
<input
{...form.bindInput("username", {
id: "custom-username-input",
getter: () => model.username,
setter: (v) => (model.username = v),
})}
/>Error Display
All bindings automatically set ARIA attributes for accessibility:
const MyForm = observer(({ model }) => {
const form = Form.get(model);
return (
<div>
<input {...form.bindInput("email", { /* ... */ })} />
{/* Binding sets aria-invalid and aria-errormessage automatically */}
{/* Optionally display errors manually */}
{form.field("email").isErrorReported && (
<div className="error">
{Array.from(form.field("email").errors).join(", ")}
</div>
)}
</div>
);
});