This is the interactive counterpart to our talk at Remix Conf 2022. Get the full slides.
Type coercions
But to make our server validations work, we'll need to coerce our FormData into the correct types. Let's use z.preprocess for that.
92
lines of code
import { Form } from '@remix-run/react'
import { ActionFunction, redirect, json } from '@remix-run/node'
import Label from '~/ui/label'
import Input from '~/ui/input'
import Select from '~/ui/select'
import TextArea from '~/ui/text-area'
import Button from '~/ui/button'
import { useActionData } from '@remix-run/react'
import { z } from 'zod'
function parseDate(value: unknown) {
const [year, month, day] = String(value).split('-').map(Number)
return new Date(year, month - 1, day)
}
const reservationSchema = z.object({
city: z.enum(['saltLakeCity', 'lasVegas', 'losAngeles']),
checkIn: z.preprocess(parseDate, z.date()),
checkOut: z.preprocess(parseDate, z.date()),
adults: z.preprocess(Number, z.number().int().positive()),
children: z.preprocess(Number, z.number().int()),
bedrooms: z.preprocess(Number, z.number().int().positive()),
specialRequests: z.string().optional(),
})
async function makeReservation(values: z.infer<typeof reservationSchema>) {
// Here you would store data instead
console.log(values)
}
type ActionData = { errors: z.ZodIssue[] }
export const action: ActionFunction = async ({ request }) => {
const formValues = Object.fromEntries(await request.formData())
const result = reservationSchema.safeParse(formValues)
if (result.success) {
await makeReservation(result.data)
return redirect('conf/success/03')
}
return json<ActionData>({ errors: result.error.issues })
}
function renderError(name: string) {
const errors = useActionData<ActionData>()?.errors
const message = errors?.find(({ path }) => path[0] === name)?.message
if (!message) return null
return <div className="mt-1 text-red-500">{message}</div>
}
export default function Component() {
return (
<Form method="post" className="flex flex-col space-y-4">
<div>
<Label htmlFor="city">City</Label>
<Select name="city" id="city">
<option value="saltLakeCity">Salt Lake City</option>
<option value="lasVegas">Las Vegas</option>
<option value="losAngeles">Los Angeles</option>
</Select>
<FieldError name="city" />
</div>
<div className="flex w-full space-x-4">
<div className="flex-1">
<Label htmlFor="checkIn">Check In</Label>
<Input name="checkIn" id="checkIn" type="date" />
<FieldError name="checkIn" />
</div>
<div className="flex-1">
<Label htmlFor="checkOut">Check Out</Label>
<Input name="checkOut" id="checkOut" type="date" />
<FieldError name="checkOut" />
</div>
</div>
<div className="flex w-full space-x-4">
<div className="flex-1">
<Label htmlFor="adults">Adults</Label>
<Input name="adults" id="adults" />
<FieldError name="adults" />
</div>
<div className="flex-1">
<Label htmlFor="children">Children</Label>
<Input name="children" id="children" />
<FieldError name="children" />
</div>
<div className="flex-1">
<Label htmlFor="bedrooms">Bedrooms</Label>
<Input name="bedrooms" id="bedrooms" />
<FieldError name="bedrooms" />
</div>
</div>
<div>
<Label htmlFor="specialRequests">Special Requests</Label>
<TextArea name="specialRequests" id="specialRequests" />
<FieldError name="specialRequests" />
</div>
<Button>Make reservation</Button>
</Form>
)
}
This is the interactive counterpart to our talk at Remix Conf 2022. Get the full slides.
Type coercions
But to make our server validations work, we'll need to coerce our FormData into the correct types. Let's use z.preprocess for that.
92
lines of code
import { Form } from '@remix-run/react'
import { ActionFunction, redirect, json } from '@remix-run/node'
import Label from '~/ui/label'
import Input from '~/ui/input'
import Select from '~/ui/select'
import TextArea from '~/ui/text-area'
import Button from '~/ui/button'
import { useActionData } from '@remix-run/react'
import { z } from 'zod'
function parseDate(value: unknown) {
const [year, month, day] = String(value).split('-').map(Number)
return new Date(year, month - 1, day)
}
const reservationSchema = z.object({
city: z.enum(['saltLakeCity', 'lasVegas', 'losAngeles']),
checkIn: z.preprocess(parseDate, z.date()),
checkOut: z.preprocess(parseDate, z.date()),
adults: z.preprocess(Number, z.number().int().positive()),
children: z.preprocess(Number, z.number().int()),
bedrooms: z.preprocess(Number, z.number().int().positive()),
specialRequests: z.string().optional(),
})
async function makeReservation(values: z.infer<typeof reservationSchema>) {
// Here you would store data instead
console.log(values)
}
type ActionData = { errors: z.ZodIssue[] }
export const action: ActionFunction = async ({ request }) => {
const formValues = Object.fromEntries(await request.formData())
const result = reservationSchema.safeParse(formValues)
if (result.success) {
await makeReservation(result.data)
return redirect('conf/success/03')
}
return json<ActionData>({ errors: result.error.issues })
}
function renderError(name: string) {
const errors = useActionData<ActionData>()?.errors
const message = errors?.find(({ path }) => path[0] === name)?.message
if (!message) return null
return <div className="mt-1 text-red-500">{message}</div>
}
export default function Component() {
return (
<Form method="post" className="flex flex-col space-y-4">
<div>
<Label htmlFor="city">City</Label>
<Select name="city" id="city">
<option value="saltLakeCity">Salt Lake City</option>
<option value="lasVegas">Las Vegas</option>
<option value="losAngeles">Los Angeles</option>
</Select>
<FieldError name="city" />
</div>
<div className="flex w-full space-x-4">
<div className="flex-1">
<Label htmlFor="checkIn">Check In</Label>
<Input name="checkIn" id="checkIn" type="date" />
<FieldError name="checkIn" />
</div>
<div className="flex-1">
<Label htmlFor="checkOut">Check Out</Label>
<Input name="checkOut" id="checkOut" type="date" />
<FieldError name="checkOut" />
</div>
</div>
<div className="flex w-full space-x-4">
<div className="flex-1">
<Label htmlFor="adults">Adults</Label>
<Input name="adults" id="adults" />
<FieldError name="adults" />
</div>
<div className="flex-1">
<Label htmlFor="children">Children</Label>
<Input name="children" id="children" />
<FieldError name="children" />
</div>
<div className="flex-1">
<Label htmlFor="bedrooms">Bedrooms</Label>
<Input name="bedrooms" id="bedrooms" />
<FieldError name="bedrooms" />
</div>
</div>
<div>
<Label htmlFor="specialRequests">Special Requests</Label>
<TextArea name="specialRequests" id="specialRequests" />
<FieldError name="specialRequests" />
</div>
<Button>Make reservation</Button>
</Form>
)
}