Skip to content

Commit

Permalink
docs: useFetcher examples (#203)
Browse files Browse the repository at this point in the history
  • Loading branch information
jeyj0 authored Jul 8, 2023
1 parent cfc41f3 commit 52aa84f
Show file tree
Hide file tree
Showing 5 changed files with 208 additions and 5 deletions.
4 changes: 3 additions & 1 deletion examples/react-router/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const router = createBrowserRouter(
createRoutesFromElements(
<Route path="/" Component={Example}>
<Route path="login" lazy={() => import('./login')} />
<Route path="login-fetcher" lazy={() => import('./login-fetcher')} />
<Route path="todos" lazy={() => import('./todos')} />
<Route path="signup" lazy={() => import('./signup')} />
</Route>,
Expand All @@ -30,7 +31,8 @@ function Example() {

<ul>
<li>
<Link to="login">Login</Link>
<Link to="login">Login</Link> (
<Link to="login-fetcher">with useFetcher</Link>)
</li>
<li>
<Link to="todos">Todo list</Link>
Expand Down
86 changes: 86 additions & 0 deletions examples/react-router/src/login-fetcher.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import type { Submission } from '@conform-to/react';
import { useForm, parse, validateConstraint } from '@conform-to/react';
import type { ActionFunctionArgs } from 'react-router-dom';
import { useFetcher, json, redirect } from 'react-router-dom';

interface Login {
email: string;
password: string;
remember: string;
}

async function isAuthenticated(email: string, password: string) {
return new Promise((resolve) => {
resolve(email === '[email protected]' && password === '12345');
});
}

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

if (
!(await isAuthenticated(
submission.payload.email,
submission.payload.password,
))
) {
return json({
...submission,
// '' denote the root which is treated as form error
error: { '': 'Invalid credential' },
});
}

return redirect('/');
}

export function Component() {
const fetcher = useFetcher<Submission>();
const [form, { email, password }] = useForm<Login>({
lastSubmission: fetcher.data,
shouldRevalidate: 'onBlur',
onValidate(context) {
return validateConstraint(context);
},
});

return (
<fetcher.Form method="post" {...form.props}>
<div className="form-error">{form.error}</div>
<label>
<div>Email</div>
<input
className={email.error ? 'error' : ''}
name="email"
type="email"
required
pattern="[^@]+@[^@]+\.[^@]+"
/>
{email.error === 'required' ? (
<div>Email is required</div>
) : email.error === 'type' || email.error === 'pattern' ? (
<div>Email is invalid</div>
) : null}
</label>
<label>
<div>Password</div>
<input
className={password.error ? 'error' : ''}
name="password"
type="password"
required
/>
{password.error === 'required' ? <div>Password is required</div> : null}
</label>
<label>
<div>
<span>Remember me</span>
<input name="remember" type="checkbox" value="yes" />
</div>
</label>
<hr />
<button>Login</button>
</fetcher.Form>
);
}
5 changes: 2 additions & 3 deletions examples/react-router/src/login.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import type { Submission } from '@conform-to/react';
import { useForm, parse, validateConstraint } from '@conform-to/react';
import type { ActionFunctionArgs } from 'react-router-dom';
import { Form, useActionData } from 'react-router-dom';
import { json, redirect } from 'react-router-dom';
import { Form, useActionData, json, redirect } from 'react-router-dom';

interface Login {
email: string;
Expand Down Expand Up @@ -40,7 +39,7 @@ export function Component() {
const lastSubmission = useActionData() as Submission;
const [form, { email, password }] = useForm<Login>({
lastSubmission,
shouldValidate: 'onBlur',
shouldRevalidate: 'onBlur',
onValidate(context) {
return validateConstraint(context);
},
Expand Down
3 changes: 2 additions & 1 deletion examples/remix/app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ export default function App() {

<ul>
<li>
<Link to="login">Login</Link>
<Link to="login">Login</Link> (
<Link to="login-fetcher">with useFetcher</Link>)
</li>
<li>
<Link to="todos">Todo list</Link>
Expand Down
115 changes: 115 additions & 0 deletions examples/remix/app/routes/login-fetcher.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { conform, parse, useForm } from '@conform-to/react';
import type { ActionArgs } from '@remix-run/node';
import { json } from '@remix-run/node';
import { useFetcher } from '@remix-run/react';

interface SignupForm {
email: string;
password: string;
confirmPassword: string;
}

function parseFormData(formData: FormData) {
return parse<SignupForm>(formData, {
resolve({ email, password, confirmPassword }) {
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 (!confirmPassword) {
error.confirmPassword = 'Confirm password is required';
} else if (confirmPassword !== password) {
error.confirmPassword = 'Password does not match';
}

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

// Return the value only if no error
return {
value: {
email,
password,
confirmPassword,
},
};
},
});
}

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

/**
* Signup only when the user click on the submit button and no error found
*/
if (!submission.value || submission.intent !== 'submit') {
// Always sends the submission state back to client until the user is signed up
return json({
...submission,
payload: {
// Never send the password back to client
email: submission.payload.email,
},
});
}

throw new Error('Not implemented');
}

export default function Signup() {
const fetcher = useFetcher<typeof action>();
// Last submission returned by the server
const lastSubmission = fetcher.data;
const [form, { email, password, confirmPassword }] = useForm({
// Sync the result of last submission
lastSubmission,

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

return (
<fetcher.Form method="post" {...form.props}>
<div className="form-error">{form.error}</div>
<div>
<label>Email</label>
<input
className={email.error ? 'error' : ''}
{...conform.input(email)}
/>
<div>{email.error}</div>
</div>
<div>
<label>Password</label>
<input
className={password.error ? 'error' : ''}
{...conform.input(password, { type: 'password' })}
/>
<div>{password.error}</div>
</div>
<div>
<label>Confirm Password</label>
<input
className={confirmPassword.error ? 'error' : ''}
{...conform.input(confirmPassword, { type: 'password' })}
/>
<div>{confirmPassword.error}</div>
</div>
<hr />
<button type="submit">Signup</button>
</fetcher.Form>
);
}

0 comments on commit 52aa84f

Please sign in to comment.