Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(conform-react)!: simplify intent #91

Merged
merged 5 commits into from
Feb 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export async function action({ request }: ActionArgs) {
const submission = validate(formData);

try {
if (submission.error.length === 0 && submission.type === 'submit') {
if (submission.error.length === 0 && submission.intent === 'submit') {
const user = await authenticate(submission.value);

if (!user) {
Expand Down
47 changes: 28 additions & 19 deletions docs/commands.md → docs/intent-button.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,36 @@
# Commands
# Intent button

A submit button can contribute to the form data when it triggers the submission as a [submitter](https://developer.mozilla.org/en-US/docs/Web/API/SubmitEvent/submitter).

<!-- aside -->

## On this page

- [Command button](#command-button)
- [Submission Intent](#submission-intent)
- [Modifying a list](#modifying-a-list)
- [Validation](#validation)
- [Triggering a command](#triggering-a-command)

<!-- /aside -->

### Command button
### Submission Intent

The submitter allows us to extend the form with different behaviour based on the intent. However, it pollutes the form data with information used for controlling form behaviour.
The submitter allows us to extend the form with different behaviour based on the intent. However, it pollutes the form value with data that are used to control form behaviour.

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

function Product() {
const [form] = useForm({
onSubmit(event, { submission }) {
event.preventDefault();

// This will log `{ productId: 'rf23g43', intent: 'add-to-cart' }`
// or `{ productId: 'rf23g43', intent: 'buy-now' }`
console.log(submission.value);
},
});

return (
<form>
<input type="hidden" name="productId" value="rf23g43" />
Expand All @@ -33,19 +45,16 @@ function Product() {
}
```

**Conform** introduces this pattern as a **command button**. If you name the button with a prefix `conform/`, e.g. `conform/submit`, its name and value will be used to populate the type and intent of the submission instead of the form value.
In **Conform**, if the name of a button is configured with `conform.intent`, its value will be treated as the intent of the submission instead.

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

function Product() {
const [form] = useForm({
onSubmit(event, { submission }) {
event.preventDefault();

// This will log `submit`
console.log(submission.type);

// This will log `add-to-cart` or `buy-now`
console.log(submission.intent);

Expand All @@ -57,10 +66,10 @@ function Product() {
return (
<form {...form.props}>
<input type="hidden" name="productId" value="rf23g43" />
<button type="submit" name="conform/submit" value="add-to-cart">
<button type="submit" name={conform.intent} value="add-to-cart">
Add to Cart
</button>
<button type="submit" name="conform/submit" value="buy-now">
<button type="submit" name={conform.intent} value="buy-now">
Buy now
</button>
</form>
Expand All @@ -70,7 +79,7 @@ function Product() {

### Modifying a list

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

```tsx
import { useForm, useFieldList, conform, list } from '@conform-to/react';
Expand Down Expand Up @@ -102,7 +111,7 @@ export default function Todos() {

## Validation

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

```tsx
import { useForm, conform, validate } from '@conform-to/react';
Expand All @@ -123,9 +132,9 @@ export default function Todos() {
}
```

## Triggering a command
## Triggering an intent

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

```tsx
import { useForm, useFieldList, conform, list } from '@conform-to/react';
Expand Down Expand Up @@ -164,15 +173,15 @@ export default function Todos() {
}
```

However, if the command button can not be pre-configured easily, like drag and drop an item on the list with dynamic `from` / `to` index, a command can be triggered by using the [requestCommand](/packages/conform-react/README.md#requestCommand) helper.
However, if the intent button can not be pre-configured easily, like drag and drop an item on the list with dynamic `from` / `to` index, an intent can be triggered by using the [requestIntent](/packages/conform-react/README.md#requestintent) helper.

```tsx
import {
useForm,
useFieldList,
conform,
list,
requestCommand,
requestIntent,
} from '@conform-to/react';
import DragAndDrop from 'awesome-dnd-example';

Expand All @@ -181,7 +190,7 @@ export default function Todos() {
const taskList = useFieldList(form.ref, tasks.config);

const handleDrop = (from, to) =>
requestCommand(form.ref.current, list.reorder({ from, to }));
requestIntent(form.ref.current, list.reorder({ from, to }));

return (
<form {...form.props}>
Expand All @@ -198,4 +207,4 @@ export default function Todos() {
}
```

Conform also utilises this helper to trigger the validate command based on the event received and its event target, e.g. blur / input event on each field.
Conform also utilises this helper to trigger the validate intent based on the event received and its event target, e.g. blur / input event on each field.
146 changes: 57 additions & 89 deletions docs/validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,60 +53,41 @@ export async function action({ request }: ActionArgs) {
const formData = await request.formData();
const submission = parse<SignupForm>(formData);

try {
switch (submission.type) {
// The type will be `submit` by default
case 'submit':
// The type will be `validate` for validation
case 'validate':
if (!submission.value.email) {
submission.error.push(['email', 'Email is required']);
} else if (!submission.value.email.includes('@')) {
submission.error.push(['email', 'Email is invalid']);
}

if (!submission.value.password) {
submission.error.push(['password', 'Password is required']);
}

if (!submission.value.confirmPassword) {
submission.error.push([
'confirmPassword',
'Confirm password is required',
]);
} else if (
submission.value.confirmPassword !== submission.value.password
) {
submission.error.push(['confirmPassword', 'Password does not match']);
}
if (!submission.value.email) {
submission.error.push(['email', 'Email is required']);
} else if (!submission.value.email.includes('@')) {
submission.error.push(['email', 'Email is invalid']);
}

/**
* Signup only when the user click on the submit button
* and no error found
*/
if (submission.type === 'submit' && !hasError(submission.error)) {
return await signup(submission.value);
}
if (!submission.value.password) {
submission.error.push(['password', 'Password is required']);
}

break;
}
} catch (error) {
/**
* By specifying the key as '', the message will be
* treated as a form-level error and populated
* on the client side as `form.error`
*/
submission.error.push(['', 'Oops! Something went wrong.']);
if (!submission.value.confirmPassword) {
submission.error.push(['confirmPassword', 'Confirm password is required']);
} else if (submission.value.confirmPassword !== submission.value.password) {
submission.error.push(['confirmPassword', 'Password does not match']);
}

// Always sends the submission state back to client until the user is signed up
return json({
...submission,
value: {
// Never send the password back to client
email: submission.value.email,
},
});
if (hasError(submission.error) || submission.intent !== 'submit') {
return json(submission);
}

try {
return await signup(submission.value);
} catch (error) {
return json({
...submission,
error: [
/**
* By specifying the key as '', the message will be
* treated as a form-level error and populated
* on the client side as `form.error`
*/
['', 'Oops! Something went wrong.'],
],
});
}
}

export default function Signup() {
Expand Down Expand Up @@ -151,17 +132,10 @@ export async function action({ request }: ActionArgs) {
const submission = parse(formData);

try {
switch (submission.type) {
case 'validate':
case 'submit': {
const data = schema.parse(submission.value);

if (submission.type === 'submit') {
return await signup(data);
}
const data = schema.parse(submission.value);

break;
}
if (submission.intent === 'submit') {
return await signup(data);
}
} catch (error) {
submission.error.push(...formatError(error));
Expand Down Expand Up @@ -257,8 +231,9 @@ export default function Signup() {
* the username field and no error found on the client
*/
if (
submission.type === 'validate' &&
(submission.intent !== 'username' || hasError(error, 'username'))
submission.intent !== 'submit' &&
(submission.intent !== 'validate/username' ||
hasError(submission.error, 'username'))
) {
event.preventDefault();
}
Expand All @@ -281,33 +256,26 @@ export async function action({ request }: ActionArgs) {
const submission = parse(formData);

try {
switch (submission.type) {
case 'validate':
case 'submit': {
const data = await schema
.refine(
async ({ username }) => {
// Continue checking only if necessary
if (!shouldValidate(submission, 'username')) {
return true;
}

// Verifying if the username is already registed
return await isUsernameUnique(username);
},
{
message: 'Username is already used',
path: ['username'],
},
)
.parseAsync(submission.value);

if (submission.type === 'submit') {
return await signup(data);
}

break;
}
const data = await schema
.refine(
async ({ username }) => {
// Continue checking only if necessary
if (!shouldValidate(submission.intent, 'username')) {
return true;
}

// Verifying if the username is already registed
return await isUsernameUnique(username);
},
{
message: 'Username is already used',
path: ['username'],
},
)
.parseAsync(submission.value);

if (submission.intent === 'submit') {
return await signup(data);
}
} catch (error) {
submission.error.push(...formatError(error));
Expand Down
43 changes: 18 additions & 25 deletions examples/async-validation/app/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,31 +46,24 @@ export async function action({ request }: ActionArgs) {
const submission = parse(formData);

try {
switch (submission.type) {
case 'validate':
case 'submit': {
const data = await schema
.refine(
async ({ username }) => {
if (!shouldValidate(submission, 'username')) {
return true;
}
const data = await schema
.refine(
async ({ username }) => {
if (!shouldValidate(submission.intent, 'username')) {
return true;
}

return await isUsernameUnique(username);
},
{
message: 'Username is already used',
path: ['username'],
},
)
.parseAsync(submission.value);
return await isUsernameUnique(username);
},
{
message: 'Username is already used',
path: ['username'],
},
)
.parseAsync(submission.value);

if (submission.type === 'submit') {
return await signup(data);
}

break;
}
if (submission.intent === 'submit') {
return await signup(data);
}
} catch (error) {
submission.error.push(...formatError(error));
Expand All @@ -97,8 +90,8 @@ export default function Signup() {
// Only the email field requires additional validation from the server
// We trust the client result otherwise
if (
submission.type === 'validate' &&
(submission.intent !== 'username' ||
submission.intent !== 'submit' &&
(submission.intent !== 'validate/username' ||
hasError(submission.error, 'username'))
) {
event.preventDefault();
Expand Down
Loading