With fields, you have to have unique IDs to associate inputs and labels as well as errors (the aria-errormessage
prop on the input should be assigned to the ID of the error message element if there is an error, and it should be undefined
if not).
I think conform could definitely manage all this for me. I tried migrating one of my existing forms to conform and I ran into a few things that I think conform should do itself. I added โ next to comments that I want to call out specifically:
import type { DataFunctionArgs } from '@remix-run/node'
import { json } from '@remix-run/node'
import { Link, useFetcher, useNavigate } from '@remix-run/react'
import { useEffect, useRef, useState } from 'react'
import { AuthorizationError } from 'remix-auth'
import { FormStrategy } from 'remix-auth-form'
import { useForm, parse, conform } from '@conform-to/react'
import { formatError, getFieldsetConstraint } from '@conform-to/zod'
import { z } from 'zod'
import { authenticator } from '~/utils/auth.server'
import { commitSession, getSession } from '~/utils/session.server'
import { passwordSchema, usernameSchema } from '~/utils/user-validation'
import { ErrorList, useFocusInvalid } from '~/utils/forms'
export const LoginFormSchema = z.object({
username: usernameSchema,
password: passwordSchema,
remember: z.boolean().optional(),
})
export async function action({ request }: DataFunctionArgs) {
const formData = await request.clone().formData()
const submission = parse(formData)
const result = LoginFormSchema.safeParse(submission.value)
if (!result.success) {
// โ it's uncertain to me whether this handles formErrors and fieldErrors
// or if it's all just flattened...
submission.error.push(...formatError(result.error))
return json({ status: 'invalid-form', submission } as const)
}
if (submission.type !== 'submit') {
return json({ status: 'valid-form', submission } as const)
}
let userId: string | null = null
try {
userId = await authenticator.authenticate(FormStrategy.name, request, {
throwOnError: true,
})
} catch (error) {
if (error instanceof AuthorizationError) {
return json(
{
status: 'auth-error',
errors: {
formErrors: [error.message],
fieldErrors: {},
},
} as const,
{ status: 400 },
)
}
throw error
}
const session = await getSession(request.headers.get('cookie'))
session.set(authenticator.sessionKey, userId)
const { remember } = result.data
const newCookie = await commitSession(session, {
maxAge: remember
? 60 * 60 * 24 * 7 // 7 days
: undefined,
})
return json({ status: 'success', errors: null } as const, {
headers: { 'Set-Cookie': newCookie },
})
}
export function InlineLogin({ redirectTo }: { redirectTo?: string }) {
const loginFetcher = useFetcher<typeof action>()
const navigate = useNavigate()
const formRef = useRef<HTMLFormElement>(null)
const [form, { username, password, remember }] = useForm({
constraint: getFieldsetConstraint(LoginFormSchema),
state:
loginFetcher.data?.status === 'invalid-form'
? loginFetcher.data.submission
: undefined,
})
// โ I don't want to have to set these at all. It should be possible to
// override, but these should just default for me.
const usernameId = 'username'
const usernameErrorId = 'username-error'
const passwordId = 'password'
const passwordErrorId = 'password-error'
const rememberId = 'remember'
const fields = {
username: {
labelProps: { htmlFor: usernameId },
fieldProps: {
id: usernameId,
'aria-errormessage': username.error ? usernameErrorId : undefined,
...conform.input(username.config),
},
// โ I'd really prefer it to be possible to have an array of errors
errorUI: username.error ? (
<ErrorList errors={[username.error]} id={usernameErrorId} />
) : null,
},
password: {
labelProps: { htmlFor: passwordId },
fieldProps: {
id: passwordId,
'aria-errormessage': password.error ? passwordErrorId : undefined,
...conform.input(password.config),
},
errorUI: password.error ? (
<ErrorList errors={[password.error]} id={passwordErrorId} />
) : null,
},
remember: {
labelProps: { htmlFor: rememberId },
fieldProps: {
id: rememberId,
...conform.input(remember.config),
},
errorUI: null,
},
} as const
const formErrorUI = form.error ? (
<ErrorList errors={[form.error]} id="form-error" />
) : null
const success = loginFetcher.data?.status === 'success'
useEffect(() => {
if (!redirectTo) return
if (!success) return
navigate(redirectTo)
}, [success, redirectTo])
// โ doing this to be able to control the noValidate prop
const [hydrated, setHydrated] = useState(false)
useEffect(() => {
setHydrated(true)
}, [])
// โ this would be really nice to have built-in
useFocusInvalid(formRef.current, {
formErrors: [form.error],
fieldErrors: {
username: [username.error],
password: [password.error],
},
})
return (
<div>
<div className="mx-auto w-full max-w-md px-8">
<loginFetcher.Form
method="post"
action="/resources/login"
className="space-y-6"
// โ I have to manually set the aria-errormessage here
aria-errormessage={formErrorUI ? 'form-error' : undefined}
{...form.props}
// โ I have to override the ref ๐ฌ I should be able to provide my own
// ref without messing up conform...
ref={formRef}
// โ I'm passing noValidate here to make sure noValidate only gets set
// to true once we've successfully hydrated
noValidate={hydrated}
>
<div>
<label
className="block text-sm font-medium text-gray-700"
{...fields.username.labelProps}
>
Username
</label>
<div className="mt-1">
<input
autoComplete="username"
className="w-full rounded border border-gray-500 px-2 py-1 text-lg"
{...fields.username.fieldProps}
/>
{/*
โ notice the lack of logic here. I just render it and the logic
above ensures that the errorUI is `null` if there is no error
*/}
{fields.username.errorUI}
</div>
</div>
<div>
<label
className="block text-sm font-medium text-gray-700"
{...fields.password.labelProps}
>
Password
</label>
<div className="mt-1">
<input
autoComplete="current-password"
className="w-full rounded border border-gray-500 px-2 py-1 text-lg"
{...fields.password.fieldProps}
type="password"
/>
{fields.password.errorUI}
</div>
</div>
<div className="flex items-center">
<input
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
{...fields.remember.fieldProps}
// โ I have to manually set the type here. I'd love to either have a
// conform.checkbox or have conform.input be able to handle this automatically
type="checkbox"
/>
<label
className="ml-2 block text-sm text-gray-900"
{...fields.remember.labelProps}
>
Remember me
</label>
</div>
{formErrorUI}
<div className="flex items-center justify-between gap-6">
<button
type="submit"
className="w-full rounded bg-blue-500 py-2 px-4 text-white hover:bg-blue-600 focus:bg-blue-400"
>
Log in
</button>
</div>
</loginFetcher.Form>
<div className="flex justify-around pt-6">
<Link to="/signup" className="text-blue-600 underline">
New here?
</Link>
<Link to="/forgot-password" className="text-blue-600 underline">
Forgot password?
</Link>
</div>
</div>
</div>
)
}
There are several things in this code example that I think could use some cleanup and help from the conform library. My fields
object I think has things that conform could do for me.