Skip to main content

Cushion

Cushion helps you auto-save forms. This prevents users from re-typing a whole form in case they accidentally close the tab or browser. And it lets customers continue their checkout later without them needing to keep the tab open.

Installation

To use Cushion you need to install a Composer and an npm package:

1composer require larasell-dev/cushion
2npm i @larasell-dev/cushion

Defining a form

Extend a Form class and define the fields with Laravel's validation rules:

 1use App\Models\User;
 2use Larasell\Cushion\Form;
 3
 4final class ContactForm extends Form
 5{
 6    public function fields(): array
 7    {
 8        return [
 9            'first_name' => 'string|max:255|required',
10            'email' => 'email|max:255|required',
11            'message' => 'string|required',
12        ];
13    }
14}

The required rules only apply on submit; draft saves treat every field as optional.

Registering the form

Now, you need to register the form in your web.php file:

1Form::add(ContactForm::class);

Some forms should only be accessible by authenticated users. To make sure this is the case you can add a middleware to a form:

1Form::add(CheckoutForm::class)
2    ->middleware('auth');

Wiring up Inertia

Use the useFormDraft hook to persist edits as the user types.

1import { useFormDraft } from '@larasell-dev/cushion';
2
3function MyForm() {
4    const { track } = useFormDraft(draft, {
5        url: '/forms/contact/draft',
6    });
7
8    return <Input type="text" name="first_name" {...track('first_name')} />;
9}

Use the track method for uncontrolled inputs. And the set method for controlled ones:

 1function MyForm() {
 2    const [firstName, setFirstName] = useState(draft.first_name);
 3
 4    const { set } = useFormDraft(draft, {
 5        url: '/forms/contact/draft',
 6    });
 7
 8    return (
 9        <Input
10            type="text"
11            name="first_name"
12            value={firstName}
13            onChange={(event) => {
14                setFirstName(event.target.value);
15                set('first_name', event.target.value);
16            }}
17        />
18    );
19}

Default values

Sometimes, it's useful to pre-fill a form with data you already have access to, like filling out the email and name of a user, or re-using the address from a previous order:

 1final class CheckoutForm extends Form
 2{
 3    // ...
 4
 5    public function defaults(?User $user): array
 6    {
 7        if ($user === null) {
 8            return [];
 9        }
10
11        return [
12            'first_name' => str($user->name)->beforeLast(' ')->toString(),
13            'last_name' => str($user->name)->afterLast(' ')->toString(),
14            'email' => $user->email,
15        ];
16    }
17}

Pass any arguments the form needs for the defaults, e.g. the authenticated user. The hydrate method merges the default values and the stored draft values and returns them:

1public function show(Request $request): Response
2{
3    return Inertia::render('checkout', [
4        'billing' => CheckoutForm::get()->hydrate($request->user()),
5    ]);
6}

A stored draft value overwrites any default value. So if a user changed a field with a default value and then reloads the page, they'll see the stored draft.

Clearing a draft

There are two ways to clear a draft and each one is useful in a different scenario.

  1. If you need a "Reset" button then you can use the clear method of the useFormDraft hook:
1function MyForm() {
2    const { clear } = useFormDraft(draft, {
3        url: '/forms/contact/draft',
4    });
5
6    return <Button onClick={clear}>Start over</Button>;
7}

clear first drops pending edits and aborts an in-flight save, then sends a request to the form's draft route.

  1. On the server you can remove the draft yourself, e.g. after handling a submission:
1ContactForm::get()->clearDraft();

Transferring a guest draft on login

Drafts for guests are scoped to the session. This is fine, but when the user logs in or creates a new account they'll lose the form drafts.

To prevent this you can transfer the drafts from a guest to an authenticated user:

1// Get the form from the current session,
2// must be called before logging the user in
3$guestForm = ContactForm::get();
4
5$request->authenticate();
6$request->session()->regenerate();
7
8$guestForm->transfer($request->user());

This will overwrite an existing draft of the same form, if the user already has a draft. Nothing happens if the user does not have a draft.