Livewire 3 Forms

Image by Annie Ruygt

Fly.io can build and run your Laravel apps globally. Deploy your Laravel application on Fly.io, you’ll be up and running in minutes!

Forms are why I use Laravel. Anything that abstracts away the tedium of form security, validation, user-messaging (“hey this is required”), and saving state is my favorite thing. This is, in fact, a huge part of what keeps me using Laravel over any other framework.

Livewire has always made Forms easy to work with. In Livewire 3, it gets even better with a handy abstraction (or perhaps we’ll call this an “extraction”).

Similar to how Laravel has custom Request objects, InertiaJS has a form helper, and Spatie has (had?) that amazing-for-its-time frontend form validation, Livewire 3 now has a form abstraction.

This cleans up your Component code a ton - form input validation / handling in any web app is so routine, so rote, so tediously omnipresent that it’s nice to have it be easy AND tucked out of the way - giving the more interesting code the chance to stick in our brain in our flow state.

Forms

Instead of jamming form “stuff” into our Livewire component, we can create a new Form object:

php artisan livewire:form SnackRequestForm

We can take that form object and add the fields / validation rules:

namespace App\Livewire\Forms;

use Livewire\Attributes\Rule;
use Livewire\Form;

class SnackRequestForm extends Form
{
    #[Rule('required|in:sweet,salty,hot,cold,disgusting')]
    public $type = '';

    #[Rule('required|in:asap,morning,afternoon')]
    public $time = '';
}

Then, in our component, we can use that form:

namespace App\Livewire;

use Livewire\Component;
use App\Models\SnackRequest;
use App\Livewire\Forms\SnackRequestForm;

class SnackRequest extends Component
{
    public SnackRequestForm $form;

    public function save()
    {
        SnackRequest::create(
            $this->form->all()
        );

        return $this->redirect('/requests');
    }

    public function render()
    {
        return view('livewire.request-snack');
    }
}

Finally, your inputs in the HTML form would reference the form itself, e.g. your input list of possible snack types would have wire:model="form.type" instead of wire:model="type". The same goes for validation errors, e.g. you can use @error('form.type') {{ $message }} @enderror if an invalid type was passed.

<form wire:submit="save">
    <select wire:model="form.type">
        <option value="disgusting">Disgusting</option>
        <!-- and so on -->
    </select>
    <div>
        @error('form.type') <span class="error">{{ $message }}</span> @enderror
    </div>
    <!-- and so on -->
    <button type="submit">Save</button>
</form>

Now the more boring form stuff is tucked away.

Validation

Our form right now won’t actually do any validation - we need to use one of 2 options to get it validating.

The first way is to just call $this->validate() in our save() method:

public function save()
{
    $this->validate();

    SnackRequest::create(
        $this->form->all()
    );

    return $this->redirect('/requests');
}

This method will validate only on form submit.

However, we could instead make our model bindings “live”. Adding this makes it so updating field values sends a request across the wire (thus updating the field value “live”), which gives the backend a chance to validate it.

This works well with select lists in a simple form like ours. To use it, we append .live to wire:model in our blade template:

<form wire:submit="save">
-    <select wire:model="form.type">  
+    <select wire:model.live="form.type">
        <option value="disgusting">Disgusting</option>
        <!-- and so on -->
    </select>
    <div>
        @error('form.type') <span class="error">{{ $message }}</span> @enderror
    </div>
    <!-- and so on -->
    <button type="submit">Save</button>
</form>

Note the diff there where we append .live so it becomes wire:model.live="foo".

Further Refactoring

Since the Form object is just a class, you could add methods to it. Perhaps you could have the form save itself?

The component:

public function save()
{
+    $this->form->submit();
-    SnackRequest::create(
-        $this->form->all()
-    );

    return $this->redirect('/requests');
}

And then in the form object:

public function submit()
{
    SnackRequest::create(
        $this->all()
    );
}

That’s (potentially) tidier, depending on what data/logic your form needs when performing needed actions. You’ll need to decide if you like this “pattern” or not.

Fly.io ❤️ Laravel

Fly your servers close to your users—and marvel at the speed of close proximity. Deploy globally on Fly in minutes!

Deploy your Laravel app!  

How’s It Work?

As I looked into the new form objects, I realized it wasn’t obvious when validation happened!

You can call $this->validate() anytime within the component. However, validation also happens automagically whenever a mutation happens to one of the public properties (e.g. when you wire:model.live="form.type" and make a change to that input field).

We needed to add the .live modifier to trigger the validation, however, as otherwise Livewire doesn’t see the value of the field changing on the backend and thus wouldn’t validate it.

Digging a bit deeper - the form objects all extend this empty (but nicely namespaced) class which in turn extends this internal base class.

The new form objects collects older-style, component-defined validation rules (returned via method rules() on a component) and finds properties with attribute based validation defined within the form object. These get merged together, so validation happens smoothly and is backwards compatible from Livewire 2.