Skip to content

Commit

Permalink
docs: refine tutorial (#121)
Browse files Browse the repository at this point in the history
  • Loading branch information
edmundhung committed Mar 18, 2023
1 parent 59564a4 commit b2cc882
Show file tree
Hide file tree
Showing 7 changed files with 109 additions and 127 deletions.
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# CONFORM [![latest release](https://img.shields.io/github/v/release/edmundhung/conform?display_name=tag&sort=semver&style=flat-square&labelColor=000&color=2a4233)](https://github.com/edmundhung/conform/releases) [![GitHub license](https://img.shields.io/github/license/edmundhung/conform?style=flat-square&labelColor=000&color=2a4233)](https://github.com/edmundhung/conform/blob/main/LICENSE)

A progressive enhancement first form validation library for [Remix](https://remix.run)
A progressive enhancement first form validation library for Remix and React Router

### Highlights

Expand All @@ -14,13 +14,13 @@ A progressive enhancement first form validation library for [Remix](https://remi

### Quick Start

Here is a real world example built with [Remix](https://remix.run).
Here is an example built with Remix:

```tsx
import { useForm } from '@conform-to/react';
import { parse } from '@conform-to/zod';
import { Form } from '@remix-run/react';
import { json, redirect } from '@remix-run/node';
import { json } from '@remix-run/node';
import { z } from 'zod';
import { authenticate } from '~/auth';

Expand All @@ -34,7 +34,7 @@ export async function action({ request }: ActionArgs) {
const submission = parse(formData, { schema });

if (!submission.value || submission.intent !== 'submit') {
return json(submission);
return json(submission, { status: 400 });
}

return await authenticate(submission.value);
Expand All @@ -53,12 +53,12 @@ export default function LoginForm() {
<Form method="post" {...form.props}>
<div>
<label>Email</label>
<input {...conform.input(email)} />
<input type="email" name={email.name} />
<div>{email.error}</div>
</div>
<div>
<label>Password</label>
<input {...conform.input(password)} />
<input type="password" name={password.name} />
<div>{password.error}</div>
</div>
<button>Login</button>
Expand Down
193 changes: 74 additions & 119 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 build a login form with [Remix](https://remix.run).
In this tutoiral, we will show you how to build a login form.

<!-- aside -->

Expand All @@ -9,8 +9,7 @@ In this tutoiral, we will show you how to build a login form with [Remix](https:
- [Installation](#installation)
- [Initial setup](#initial-setup)
- [Introducing Conform](#introducing-conform)
- [Sharing validation](#sharing-validation)
- [Removing boilerplates](#removing-boilerplates)
- [Further enhancement](#further-enhancement)

<!-- /aside -->

Expand Down Expand Up @@ -40,7 +39,7 @@ export async function action({ request }: ActionArgs) {
// Do some basic validation
const error = {};

if (email) {
if (!email) {
error.email = 'Email is required';
} else if (!email.includes('@')) {
error.email = 'Email is invalid';
Expand Down Expand Up @@ -83,33 +82,31 @@ export default function LoginForm() {
<input type="password" name="password" />
<div>{result.error.password}</div>
</div>
<button type="submit">Login</button>
<button>Login</button>
</Form>
);
}
```

By this point, we have a basic login form working already. It validates the fields on submit and shows errors if failed. Most importantly, it also works without JS!
By this point, we have a basic login form working already. It validates the fields on submit and shows errors if failed. It also works without JS thanks to progressive enhancement!

## Introducing Conform

Then, it's time to introduce the [parse](/packages/conform-react/README.md#parse) helper function and the [useForm](/packages/conform-react/README.md#useform) hook:
Now, it's time to introduce the [parse](/packages/conform-react/README.md#parse) helper function and the [useForm](/packages/conform-react/README.md#useform) hook:

```tsx
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';

export async function action({ request }: ActionArgs) {
const formData = await request.formData();
// Replace `Object.fromEntries()` with parse()
const submission = parse(formData, {
// Prepare a custom parse function based on your form requirements
function parseLoginForm(formData: FormData) {
return parse(formData, {
resolve({ email, password }) {
const error: Record<String, string> = {};

if (!email) {
// Define the error as key-value pair instead
error.email = 'Email is required';
} else if (!email.includes('@')) {
error.email = 'Email is invalid';
Expand All @@ -129,14 +126,21 @@ export async function action({ request }: ActionArgs) {
};
},
});
}

export async function action({ request }: ActionArgs) {
const formData = await request.formData();
// Replace `Object.fromEntries()` with your custom parse function
const submission = parseLoginForm(formData);

// Send the last submission result back to client if there is any error
// or if the intent is not `submit`.
if (!submission.value || submission.intent !== 'submit') {
return json(
{
...submission,
// The payload will be used as the default value if the document is reloaded on submit
// The payload will be used as the default value
// if the document is reloaded on form submit
payload: {
email: submission.payload.email,
},
Expand All @@ -161,6 +165,11 @@ export default function LoginForm() {
// Now Conform will start validating once user leave the field and
// revalidate for any changes triggered later
initialReport: 'onBlur',

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

return (
Expand All @@ -175,153 +184,99 @@ export default function LoginForm() {
<input type="password" name="password" />
<div>{password.error}</div>
</div>
<button type="submit">Login</button>
<button>Login</button>
</Form>
);
}
```

The login form is getting even better! In addition to the features we mentioned before, it now...
The login form is now enhanced with Conform. In addition to the features we mentioned before, it now...

- Runs the same validation on both client and server
- Validates the fields when you leave the input
- Revalidates it once the value is changed
- Auto focuses on the first invalid field

Note that all the validation are currently done on server side, which means:
- Revalidates it if the value is changed again
- Auto focuses on the first invalid field on form submit

- It is not a must to bring in another package to the client bundle for validation (e.g. zod / yup)
- You will have one single place defining all the validation logic
## Further enhancement

## Sharing validation

Validating fully on the server is cool. However, network latency would be a concern if there is a need to provide instant feedback while user is typing. In this case, you can consider reusing the validation logic on the client as well.
Our login form example is already working quite well. But there are still things that could be improved, such as accessiblity and focus management before JS is loaded.

```tsx
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';

// Refactor the validation logic to a standalone function
function parseLoginForm(formData: FormData) {
return parse(formData, {
resolve({ email, password }) {
const error: Record<String, string> = {};

if (!email) {
error.email = 'Email is required';
} else if (!email.includes('@')) {
error.email = 'Email is invalid';
}

if (!password) {
error.password = 'Password is required';
}

if (error.email || error.password) {
return { error };
}

return {
value: { email, password },
};
},
});
}

export async function action({ request }: ActionArgs) {
const formData = await request.formData();
// Parse and validate the formData
const submission = parseLoginForm(formData);

if (!submission.value || submission.intent !== 'submit') {
return json(
{
...submission,
payload: {
email: submission.payload.email,
},
},
{
status: 400,
},
);
}

return await authenticate(submission.value.email, submission.value.password);
interface Schema {
email: string;
password: string;
}

export default function LoginForm() {
const lastSubmission = useActionData<typeof action>();
const [form, { email, password }] = useForm({
lastSubmission,
initialReport: 'onBlur',
onValidate({ formData }) {
// Run the same validation logic on client side
return parseFormData(formData);
},

// Conform will type check all the fields name based on the schema
const [form, { email, password }] = useForm<Schema>({
// By providing a form ID, you will enable Conform to generate all necessary ids for aria-attributes
id: 'login',

// The rest of the config should remains unchanged
// ...
});

return (
<Form method="post" {...form.props}>
{/* nothing changed on this part */}
<div>
<label htmlFor={emai.id}>Email</label>
{/* The `autoFocus` attribute instruct browser to focus on the first invalid input before js is loaded */}
<input
type="email"
name={email.name}
defaultValue={email.defaultValue}
autoFocus={Boolean(email.initialError)}
aria-invalid={email.error ? true : undefined}
aria-describedby={email.errorId}
/>
<div id={email.errorId}>{email.error}</div>
</div>
<div>
<label htmlFor={password.id}>Password</label>
<input
type="password"
id={password.id}
name={password.name}
autoFocus={Boolean(password.initialError)}
aria-invalid={password.error ? true : undefined}
aria-describedby={password.errorId}
/>
<div id={password.errorId}>{password.error}</div>
</div>
<button>Login</button>
</Form>
);
}
```

## Removing boilerplates

Configuring each input is tedious especially when dealing with a complex form. The [conform](/packages/conform-react/README.md#conform) helpers can be used to remove these boilerplates.

It will set the name of the input and also derive attributes for [accessibility](/docs/accessibility.md#configuration) concerns with helps on [focus management](/docs/focus-management.md#focusing-before-javascript-is-loaded) before JS is loaded.
You can also remove these boilerplates using the [conform](/packages/conform-react/README.md#conform) helper:

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

interface Schema {
email: string;
password: string;
}

function parseForm(formData: FormData) {
// as shown before
}

export async function action({ request }: ActionArgs) {
// as shown before
}

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

// By providing the schema, it will type check all the fields name
const [form, { email, password }] = useForm<Schema>({
// By providing a form ID, you will enable Conform to generate all necessary ids for aria-attributes
id: 'login',
initialReport: 'onBlur',
lastSubmission,
onValidate({ formData }) {
return parseForm(formData);
},
});
// ...

return (
<Form method="post" {...form.props}>
<div>
<label>Email</label>
<label htmlFor={email.id}>Email</label>
<input {...conform.input(email, { type: 'email' })} />
<div>{email.error}</div>
<div id={email.errorId}>{email.error}</div>
</div>
<div>
<label>Password</label>
<label htmlFor={password.id}>Password</label>
<input {...conform.input(password, { type: 'password' })} />
<div>{password.error}</div>
<div id={password.errorId}>{password.error}</div>
</div>
<button type="submit">Login</button>
<button>Login</button>
</Form>
);
}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,6 @@
"lint-staged": {
"*.{js,jsx,ts,tsx}": "eslint --ignore-path .gitignore --cache --ext .js,.jsx,.ts,.tsx",
"*.{js,jsx,ts,tsx,css,json,yaml}": "prettier --ignore-path .gitignore --write",
"*.{md}": "prettier --ignore-path .gitignore --write --use-tabs false"
"*.md": "prettier --ignore-path .gitignore --write --use-tabs false"
}
}
3 changes: 3 additions & 0 deletions packages/conform-dom/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"name": "@conform-to/dom",
"description": "A set of opinionated helpers built on top of the Constraint Validation API",
"homepage": "https://conform.guide",
"license": "MIT",
"version": "0.6.0",
"main": "index.js",
Expand All @@ -22,6 +23,8 @@
"constraint-validation",
"form",
"form-validation",
"html",
"progressive-enhancement",
"validation",
"dom"
],
Expand Down

0 comments on commit b2cc882

Please sign in to comment.