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!: finialize server validation mode #42

Merged
merged 11 commits into from
Oct 21, 2022
5 changes: 1 addition & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,13 @@ import { useForm, useFieldset } from '@conform-to/react';

export default function LoginForm() {
const form = useForm({
onValidate({ form }) {
return form.reportValidity();
},
onSubmit(event, { submission }) {
event.preventDefault();

console.log(submission);
},
});
const { email, password } = useFieldset(form.ref, form.config);
const { email, password } = useFieldset(form.ref);

return (
<form {...form.props}>
Expand Down
3 changes: 0 additions & 3 deletions examples/basics/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,6 @@ import { useForm, useFieldset } from '@conform-to/react';

export default function LoginForm() {
const form = useForm({
onValidate({ form }) {
return form.reportValidity();
}
onSubmit(event) {
event.preventDefault();

Expand Down
5 changes: 1 addition & 4 deletions examples/basics/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,13 @@ import { useForm, useFieldset } from '@conform-to/react';

export default function LoginForm() {
const form = useForm({
onValidate({ form }) {
return form.reportValidity();
},
onSubmit(event, { submission }) {
event.preventDefault();

console.log(submission);
},
});
const { email, password } = useFieldset(form.ref, form.config);
const { email, password } = useFieldset(form.ref);

return (
<form {...form.props}>
Expand Down
5 changes: 1 addition & 4 deletions examples/list/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,7 @@ interface Todo {
export default function TodoForm() {
const form = useForm<Todo>({
initialReport: 'onBlur',
onValidate({ form }) {
return form.reportValidity();
},
async onSubmit(event, { submission }) {
onSubmit(event, { submission }) {
event.preventDefault();

console.log(submission);
Expand Down
3 changes: 0 additions & 3 deletions examples/material-ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@ interface Article {
export default function ArticleForm() {
const form = useForm<Article>({
initialReport: 'onBlur',
onValidate({ form }) {
return form.reportValidity();
},
onSubmit: (event, { submission }) => {
event.preventDefault();

Expand Down
3 changes: 0 additions & 3 deletions examples/nested/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@ interface Payment {

export default function PaymentForm() {
const form = useForm<Payment>({
onValidate({ form }) {
return form.reportValidity();
},
onSubmit(event, { submission }) {
event.preventDefault();

Expand Down
18 changes: 7 additions & 11 deletions examples/remix/app/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
useFieldList,
conform,
parse,
reportValidity,
setFormError,
} from '@conform-to/react';
import { getError } from '@conform-to/zod';
import type { ActionArgs } from '@remix-run/node';
Expand All @@ -28,9 +28,7 @@ export let action = async ({ request }: ActionArgs) => {
const formData = await request.formData();
const submission = parse(formData);
const result = todoSchema.safeParse(submission.value);
const error = !result.success
? submission.error.concat(getError(result.error, submission.scope))
: submission.error;
const error = submission.error.concat(getError(result));

switch (submission.type) {
case 'validate': {
Expand Down Expand Up @@ -59,14 +57,12 @@ export default function TodoForm() {
state,
onValidate({ form, submission }) {
const result = todoSchema.safeParse(submission.value);
const error = !result.success
? submission.error.concat(getError(result.error, submission.scope))
: submission.error;

return reportValidity(form, {
...submission,
error,
});
if (!result.success) {
submission.error = submission.error.concat(getError(result.error));
}

setFormError(form, submission);
},
onSubmit(event, { submission }) {
switch (submission.type) {
Expand Down
95 changes: 37 additions & 58 deletions examples/server-validation/app/routes/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import type { FormState } from '@conform-to/react';
import type { Submission } from '@conform-to/react';
import {
conform,
parse,
useFieldset,
useForm,
hasError,
reportValidity,
shouldValidate,
setFormError,
} from '@conform-to/react';
import { getError } from '@conform-to/zod';
import type { ActionArgs } from '@remix-run/node';
Expand Down Expand Up @@ -43,27 +44,20 @@ export let action = async ({ request }: ActionArgs) => {
* (1) `submission.value`: Structured form value based on the name (path)
* (2) `submission.error`: Error (if any) while parsing the FormData object,
* (3) `submission.type` : Type of the submission.
* Set only when the user click on named button with pattern (`conform/${type}`),
* e.g. `validate`
* (4) `submission.scope`: Scope of the submission. Name of the fields that should be validated.
* e.g. The scope will be `name` only when the user is typing on the name field.
* The type would be `undefined` when user click on any normal submit button.
* It would be set only when the user click on named button with pattern (`conform/${type}`),
* e.g. Conform is clicking on a button with name `conform/validate` when validating, so the type would be `valdiate`.
*/
const submission = parse(formData);
const result = await schema
// Async validation. e.g. checking uniqueness
.refine(
async (employee) => {
// Zod does
if (!submission.scope.includes('email')) {
return true;
}

// Async validation. e.g. checking uniqueness
return new Promise((resolve) => {
async (employee) =>
new Promise((resolve) => {
setTimeout(() => {
resolve(employee.email === 'hey@conform.guide');
}, Math.random() * 100);
});
},
}),
{
message: 'Email is already used',
path: ['email'],
Expand All @@ -74,11 +68,8 @@ export let action = async ({ request }: ActionArgs) => {
// Return the state to the client if the submission is made for validation purpose
if (!result.success || submission.type === 'validate') {
return json({
scope: submission.scope,
value: submission.value,
error: submission.error.concat(
!result.success ? getError(result.error, submission.scope) : [],
),
...submission,
error: submission.error.concat(getError(result)),
});
}

Expand All @@ -87,9 +78,9 @@ export let action = async ({ request }: ActionArgs) => {
return redirect('/');
};

export default function TodoForm() {
// FormState returned from the server
const state = useActionData<FormState<Schema>>();
export default function EmployeeForm() {
// Last submission returned from the server
const state = useActionData<Submission<Schema>>();

/**
* The useForm hook now returns a `Form` object
Expand All @@ -98,63 +89,51 @@ export default function TodoForm() {
* (2) form.config: Fieldset config to be passed to the useFieldset hook.
* [Optional] Needed only if the fields have default value / nojs support is needed)
* (3) form.ref: Ref object of the form element. Same as `form.props.ref`
* (4) form.error: Form error. Set when an error with an empty string name is provided by the form state.
* (4) form.error: Form error. Set when an error with an empty string name is provided.
*/
const form = useForm<Schema>({
// Enable server validation mode
mode: 'server-validation',

// Begin validating on blur
initialReport: 'onBlur',

// Just hook it up with the result from useActionData()
state,
initialReport: 'onBlur',

/**
* The validate hook - `onValidate(context: FormContext): boolean`
* Changes includes:
*
* (1) Renamed from `validate` to `onValidate`
* (2) Changed the function signature with a new context object, including `form`, `formData` and `submission`
* (3) It should now returns a boolean indicating if the server validation is needed
*
* If both `onValidate` and `onSubmit` are commented out, then it will validate the form completely by server validation
*/
onValidate({ form, submission }) {
// Similar to server validation without the extra refine()
const result = schema.safeParse(submission.value);
const error = submission.error.concat(
!result.success ? getError(result.error) : [],
);

/**
* Since only `email` requires extra validation from the server.
* We skip reporting client error if the email is being validated while there is no error found from the client.
* e.g. Client validation would be enough if the email is invalid
*/
if (submission.scope.includes('email') && !hasError(error, 'email')) {
// Server validation is needed
return true;
if (!result.success) {
submission.error = submission.error.concat(getError(result.error));
}

if (
shouldValidate(submission, 'email') &&
!hasError(submission.error, 'email')
) {
// Skip reporting client error
throw form;
}

/**
* The `reportValidity` helper does 2 things for you:
* (1) Set all error to the dom and trigger the `invalid` event through `form.reportValidity()`
* (2) Return whether the form is valid or not. If the form is invalid, stop it.
* Set the submission error to the dom
*/
return reportValidity(form, {
...submission,
error,
});
setFormError(form, submission);
},
async onSubmit(event, { submission }) {
/**
* The `onSubmit` hook will be called only if `onValidate` returns true,
* or when `noValidate` / `formNoValidate` is configured
*/
switch (submission.type) {
case 'validate': {
if (submission.data !== 'email') {
// We need server validation only for the email field, stop the rest
event.preventDefault();
}
break;
}
onSubmit(event, { submission }) {
if (submission.type === 'validate' && submission.metadata !== 'email') {
event.preventDefault();
}
},
});
Expand Down
2 changes: 1 addition & 1 deletion examples/validation/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export default function SignupForm() {
const form = useForm<Signup>({
onValidate({ form, submission }) {
for (const field of Array.from(form.elements)) {
if (isFieldElement(field) && submission.scope.includes(field.name)) {
if (isFieldElement(field)) {
switch (field.name) {
case 'email':
if (field.validity.valueMissing) {
Expand Down
8 changes: 3 additions & 5 deletions examples/yup/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useFieldset, useForm, reportValidity } from '@conform-to/react';
import { useFieldset, useForm, setFormError } from '@conform-to/react';
import { getError } from '@conform-to/yup';
import * as yup from 'yup';

Expand Down Expand Up @@ -26,17 +26,15 @@ export default function SignupForm() {
});
} catch (error) {
if (error instanceof yup.ValidationError) {
submission.error = submission.error.concat(
getError(error, submission.scope),
);
submission.error = submission.error.concat(getError(error));
} else {
submission.error = submission.error.concat([
['', 'Validation failed'],
]);
}
}

return reportValidity(form, submission);
setFormError(form, submission);
},
onSubmit: async (event, { submission }) => {
event.preventDefault();
Expand Down
13 changes: 6 additions & 7 deletions examples/zod/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { reportValidity, useFieldset, useForm } from '@conform-to/react';
import { setFormError, useFieldset, useForm } from '@conform-to/react';
import { getError } from '@conform-to/zod';
import { z } from 'zod';

Expand All @@ -24,12 +24,11 @@ export default function SignupForm() {
onValidate({ form, submission }) {
const result = schema.safeParse(submission.value);

return reportValidity(form, {
...submission,
error: !result.success
? submission.error.concat(getError(result.error, submission.scope))
: submission.error,
});
if (!result.success) {
submission.error = submission.error.concat(getError(result.error));
}

setFormError(form, submission);
},
onSubmit: async (event, { submission }) => {
event.preventDefault();
Expand Down
Loading