Skip to content

🧘‍♂️ Simple form

The simple way to validate forms in your fullstack app.

src/pages/index.astro
---
import { z } from "zod";
import { createForm } from "simple:form";
const checkout = createForm({
quantity: z.number(),
email: z.string().email(),
allowAlerts: z.boolean(),
});
const result = await Astro.locals.form.getData(checkout);
if (result?.data) {
await myDb.insert(result.data);
// proceed to checkout
}
---
<form method="POST">
<label for="quantity">Quantity</label>
<input id="quantity" {...checkout.inputProps.quantity} />
<label for="email">Email</label>
<input id="email" {...checkout.inputProps.email} />
<label for="allowAlerts">Allow alerts</label>
<input id="allowAlerts" {...checkout.inputProps.allowAlerts} />
</form>

Installation

Simple form is an Astro integration. You can install and configure this via the Astro CLI using astro add:

Terminal window
npm run astro add simple-stack-form

After installing, you’ll need to add a type definition to your environment for editor hints. Add this reference to a new or existing src/env.d.ts file:

env.d.ts
/// <reference types="simple-stack-form/types" />

Create a validated form

Type: createForm(ZodRawShape): { inputProps: Record<string, InputProps>, validator: ZodRawShape }

You can create a simple form with the createForm() function. This lets you specify a validation schema using Zod, where each input corresponds to an object key. Simple form supports string, number, or boolean (checkbox) fields.

import { createForm } from "simple:form";
import z from "zod";
const signupForm = createForm({
name: z.string(),
age: z.number().min(18).optional(),
newsletterOptIn: z.boolean(),
});

createForm() returns both a validator and the inputProps object. inputProps converts each key of your validator to matching HTML props / attributes. The following props are generated today:

  • name - the object key.
  • type - checkbox for booleans, number for numbers, and text for strings.
  • aria-required - true by default, false when .optional() is used. Note aria-required is used to add semantic meaning for screenreaders, but leave room to add a custom error banner.

Our signupForm example generates the following inputProps object:

const signupForm = createForm({
name: z.string(),
age: z.number().min(18).optional(),
newsletterOptIn: z.boolean(),
});
signupForm.inputProps;
/*
name: { name: 'name', type: 'text', 'aria-required': true }
age: { name: 'age', type: 'number', 'aria-required': false }
newsletterOptIn: { name: 'newsletterOptIn', type: 'checkbox', 'aria-required': true }
*/

Handle array values

You may want to submit multiple form values under the same name. This is common for multi-select file inputs, or generated inputs like “add a second contact.”

You can aggregate values under the same name using z.array() in your validator:

import { createForm } from "simple:form";
import z from "zod";
const contact = createForm({
contactNames: z.array(z.string()),
});

Now, all inputs with the name contactNames will be aggregated. This uses FormData.getAll() behind the scenes:

---
import { createForm } from "simple:form";
import z from "zod";
const contact = createForm({
contactNames: z.array(z.string()),
});
const res = await Astro.locals.form.getData(contact);
console.log(res?.data);
// contactNames: ["Ben", "George"]
---
<form method="POST">
<label for="contact-1">Contact 1</label>
<input id="contact-1" {...contact.inputProps.contactNames} />
{res.fieldErrors?.contactNames?.[0]}
<label for="contact-2">Contact 2</label>
<input id="contact-2" {...contact.inputProps.contactNames} />
{res.fieldErrors?.contactNames?.[1]}
</form>

Note that fieldErrors can be retrieved by index. For example, to get parse errors for the second input, use fieldErrors.contactNames[1].

Sanitize User Input

You may need to sanitize user input with rich text content. This is important for any text rendered as HTML to prevent Cross-Site Scripting (XSS) attacks. You can use the sanitize-html library for this:

Terminal window
npm install --save sanitize-html
npm install --save-dev @types/sanitize-html

Next, call sanitize-html from your text validator with a Zod transform():

import sanitizeHtml from "sanitize-html";
const signupForm = createForm({
name: z.string(),
name: z.string().transform((dirty) => sanitizeHtml(dirty)),
age: z.number().min(18).optional(),
newsletterOptIn: z.boolean(),
});

You can find a sanitization example in our Astro playground