form validation middleware
npm install @oomfware/formsform validation middleware.
``sh`
npm install @oomfware/forms
`tsx
import { asyncContext, createRouter, route } from '@oomfware/fetch-router';
import { form, forms } from '@oomfware/forms';
import { render } from '@oomfware/jsx';
import * as v from 'valibot';
const routes = route({
login: '/login',
});
const router = createRouter({
middleware: [asyncContext()],
});
const loginForm = form(
v.object({
email: v.pipe(v.string(), v.email()),
_password: v.pipe(v.string(), v.minLength(8)),
}),
async (data, issue) => {
const user = await findUserByEmail(data.email);
if (!user || !(await verifyPassword(user, data._password))) {
invalid(issue.email('Invalid email or password'));
}
return { userId: user.id };
},
);
router.map(routes, {
middleware: [forms({ loginForm })],
actions: {
login() {
return render(
$3
use
invalid() to add validation errors after schema validation passes:`ts
import { form, invalid } from '@oomfware/forms';
import * as v from 'valibot';export const registerForm = form(
v.object({
username: v.pipe(v.string(), v.minLength(3)),
email: v.pipe(v.string(), v.email()),
_password: v.pipe(v.string(), v.minLength(8)),
}),
async (data, issue) => {
const issues = [];
if (await isUsernameTaken(data.username)) {
issues.push(issue.username('Username is already taken'));
}
if (await isEmailRegistered(data.email)) {
issues.push(issue.email('Email is already registered'));
}
if (issues.length > 0) {
invalid(...issues);
}
return await createUser(data);
},
);
`$3
`tsx
const profileForm = form(
v.object({
user: v.object({
name: v.string(),
bio: v.optional(v.string()),
}),
socialLinks: v.array(
v.object({
platform: v.string(),
url: v.pipe(v.string(), v.url()),
}),
),
}),
async (data) => {
return await updateProfile(data);
},
);// in template:
{socialLinks.map((_, i) => (
<>
>
))}
`$3
the
.as() method generates appropriate props for different input types:`tsx
// text inputs
// numeric inputs (auto-parsed via n: prefix)
// boolean checkbox (auto-parsed via b: prefix)
// checkbox group (multiple values)
// radio buttons
// select
// hidden fields (for IDs, tokens, etc.)
// file uploads
`$3
`tsx
router.map(routes, {
middleware: [forms({ messageForm })],
messages() {
if (messageForm.result) {
return render(Message sent! ID: {messageForm.result.messageId}
);
} return render(
);
},
});
`$3
use
buttonProps when a form has multiple submit buttons that trigger different actions:`tsx
import { form, forms } from '@oomfware/forms';
import { render } from '@oomfware/jsx';
import * as v from 'valibot';const draftSchema = v.object({
title: v.string(),
content: v.string(),
});
const saveDraft = form(draftSchema, async (data) => {
return await saveDraftToDb(data);
});
const publishPost = form(draftSchema, async (data) => {
return await publishToDb(data);
});
router.map(routes, {
middleware: [forms({ saveDraft, publishPost })],
actions: {
editor() {
return render(
,
);
},
},
});
`$3
fields prefixed with
_ are automatically redacted from form state on validation failure:`tsx
const loginForm = form(
v.object({
email: v.pipe(v.string(), v.email()),
_password: v.pipe(v.string(), v.minLength(8)), // redacted on error
}),
async (data) => {
// data._password is available here
},
);// if validation fails, loginForm.fields._password.value() returns undefined
// this prevents passwords from being echoed back in the form
``