Skip to content

Commit

Permalink
feat!: revert report helper (#256)
Browse files Browse the repository at this point in the history
  • Loading branch information
edmundhung authored Aug 6, 2023
1 parent d3488ce commit 4c6c6b9
Show file tree
Hide file tree
Showing 37 changed files with 236 additions and 205 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ A progressive enhancement first form validation library for Remix and React Rout
Here is an example built with Remix:

```tsx
import { useForm, report } from '@conform-to/react';
import { useForm } from '@conform-to/react';
import { parse } from '@conform-to/zod';
import { Form } from '@remix-run/react';
import { json } from '@remix-run/node';
Expand All @@ -36,7 +36,7 @@ export async function action({ request }: ActionArgs) {
const submission = parse(formData, { schema });

if (!submission.value || submission.intent !== 'submit') {
return json(report(submission));
return json(submission);
}

return await authenticate(submission.value);
Expand Down
213 changes: 119 additions & 94 deletions docs/tutorial.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Tutorial

In this tutoiral, we will show you how to enhance a login form with Conform.
In this tutoiral, we will show you how to enhance a contact form with Conform.

<!-- aside -->

Expand All @@ -10,7 +10,7 @@ In this tutoiral, we will show you how to enhance a login form with Conform.
- [Initial setup](#initial-setup)
- [Introducing Conform](#introducing-conform)
- [Setting client validation](#setting-client-validation)
- [Enhancing user experience](#enhancing-user-experience)
- [Making it accessible](#making-it-accessible)

<!-- /aside -->

Expand All @@ -24,19 +24,19 @@ npm install @conform-to/react @conform-to/zod --save

## Initial setup

Let's create a basic login form with Remix.
Let's build a simple contact form with Remix.

```tsx
import { type ActionArgs, json } from '@remix-run/node';
import { Form, useActionData } from '@remix-run/react';
import { z } from 'zod';
import { authenticate } from '~/auth';
import { sendMessage } from '~/message';

const schema = z.object({
email: z
.string({ required_error: 'Email is required' })
.email('Email is invalid'),
password: z.string({ required_error: 'Password is required' }),
message: z.string({ required_error: 'Message is required' }),
});

export async function action({ request }: ActionArgs) {
Expand All @@ -53,10 +53,10 @@ export async function action({ request }: ActionArgs) {
});
}

return await authenticate(result.data);
return await sendMessage(result.data);
}

export default function LoginForm() {
export default function ContactUs() {
const result = useActionData<typeof action>();

return (
Expand All @@ -67,37 +67,30 @@ export default function LoginForm() {
<div>{result?.error.email}</div>
</div>
<div>
<label>Password</label>
<input
type="password"
name="password"
defaultValue={result?.payload.password}
/>
<div>{result?.error.password}</div>
<label>Message</label>
<textarea name="message" defaultValue={result?.payload.message} />
<div>{result?.error.message}</div>
</div>
<button>Login</button>
<button>Send</button>
</Form>
);
}
```

## Introducing Conform

Now, it's time to enhance the login form using Conform.
Now, it's time to enhance it using Conform.

```tsx
import { report, useForm } from '@conform-to/react';
import { useForm } from '@conform-to/react';
import { parse } from '@conform-to/zod';
import { type ActionArgs, json } from '@remix-run/node';
import { Form, useActionData } from '@remix-run/react';
import { z } from 'zod';
import { authenticate } from '~/auth';
import { sendMessage } from '~/message';

const schema = z.object({
email: z
.string({ required_error: 'Email is required' })
.email('Email is invalid'),
password: z.string({ required_error: 'Password is required' }),
// ...
});

export async function action({ request }: ActionArgs) {
Expand All @@ -110,37 +103,44 @@ export async function action({ request }: ActionArgs) {
// 1) if the intent is not `submit`, or
// 2) if there is any error
if (submission.intent !== 'submit' || !submission.value) {
return json(report(submission));
return json(submission);
}

return await authenticate(submission.value);
return await sendMessage(submission.value);
}

export default function LoginForm() {
export default function ContactUs() {
const lastSubmission = useActionData<typeof action>();

// The `useForm` hook will return everything you need to setup a form
// including the error and config of each field
const [form, { email, password }] = useForm({
const [form, fields] = useForm({
// The last submission will be used to report the error and
// served as the default value and initial error of the form
// for progressive enhancement
lastSubmission,

// Validate the field once a `blur` event is triggered
shouldValidate: 'onBlur',
});

return (
<Form method="post" {...form.props}>
<div>
<label>Email</label>
<input type="email" name="email" defaultValue={email.defaultValue} />
<div>{email.error}</div>
<input
type="email"
name="email"
defaultValue={fields.email.defaultValue}
/>
<div>{fields.email.errors}</div>
</div>
<div>
<label>Password</label>
<input type="password" name="password" />
<div>{password.error}</div>
<label>Message</label>
<textarea name="message" defaultValue={fields.message.defaultValue} />
<div>{fields.message.errors}</div>
</div>
<button>Login</button>
<button>Send</button>
</Form>
);
}
Expand All @@ -153,123 +153,148 @@ Conform will trigger a [server validation](./validation.md#server-validation) to
Server validation might some time be too slow for a good user experience. We can also reuse the validation logic on the client for a instant feedback.

```tsx
import { parse, report, useForm } from '@conform-to/react';
import { parse, useForm } from '@conform-to/react';
import { type ActionArgs, json } from '@remix-run/node';
import { Form, useActionData } from '@remix-run/react';
import { authenticate } from '~/auth';
import { sendMessage } from '~/message';

const schema = z.object({
email: z
.string({ required_error: 'Email is required' })
.email('Email is invalid'),
password: z.string({ required_error: 'Password is required' }),
// ...
});

export async function action({ request }: ActionArgs) {
const formData = await request.formData();
const submission = parse(formData, { schema });

if (submission.intent !== 'submit' || !submission.value) {
return json(report(submission));
}

return await authenticate(submission.value);
// ...
}

export default function LoginForm() {
export default function ContactUs() {
const lastSubmission = useActionData<typeof action>();
const [form, { email, password }] = useForm({
const [form, fields] = useForm({
lastSubmission,
shouldValidate: 'onBlur',

// Run the same validation logic on client
onValidate({ formData }) {
return parse(formData, { schema });
},
});

// ...
}
```

## Making it accessible

There is more we need do to make a form accessible. For example:

- Set an `id` for each field and use it as the `for` attribute of the label
- Set an `aria-invalid` attribute of the field to `true` when there is an error
- Set an `id` for the error message and use it as the `aria-describedby` attribute of the field when there is an error

```tsx
import { parse, useForm } from '@conform-to/react';
import { type ActionArgs, json } from '@remix-run/node';
import { Form, useActionData } from '@remix-run/react';
import { sendMessage } from '~/message';

const schema = z.object({
// ...
});

export async function action({ request }: ActionArgs) {
// ...
}

export default function LoginForm() {
const lastSubmission = useActionData<typeof action>();
const [form, fields] = useForm({
// ...
});

return (
<Form method="post" {...form.props}>
<div>
<label>Email</label>
<input type="email" name="email" defaultValue={email.defaultValue} />
<div>{email.error}</div>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
name="email"
defaultValue={fields.email.defaultValue}
aria-invalid={fields.email.errors.length > 0 || undefined}
aria-describedby={
fields.email.errors.length > 0 ? 'email-error' : undefined
}
/>
<div id="email-error">{fields.email.errors}</div>
</div>
<div>
<label>Password</label>
<input type="password" name="password" />
<div>{password.error}</div>
<label htmlFor="message">Message</label>
<textarea
id="message"
name="message"
defaultValue={fields.message.defaultValue}
aria-invalid={fields.message.errors.length > 0 || undefined}
aria-describedby={
fields.message.errors.length > 0 ? 'message-error' : undefined
}
/>
<div id="message-error">{fields.message.error}</div>
</div>
<button>Login</button>
<button>Send</button>
</Form>
);
}
```

## Enhancing user experience

There is more we can do to enhance the user experience. For example:

- Utilize Conform to manage aria attributes for you by setting a form id
- Customize when to trigger the validation with the `shouldValidate` and `shouldRevalidate` options
- Simplify setup using the `conform` helpers which derives all necessary attributes
How about letting Conform manage all these ids for us?

```tsx
import { parse, report, useForm } from '@conform-to/react';
import { conform, useForm } from '@conform-to/react';
import { parse } from '@conform-to/zod';
import { type ActionArgs, json } from '@remix-run/node';
import { Form, useActionData } from '@remix-run/react';
import { authenticate } from '~/auth';
import { useId } from 'react';
import { sendMessage } from '~/message';

const schema = z.object({
email: z
.string({ required_error: 'Email is required' })
.email('Email is invalid'),
password: z.string({ required_error: 'Password is required' }),
// ...
});

export async function action({ request }: ActionArgs) {
const formData = await request.formData();
const submission = parse(formData, { schema });

if (submission.intent !== 'submit' || !submission.value) {
return json(report(submission));
}

return await authenticate(submission.value);
// ...
}

export default function LoginForm() {
const lastSubmission = useActionData<typeof action>();
const [form, { email, password }] = useForm({

// Generate a unique id for the form, or you can pass in your own
// Note: useId() is only available in React 18
const id = useId();
const [form, fields] = useForm({
// Let Conform manage all ids for us
id,

lastSubmission,
shouldValidate: 'onBlur',
onValidate({ formData }) {
return parse(formData, { schema });
},

// Assign an id to the form so Conform can utilize it for aria attributes
id: 'login',

// Validate the field once the `blur` event is dispatched from the input
shouldValidate: 'onBlur',

// Then, revalidate the field as user types
shouldRevalidate: 'onInput',
});

return (
<Form method="post" {...form.props}>
<div>
<label>Email</label>
<label htmlFor={fields.email.id}>Email</label>
{/* This derives attributes required by the input, such as type, name and default value */}
<input {...conform.input(email, { type: 'email' })} />
<div>{email.error}</div>
<input {...conform.input(fields.email, { type: 'email' })} />
<div id={fields.email.errorId}>{fields.email.errors}</div>
</div>
<div>
<label>Password</label>
{/* It also manages id, aria attributes, autoFocus and validation attributes for you! */}
<input {...conform.input(password, { type: 'password' })} />
<div>{password.error}</div>
<label htmlFor={fields.message.id}>Message</label>
{/* It also manages id, aria attributes, autoFocus and validation attributes for us! */}
<textarea {...conform.textarea(fields.message)} />
<div id={fields.message.errorId}>{fields.message.errors}</div>
</div>
<button>Login</button>
<button>Send</button>
</Form>
);
}
Expand Down
Loading

0 comments on commit 4c6c6b9

Please sign in to comment.