Created
April 15, 2024 08:15
-
-
Save tudorilisoi/51d89fd3ad7577e2868dafde5a15a001 to your computer and use it in GitHub Desktop.
@conform with zod and zenstack
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// schema | |
import { z } from 'zod' | |
// structure for a blog post | |
export const postCreateSchema = z.object({ | |
title: z | |
.string({ required_error: 'Title is required' }) | |
.min(10, 'Title is too short'), | |
body: z | |
.string({ required_error: 'Body is required' }) | |
.min(10, 'Message is too short') | |
.max(15, 'Message is too long'), | |
}) | |
export const postUpdateSchema = postCreateSchema.merge( | |
z.object({ | |
id: z.string(), | |
}), | |
) | |
// route handler | |
import { | |
getFormProps, | |
getInputProps, | |
getTextareaProps, | |
useForm, | |
} from '@conform-to/react' | |
import { getZodConstraint, parseWithZod } from '@conform-to/zod' | |
import { | |
LoaderFunctionArgs, | |
json, | |
redirect, | |
type ActionFunctionArgs, | |
} from '@remix-run/node' | |
import { Form, useActionData, useLoaderData } from '@remix-run/react' | |
import { getUserId } from '~/services/auth.server' | |
import { createPost, getPost, updatePost } from '~/services/post/post.server' | |
import { postCreateSchema, postUpdateSchema } from '~/services/post/post' | |
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library' | |
export async function loader(args: LoaderFunctionArgs) { | |
const userId = await getUserId(args) | |
if (userId === 'GUEST') { | |
const { request } = args | |
const url = new URL(request.url) | |
const redirectUrl = `/sign-in?redirect_url=${encodeURIComponent( | |
url.pathname + '?cacheBust=' + new Date().getTime(), | |
)}` | |
console.log(`🚀 ~ loader ~ redirectUrl:`, redirectUrl) | |
return redirect(redirectUrl, { status: 302 }) | |
} | |
const { postId } = args.params | |
if (!postId) { | |
return { | |
userId, | |
post: null, | |
postId, | |
} | |
} | |
const post = await getPost({ userId, id: postId }) | |
const data = { userId, post, postId } | |
return json(data) | |
} | |
export async function action(args: ActionFunctionArgs) { | |
const { request } = args | |
const { postId } = args.params | |
const schema = postId ? postUpdateSchema : postCreateSchema | |
const userId = await getUserId(args) | |
const formData = await request.formData() | |
const submission = parseWithZod(formData, { schema }) | |
if (submission.status !== 'success') { | |
return json(submission.reply()) | |
} | |
try { | |
if (postId) { | |
await updatePost({ ...submission.value, userId, id: postId }) | |
} else { | |
await createPost({ ...submission.value, userId }) | |
} | |
return redirect('/zz') | |
} catch (error) { | |
let message = 'Failed to save. Please try again later.' | |
if (error instanceof PrismaClientKnownRequestError) { | |
message = (error.meta?.reason as string) || 'Database error' | |
} | |
console.log(`🚀 ~ action ~ error:`, error) | |
return json( | |
submission.reply({ | |
formErrors: [message], | |
}), | |
) | |
} | |
} | |
export default function UpsertPost() { | |
const data = useLoaderData<typeof loader>() | |
const lastResult = useActionData<typeof action>() | |
let schema: typeof postCreateSchema | typeof postUpdateSchema = | |
postUpdateSchema | |
if (!data.postId) { | |
schema = postCreateSchema | |
} | |
const [form, fields] = useForm({ | |
defaultValue: data.post || {}, | |
lastResult, | |
constraint: getZodConstraint(schema), | |
shouldValidate: 'onBlur', | |
shouldRevalidate: 'onInput', | |
onValidate({ formData }) { | |
return parseWithZod(formData, { schema }) | |
}, | |
}) | |
const labelProps = { | |
className: | |
'block label text-secondary hover:text-accent-focus focus-within:text-primary font-bold mb-1 mt-2', | |
} | |
const errorProps = { | |
className: 'bg-error text-error-content mt-2 p-2 rounded-md block w-fit', | |
} | |
type fieldKeys = keyof typeof fields | |
const getErrorProps = (fieldName: fieldKeys) => { | |
return fields[fieldName].errors ? errorProps : {} | |
} | |
return ( | |
<Form method="post" {...getFormProps(form)}> | |
<div>{form.errors}</div> | |
{!data.postId ? null : ( | |
<input {...getInputProps(fields?.id, { type: 'hidden' })} /> | |
)} | |
<div> | |
<label {...labelProps} htmlFor={fields.title.id}> | |
Title | |
</label> | |
<input {...getInputProps(fields.title, { type: 'text' })} /> | |
<div {...getErrorProps('title')} id={fields.title.errorId}> | |
{fields.title.errors} | |
</div> | |
</div> | |
<div> | |
<label {...labelProps} htmlFor={fields.body.id}> | |
Message | |
</label> | |
<textarea | |
className="textarea textarea-bordered bg-base-200 focus:outline-secondary-focus" | |
{...getTextareaProps(fields.body)} | |
/> | |
<div {...getErrorProps('body')} id={fields.body.errorId}> | |
{fields.body.errors} | |
</div> | |
</div> | |
<button className="btn btn-lg btn-secondary btn-block mt-4">Send</button> | |
</Form> | |
) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment