Skip to content

Commit

Permalink
Merge branch 'next' into feat/collection-checkboxes
Browse files Browse the repository at this point in the history
  • Loading branch information
edmundhung committed Jul 22, 2023
2 parents 9724810 + 2bff957 commit 4dd0ed5
Show file tree
Hide file tree
Showing 77 changed files with 7,296 additions and 3,918 deletions.
18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# 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)
# CONFORM [![latest release](https://img.shields.io/github/v/release/edmundhung/conform?display_name=tag&sort=semver&style=flat-square&labelColor=333&color=000)](https://github.com/edmundhung/conform/releases) [![GitHub license](https://img.shields.io/github/license/edmundhung/conform?style=flat-square&labelColor=333&color=000)](https://github.com/edmundhung/conform/blob/main/LICENSE)

A progressive enhancement first form validation library for Remix and React Router

### Highlights

- Focused on progressive enhancement by default
- Progressive enhancement first APIs
- Automatic type coercion with Zod
- Simplifed integration through event delegation
- Server first validation with Zod / Yup schema support
- Field name inference with type checking
- Field name inference
- Focus management
- Accessibility support
- About 5kb compressed
Expand All @@ -17,24 +17,26 @@ A progressive enhancement first form validation library for Remix and React Rout
Here is an example built with Remix:

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

const schema = z.object({
email: z.string().min(1, 'Email is required').email('Email is invalid'),
password: z.string().min(1, 'Password is required'),
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.value || submission.intent !== 'submit') {
return json(submission, { status: 400 });
return json(report(submission));
}

return await authenticate(submission.value);
Expand Down
1 change: 0 additions & 1 deletion docs/accessibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ function Example() {
<input
{...conform.input(message, {
type: 'text',
ariaAttributes: true,
})}
/>
<div id={conform.errorId(message)}>
Expand Down
77 changes: 54 additions & 23 deletions docs/complex-structures.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,29 +17,44 @@ Conform support both nested object and array by leveraging a naming convention o

**Conform** uses the `object.property` and `array[index]` syntax to denote data structure. These notations could be combined for nest list as well. e.g. `tasks[0].content`.

The form data should be parsed using the Conform [parse](/packages/conform-react/README.md#parse) helper to resolve each data path and reconstruct the data structure accordingly.
The form data should be parsed using the Conform [parse](/packages/conform-zod/README.md#parse) helper to resolve each data path and reconstruct the data structure accordingly.

```ts
import { parse } from '@conform-to/react';

const formData = new FormData();
import { parse } from '@conform-to/zod';

// If the form data has an entry `['tasks[0].content', 'Hello World']`
const submission = parse(formData);
const submission = parse(formData, {
/* ... */
});

// The submission payload will be `{ tasks: [{ content: 'Hello World' }] }`
// The submission payload will become `{ tasks: [{ content: 'Hello World' }] }`
console.log(submission.payload);
```

## Nested Object

When you need to set up nested fields, you can pass the parent field config to the[useFieldset](/packages/conform-react/README.md#usefieldset) hook to get access to each child field with name infered automatically.
When you need to set up nested fields, you can pass the parent field config to the [useFieldset](/packages/conform-react/README.md#usefieldset) hook to get access to each child field with name infered automatically.

```tsx
import { useForm, useFieldset } from '@conform-to/react';
import { parse } from '@conform-to/zod';
import { z } from 'zod';

const schema = z.object({
address: z.object({
street: z.string(),
zipcode: z.string(),
city: z.string(),
country: z.string(),
}),
});

function Example() {
const [form, { address }] = useForm<Schema>();
const [form, { address }] = useForm({
onValidate({ formData }) {
return parse(formData, { schema });
},
});
const { city, zipcode, street, country } = useFieldset(form.ref, address);

return (
Expand All @@ -60,13 +75,23 @@ function Example() {

## Array

When you need to setup a list of fields, you can pass the parent field config to the[useFieldList](/packages/conform-react/README.md#usefieldlist) hook to get access to each item field with name infered automatically as well.
When you need to setup a list of fields, you can pass the parent field config to the [useFieldList](/packages/conform-react/README.md#usefieldlist) hook to get access to each item field with name infered automatically as well.

```tsx
import { useForm, useFieldList } from '@conform-to/react';
import { parse } from '@conform-to/zod';
import { z } from 'zod';

const schema = z.object({
tasks: z.array(z.string()),
});

function Example() {
const [form, { tasks }] = useForm();
const [form, { tasks }] = useForm({
onValidate({ formData }) {
return parse(formData, { schema });
},
});
const list = useFieldList(form.ref, tasks);

return (
Expand All @@ -85,7 +110,7 @@ function Example() {
}
```

For information about modifying list (e.g. append / remove / reorder), see the [Modifying a list](/docs/intent-button.md#modifying-a-list) section.
For information about modifying list (e.g. append / remove / reorder), see the [list intent](/docs/intent-button.md#list-intent) section.

## Nested List

Expand All @@ -94,18 +119,24 @@ You can also combine both [useFieldset](/packages/conform-react/README.md#usefie
```tsx
import type { FieldConfig } from '@conform-to/react';
import { useForm, useFieldset, useFieldList } from '@conform-to/react';

interface Todo {
title: string;
notes: string;
}

interface Schema {
todos: Todo[];
}
import { parse } from '@conform-to/zod';
import { z } from 'zod';

const schema = z.object({
todos: z.array(
z.object({
title: z.string(),
notes: z.string(),
}),
),
});

function Example() {
const [form, { tasks }] = useForm();
const [form, { tasks }] = useForm({
onValidate({ formData }) {
return parse(formData, { schema });
},
});
const todos = useFieldList(form.ref, tasks);

return (
Expand All @@ -114,15 +145,15 @@ function Example() {
{todos.map((todo) => (
<li key={todo.key}>
{/* Pass each item config to TodoFieldset */}
<TodoFieldset {...todo} />
<TodoFieldset config={todo} />
</li>
))}
</ul>
</form>
);
}

function TodoFieldset(config: FieldConfig<Todo>) {
function TodoFieldset(props: { config: FieldConfig<Todo> }) {
const ref = useRef<HTMLFieldsetElement>(null);
// Both useFieldset / useFieldList accept form or fieldset ref
const { title, notes } = useFieldset(ref, config);
Expand Down
46 changes: 16 additions & 30 deletions docs/file-upload.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,27 @@
# File Upload

Conform support validating file input as well.
Conform support validating a file input as well.

<!-- aside -->

## On this page

- [Setting up a file input](#setting-up-a-file-input)
- [Configuration](#configuration)
- [Multiple files](#multiple-files)

<!-- /aside -->

## Setting up a file input
## Configuration

The setup is similar to other form controls except the **encType** must be set to `multipart/form-data`.
Setting up a file input is similar to other form controls except the form **encType** attribute must be set to `multipart/form-data`.

```tsx
import { useForm } from '@conform-to/react';
import { parse } from '@conform-to/zod';
import { z } from 'zod';

const schema = z.object({
profile: z
.instanceof(File)
// When browser constructs a form data from an empty file input, a default file
// entry would be created. we can validate this by checking the filename and size.
.refine(
(file) => file.name !== '' && file.size !== 0,
'Profile is required',
),
profile: z.instanceof(File, { message: 'Profile is required' }),
});

function Example() {
Expand All @@ -53,10 +46,7 @@ function Example() {

## Multiple files

There are some caveats when validating a multiple file input:

- Conform will transform the value to an array only when there are more than one files selected. To ensure a consistent data structure, you need to preprocess the data as shown in the snippet.
- Conform will not populate individual error on each file. Please make sure all error messages are assigned properly, e.g. `files` instead of `files[1]`.
The setup is no different for multiple files input.

```tsx
import { useForm } from '@conform-to/react';
Expand All @@ -65,21 +55,17 @@ import { z } from 'zod';

const schema = z.object({
files: z
.preprocess((value) => {
if (Array.isArray(value)) {
// No preprocess needed if the value is already an array
return value;
} else if (value instanceof File && value.name !== '' && value.size > 0) {
// Wrap it in an array if the file is valid
return [value];
} else {
// Treat it as empty array otherwise
return [];
}
}, z.instanceof(File).array().min(1, 'At least 1 file is required'))
.array(
z
.instanceof(File)
// Don't validate individual file. The error below will be ignored.
.refine((file) => file.size < 1024, 'File size must be less than 1kb'),
)
.min(1, 'At least 1 file is required')
// Instead, please validate it on the array level
.refine(
(files) => files.reduce((size, file) => size + file.size, 0) < 5 * 1024,
'Total file size must be less than 5kb',
(files) => files.every((file) => file.size < 1024),
'File size must be less than 1kb',
),
});

Expand Down
4 changes: 0 additions & 4 deletions docs/integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ function Example() {
<input
ref={shadowInputRef}
{...conform.input(currency, {
ariaAttributes: true,
hidden: true,
})}
/>
Expand Down Expand Up @@ -148,7 +147,6 @@ function Select({ options, ...config }: SelectProps) {
<input
ref={shadowInputRef}
{...conform.input(config, {
ariaAttributes: true,
hidden: true,
})}
/>
Expand Down Expand Up @@ -184,7 +182,6 @@ function Select({ options, ...config }: SelectProps) {
<input
ref={shadowInputRef}
{...conform.input(config, {
ariaAttributes: true,
hidden: true,
})}
onFocus={() => customInputRef.current?.focus()}
Expand Down Expand Up @@ -217,7 +214,6 @@ function Select({ options, .. }: SelectProps) {
<input
ref={shadowInputRef}
{...conform.input(config, {
ariaAttributes: true,
hidden: true,
})}
onChange={(e) => setValue(e.target.value)}
Expand Down
14 changes: 7 additions & 7 deletions docs/intent-button.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ A submit button can contribute to the form data when it triggers the submission
## On this page

- [Submission Intent](#submission-intent)
- [Modifying a list](#modifying-a-list)
- [Validation](#validation)
- [Triggering an intent](#triggering-an-intent)
- [List intent](#list-intent)
- [Validate intent](#validate-intent)
- [Triggering intent](#triggering-intent)

<!-- /aside -->

Expand Down Expand Up @@ -77,7 +77,7 @@ function Product() {
}
```

### Modifying a list
### List intent

Conform provides built-in [list](/packages/conform-react/README.md#list) intent button builder for you to modify a list of fields.

Expand Down Expand Up @@ -107,7 +107,7 @@ export default function Todos() {
}
```

## Validation
## Validate intent

A validation can be triggered by configuring a button with the [validate](/packages/conform-react/README.md#validate) intent.

Expand All @@ -128,7 +128,7 @@ export default function Todos() {
}
```

## Triggering an intent
## Triggering intent

Sometimes, it could be useful to trigger an intent without requiring users to click on the intent button. We can achieve it by capturing the button element with `useRef` and triggering the intent with `button.click()`

Expand Down Expand Up @@ -203,4 +203,4 @@ export default function Todos() {
}
```

Conform is also utilizing the requestIntent helper to trigger the validate intent based on input and blur event.
Conform is also utilizing the `requestIntent` helper to trigger the `validate` intent on input or blur event.
Loading

0 comments on commit 4dd0ed5

Please sign in to comment.