Handling forms in Laravel Livewire through a component

Video thumbnail

Livewire manages the communication between your view and your Livewire component by sending small AJAX requests. You only update public properties, and Livewire does the rest. Let's see how to create our first component.

Data binding with wire:model and component lifecycle

The wire:model attribute creates a reactive link between the input and the component's property.
You change the input → the property is updated.
You change the property → the input is updated.

AJAX submissions and automatic error handling

You just add wire:submit.prevent="submit" and Livewire handles the submission, button disabling, validation, and partial view refresh.

To create a basic form in Livewire, you must first create a Livewire component and then define the public properties that will hold the form data. Then, you must create a method to handle the form submission and validate the data using Laravel's validation functions. Finally, you must add the form's HTML markup with the wire:model attributes to link them to the public properties.

In this article, we are going to create a Livewire component, which is the structure we need to use to take advantage of the main benefits of Laravel Livewire.

Remember that to continue, you must know how to create a project in Laravel Livewire.

Create a basic form in Livewire step by step

We create the component with:

php artisan make:livewire Dashboard/Category/Save

These properties represent the form fields:

public $title;
public $text;
public $image;

We create the route with:

  Route::group(['prefix' => 'category'],function () {
        Route::get('/create', App\Http\Livewire\Dashboard\Category\Save::class)->name("d-category-create");  // crear
        Route::get('/edit/{id}', App\Http\Livewire\Dashboard\Category\Save::class)->name("d-category-edit");// edit
    });

And the model looks like the following:

class Category extends Model
{
    use HasFactory;
    protected $fillable=['title','slug','image','text'];
    public function posts()
    {
        return $this->hasMany(Post::class);
    }
    public function getImageUrl()
    {
        return URL::asset("images/".$this->image);
    }
}

In the component view, we define our form for managing categories:

<form wire:submit="submit">
    <label for="title" @class(['error' => $errors->has('title')])>Título</label>
    <input type="text" id="title" wire:model.blur="title">
    @error('title')
        <p class="text-danger">{{ $message }}</p>
    @enderror
    <label for="text" @class(['error' => $errors->has('text')])>Texto</label>
    <input type="text" id="text" wire:model.blur="text">
    @error('text')
        <p class="text-danger">{{ $message }}</p>
    @enderror
    <button type="submit">Enviar</button>
</form>

And in the component class:

namespace App\Http\Livewire\Dashboard\Category;
use App\Models\Category;
use Illuminate\Support\Facades\Log;
use Livewire\Component;
use Livewire\WithFileUploads;
class Save extends Component
{
    use WithFileUploads;
    public $title;
    public $text;
    public $image;
    public $category;
    protected $rules = [
        'title' => "required|min:2|max:255",
        'text' => "nullable",
        'image' => "nullable|image|max:1024",
    ];
    public function mount($id = null)
    {
        if ($id != null) {
            $this->category = Category::findOrFail($id);
            $this->title = $this->category->title;
            $this->text = $this->category->text;
        }
    }
    public function render()
    {
        //Log::info("render");
        return view('livewire.dashboard.category.save');
    }
    public function submit()
    {
        // validate
        $this->validate();
        // save
        if ($this->category){
            $this->category->update([
                'title' => $this->title,
                'text' => $this->text,
            ]);
            $this->emit("updated");
        }else{
           $this->category = Category::create(
            [
                'title' => $this->title,
                'slug' => str($this->title)->slug(),
                'text' => $this->text,
                ]
            );
            $this->emit("created");
        }
        // upload
        if($this->image){
            $imageName = $this->category->slug . '.' . $this->image->getClientOriginalExtension();
            $this->image->storeAs('images', $imageName,'public_upload');
    
            $this->category->update([
                'image'=> $imageName
            ]);
        }
    }
}

Notice that in the submit function we handle both processes, creating and editing, depending on the status of a category that only exists if we are in the editing phase and that we initialize in the mount phase of the component.

These are the phases of any CRUD process, and in Livewire they are very direct processes as we have already seen, let's look at it piece by piece:

I use a $rules array, just like in your own code:

protected $rules = [
   'title' => "required|min:2|max:255",
   'text' => "nullable",
   'image' => "nullable|image|max:1024",
];

Real-time validation with updated()

If you want to validate while the user types:

public function updated($propertyName)
{
   $this->validateOnly($propertyName);
}

Integrate FormRequest in Livewire (advanced case)

You can reuse your FormRequest and ask for its rules inside the component:

use App\Http\Requests\CategoryRequest;
public function rules()
{
   return (new CategoryRequest)->rules();
}

Show form fields in Laravel Livewire conditionally: step by step

Video thumbnail

I was recently asked how we could conditionally display a field. For example, if I select any value here—let's say I select "person"—I'd like an additional field to appear. So, for this, there are several considerations we need to take into account.

I'll start with the step-by-step example we did in the Livewire course, although you can perfectly adapt it to a normal form without steps. The logic is the same, since this step-by-step system we implemented is quite modular—as I've always said.

Initial considerations

The first thing is that, if you're going to create the migration (or I suppose since it's conditional...), understand that if we select "company" here—because "person" is the default value—then I want to show that additional field. If it's "person," I won't show anything.

This is simply a condition that can be evaluated using a conditional, so it can be anything.

Obviously, that additional field must exist in the entity, so you'd have to create it. In my case, I'm not going to create it because this is just a demonstration, so I don't care whether it's saved in the database or not. But it makes sense to save it, because if not, you're not doing anything with that field.

That additional field should also be defined in the model, and you can mark it as nullable, or you can define a default value if it isn't set. That's up to you and how you want to handle it. But, in short, there's not much mystery to it.

Conditional validations in forms

Here, it's best to use form-type classes with the rules() method, since you can define conditional validation rules there. For example, if a certain field has a certain value, you can apply or deny certain validations.

In this case, I didn't do it that way. If we look at the class, the validations are defined directly, so I can't do much in this example. But again: using form classes with rules() is the best, since you can evaluate conditions and apply or deny validations as needed.

You could also apply validations directly using the validator() method—if I remember correctly, there are about three ways. But since I already have this structure, I don't want to change much to respect what we did in the course. This is just a demonstration.

Basic implementation in the component

In this case, I'm using Volt, as it's working well for me. The other component seems to be having issues with events.

The first thing is to define the new field attribute. I'm going to call it extra. You should also have its validations here. Since this is a demo, I'm just defining it for simplicity.

Then, we add it to the HTML. I place it right after the field type, and it can be any field type, but I'll use another select. I'll call it extra.

new #[Layout('layouts.contact')] class extends Component {
    ***

    #[Validate('required')]
    public $type;
    
    // demo
    public $extra;

    ***

    function mount(?int $id = null, ?int $step = null, string $subject = '')
    {
        ***
    }

    public function updated($property)
    {
        // $property: The name of the current property that was updated

        if ($property === 'type' && $this->type != 'company') {
            $this->extra = '';
        }
    }
    
    function submit()
    {
       ***
    }
}; ?>

<div>
   ***
        @if ($step == 1)
            <form wire:submit.prevent='submit' class="flex flex-col max-w-sm mx-auto">
                ***

                <flux:label>{{ __('Type') }}</flux:label>
                <flux:error name='type' />
                <flux:select wire:model.live='type'>
                    <option value=""></option>
                    <option value="person">{{ __('Person') }}</option>
                    <option value="company">{{ __('Company') }}</option>
                </flux:select>
                @if ($type == 'company')
                    <flux:select wire:model='extra' :label='__("Extra")'>
                        <option value=""></option>
                        <option value="extra1">{{ __('Extra 1') }}</option>
                        <option value="extra2">{{ __('Extra 2') }}</option>
                    </flux:select>
                @endif

                <flux:label>{{ __('Message') }}</flux:label>
                ***

                <div class="flex mt-5 gap-3">
                    <flux:button variant='primary' type='submit'>{{ __('Send') }}</flux:button>
                </div>
            </form>
    </div>
</div>

Conditionally hide field

By default, I don't want the field to appear. Here we set the condition for displaying it: if the selected type is equal to "company," then we display the field.

@if ($type === 'company')
   <!-- show data -->
@endif

You can print this to verify. For example, if you select "person," nothing is displayed. If you select "company," the extra field appears. This is achieved by adding .live, which must be used carefully.

As you can see it is the reverse approach to step by step since now it is the children that register the listener event the problem that we are going to have is that as the child components are not rendered by default but rather based on some condition:

@elseif ($step == 2)
   @livewire('contact.company', ['parentId' => $pk])
@elseif ($step == 2.5)
   @livewire('contact.person', ['parentId' => $pk])
@elseif ($step == 3)
   @livewire('contact.detail')
@else

Passing parameters from parent to children

Video thumbnail

As we discussed earlier passing events from parent to child for our current implementation is not going to work because at the time the parent event is sent the child components have not yet been created one possible approach to using events is to hide the stepbystep forms using CSS and display them based on the current step however using events is not necessary to send data from a parent component to its children we can perfectly use parameter passing instead as we see below

@elseif ($step == 2)
   @livewire('contact.company', ['parentId' => $pk])
@elseif ($step == 2.5)
   @livewire('contact.person', ['parentId' => $pk])
@elseif ($step == 3)
   @livewire('contact.detail', ['parentId' => $pk])

And we define the mount method in each of the child steps

app/Http/Livewire/Contact/Company.php
app/Http/Livewire/Contact/Person.php
app/Http/Livewire/Contact/Detail.php
function mount($parentId)
{
  $this->parentId($parentId);
}

Cleaning up the value when the option changes

The problem is that if we select "person" after selecting "company," the value of the extra field remains defined. To clean this up, you can use a Livewire lifecycle method, such as updated.

This method receives the updated property. For example:

    public function updated($property)
    {
        // $property: The name of the current property that was updated

        if ($property === 'type' && $this->type != 'company') {
            $this->extra = '';
        }
    }

This automatically clears the extra field when the type is changed to something other than "company".

Local Validations (Optional)

We don't have validation in this demo yet. Since the current structure doesn't easily allow for conditional validation, you could apply manual local validation: when you submit, check if the extra field is defined, and validate it at that time.

Video thumbnail

I find this very interesting: the ability to separate the definition of each form field into a separate file.

One of the things I silently criticize about Livewire—yes, I admit it—is precisely that: we can't create Request classes like we normally do in Laravel to define form fields and validations.

  1. Instead, we have to do the validations directly in the component. And yes, that works, but:
  2. It mixes validation logic with presentation logic,
  3. It makes components longer and harder to maintain,

And it breaks a bit with the traditional, clean way of working with forms in Laravel.

class Save extends Component
{

    use WithFileUploads;

    public $title;
    public $slug;
    public $text;
    public $image;

    protected $rules = [
        'title' => "required|min:2|max:255",
        'text' => "nullable",
        'image' => "nullable|image|max:1024",
    ];

Making reuse and modularization of the component impossible, but, by using a form class, we can easily solve this.

Forms in Laravel Livewire

To create a form class, which is equivalent to the Request for controllers:

$ php artisan livewire:form PostForm

We place our fields:

<?php
namespace App\Livewire\Forms;
use Livewire\Attributes\Validate;
use Livewire\Form;

class PostForm extends Form
{
    #[Validate('required|min:2|max:255')]
    public $title = '';
    
    #[Validate('required')]
    public $date = '';
    
    #[Validate('required')]
    public $category_id = '';
    
    #[Validate('required')]
    public $posted = '';

    #[Validate('required')]
    public $type = '';
    
    #[Validate('required|min:2|max:5000')]
    public $text = '';

    #[Validate('required|min:2|max:255')]
    public $description = '';

    #[Validate('nullable|image|max:1024')]
    public $image = '';
}

In this case, according to the example, we need to use the validate attribute, which we need to migrate.

Now, our component and view are much cleaner as follows, using an instance of the previous class and not a property for each attribute:

The component looks like this:

app/Livewire/Dashboard/Post/Save.php

<?php

namespace App\Livewire\Dashboard\Post;

use App\Livewire\Forms\PostForm;
use Livewire\Attributes\Locked;
use Livewire\Component;

use App\Models\Category;
use App\Models\Post;

class Save extends Component
{

   public $post;
   public PostForm $form;

   #[Locked]
   public $id;

   public function render()
   {
       $categories = Category::get();
       return view('livewire.dashboard.post.save', compact('categories'));
   }

   function mount(?int $id = null)
   {
       if ($id != null) {
           $this->id = $id;
           $this->post = Post::findOrFail($id);
           $this->form->text = $this->post->text;
           $this->form->title = $this->post->title;
           $this->form->category_id = $this->post->category_id;
           $this->form->posted = $this->post->posted;
           $this->form->type = $this->post->type;
           $this->form->description = $this->post->description;
           $this->form->date = $this->post->date;
       }
   }

   function submit(/*$content*/)
   {
       $this->validate();

       if ($this->post) {
           $this->post->update($this->form->all());
           $this->dispatch('updated');
       } else {
           $this->post = Post::create($this->form->all());
           $this->dispatch('created');
       }

       // upload
       if ($this->form->image) {
           $imageName = $this->post->slug . '.' . $this->form->image->getClientOriginalExtension();
           $this->form->image->storeAs('images/post', $imageName, 'public_upload');

           $this->post->update([
               'image' => $imageName
           ]);
       }
   }
}

And in the view, the references to each of the fields:

resources/views/livewire/dashboard/post/save.blade.php

<div>
***
       <form wire:submit.prevent="submit" class="flex flex-col gap-4">

           <flux:input wire:model="form.title" :label="__('Title')" />
           <flux:input wire:model="form.date" :label="__('Date')" type="date" />
           <flux:textarea wire:model="form.description" :label="__('Description')" />

           <flux:textarea wire:model="form.text" :label="__('Text')" />

           <flux:label>{{ __('Posted') }}</flux:label>
           <flux:select wire:model='form.posted'>
               <option value=""></option>
               <option value="yes">Yes</option>
               <option value="not">Not</option>
           </flux:select>

           <flux:label>{{ __('Type') }}</flux:label>
           <flux:select wire:model='form.type'>
               <option value=""></option>
               <option value="advert">Advert</option>
               <option value="post">Post</option>
               <option value="course">Course</option>
               <option value="movie">Movie</option>
           </flux:select>

           <flux:label>{{ __('Category') }}</flux:label>
           <flux:select wire:model='form.category_id'>
               <option value=""></option>
               @foreach ($categories as $c)
                   <option value="{{ $c->id }}">{{ $c->title }}</option>
               @endforeach
           </flux:select>

           <flux:input wire:model="form.image" type='file' :label="__('Image')" />

           @if ($post && $post->image)
               <img class="w-40 my-3" src="{{ $post->getImageUrl() }}" alt="{{ $post->title }}">
           @endif

           <div>
               <flux:button variant="primary" type="submit">
                   {{ __('Save') }}
               </flux:button>
           </div>

       </form>
   </div>
</div>

I hated using Long Forms in Laravel Livewire until I discovered.. livewire:form

Video thumbnail

I was about to make a video trashing Livewire, which might be surprising if you've followed my other videos, in which I've thrown a little bit of dirt—humorously, or at least that's how I like to see it—on Inertia. Whenever I compare Livewire to Inertia, I also throw a few criticisms at Inertia.

And now, oddly enough, I'm throwing a little dirt at Livewire, because I've been a little annoyed with Laravel lately.

Livewire components and many fields are not good companions

I wasn't a big fan of using components in Livewire to define multiple fields. Obviously, this is a simple example, but we know that when we're working with real forms, it's not just two fields... it can be 10, 15, 20 fields.

For example, for my post creation form, I have about 20 fields, including content, description, metadata, categories, tags, etc.

And of course, with individual components per field, that becomes crazy: communication, state management, validation… everything becomes unnecessarily complicated.

Process to create

Video thumbnail

Lets start showing the potential of the components in Livewire The first thing were going to do is create the component in our CRUDs always keeping in mind the comparison with the controllers in basic Laravel

Minimal implementation to create a record

app/Http/Livewire/Dashboard/Category/Save.php

<?php

namespace App\Http\Livewire\Dashboard\Category;

use App\Models\Category;
use Livewire\Component;

class Save extends Component
{
    public $title;
    public $text;

    public function render()
    {
        return view('livewire.dashboard.category.save');
    }

    public function submit()
    {
        Category::create(
            [
                'title' => $this->title,
                'slug' => str($this->title)->slug(),
                'text' => $this->text,
            ]
        );
    }
}

This is all the code we need to create a category its very clean just an extra function which can have any name that creates a category with the properties defined above in this case we are not passing the content and image since they are optional and the idea is to show a minimal implementation

And its view:

resources/views/livewire/dashboard/category/save.blade.php

<div>
    <form wire:submit.prevent='submit'>   
        <label for="">Title</label>
        <input type="text" wire:model='title'>

        <label for="">Text</label>
        <input type="text" wire:model='text'>

        <button type="submit">Send</button>
    </form>
</div>

A basic or typical form that shouldnt be surprising but with two changes to consider

  • The wiresubmitprevent that allows you to intercept and stop a submit request by the browser and invoke a method a method that we dont have defined in our PHP view this function name will invoke the function that is in the Livewire component class that is from a clientside view we can invoke a serverside function based on HTML events without having to use ajax fetch axios or similar Livewire already takes care of this process for us
  • The wiremodel that are similar to Vues v-model but using Livewire and that is following the previous scheme indicating the name of the properties of the component class we can reference them from here from a view

All these features are free when using the magic attribute called wire, along with some of the many options we can use; finally, from the form, we pass some data and hit send.

Validate data

Video thumbnail

Another important factor is form validation the same structure that we used with basic Laravel with the handling of validations in separate classes or through the request can be done locally in the Livewire component

To validate in Livewire we have to define a protected property called rules for which we have to define the validation rules

protected $rules = [
     'title' => "required|min:2|max:255",
     'text' => "nullable"
];

It is important to note that the keys of the rules have to be the properties you are using in the wiremodel and not the columns or properties of the model you are managing apart from this you have to call the validate method to apply the validations otherwise you show the possible errors using the blade error directive

Case study

Lets modify the component class as

class Save extends Component
{

    public $title;
    public $text;

    protected $rules = [
        'title' => "required|min:2|max:255",
        'text' => "nullable",
    ];*
***

    public function submit()
    {

        // validate
        $this->validate();

        $this->category = Category::create(
            [
                'title' => $this->title,
                'slug' => str($this->title)->slug(),
                'text' => $this->text,
                ]
            );
***

In which you can see the changes we indicated above the rules property and the use of the validate method to validate the data.

In the view component we show the errors:

<div>
    <form wire:submit.prevent="submit">

        <label for="">Título</label>

        @error('title')
            {{$message}}
        @enderror

        <input type="text" wire:model="title">

        @error('text')
            {{$message}}
        @enderror

        <label for="">Texto</label>
        <input type="text" wire:model="text">

        <button type="submit">Enviar</button>
    </form>
</div>

With this if we send an invalid form we will see the errors on the screen.

Create and Edit (CRUD) process

Video thumbnail

We will see how to create the CRUD process in Livewire

The routes rutas:

Route::group(['prefix' => 'post'],function () {
       Route::get('/', App\Http\Livewire\Dashboard\Post\Index::class)->name("d-post-index");        // listado
       Route::get('/create', App\Http\Livewire\Dashboard\Post\Save::class)->name("d-post-create");  // crear
       Route::get('/edit/{id}', App\Http\Livewire\Dashboard\Post\Save::class)->name("d-post-edit");// edit
});

Next, the code for the section of creating and updating a post is presented:

app/Http/Livewire/Dashboard/Post/Save.php

<?php

namespace App\Http\Livewire\Dashboard\Post;

use App\Models\Category;
use App\Models\Post;
use Illuminate\Support\Facades\Log;
use Livewire\Component;
use Livewire\WithFileUploads;

class Save extends Component
{
   use WithFileUploads;

   public $title;
   public $date;
   public $text;
   public $description;
   public $posted;
   public $type;
   public $category_id;
   public $image;

   public $post;

   protected $rules = [
       'title' => "required|min:2|max:255",
       'description' => "required|min:2|max:255",
       'date' => "required",
       'type' => "required",
       'category_id' => "required",
       'posted' => "required",
       'text' => "required|min:2|max:5000",
       'image' => "nullable|image|max:1024",
   ];

   public function mount($id = null)
   {
       if ($id != null) {
           $this->post = Post::findOrFail($id);
           $this->title = $this->post->title;
           $this->text = $this->post->text;
           $this->date = $this->post->date;
           $this->description = $this->post->description;
           $this->posted = $this->post->posted;
           $this->type = $this->post->type;
           $this->category_id = $this->post->category_id;
       }
   }

   public function render()
   {
       $categories = Category::get();
       return view('livewire.dashboard.post.save', compact('categories'));
   }

   public function submit()
   {

       // validate

       $this->validate();

       // save

       if ($this->post) {
           $this->post->update([
               'title' => $this->title,
               'text' => $this->text,
               'description' => $this->description,
               'date' => $this->date,
               'posted' => $this->posted,
               'type' => $this->type,
               'category_id' => $this->category_id,
           ]);
           $this->dispatch("updated");
       } else {
           $this->post = Post::create(
               [
                   'title' => $this->title,
                   'slug' => str($this->title)->slug(),
                   'text' => $this->text,
                   'description' => $this->description,
                   'date' => $this->date,
                   'posted' => $this->posted,
                   'type' => $this->type,
                   'category_id' => $this->category_id,
               ]
           );
           $this->dispatch("created");
       }

       // upload
       if ($this->image) {
           $imageName = $this->post->slug . '.' . $this->image->getClientOriginalExtension();
           $this->image->storeAs('images/post', $imageName, 'public_upload');

           $this->post->update([
               'image' => $imageName
           ]);
       }
   }
}

And its view which as an important detail remembers to place the field for the dates as date type in HTML:

resources/views/livewire/dashboard/post/save.blade.php

<div>

   <div class="container">

       <x-action-message on="created">
           <div class="box-action-message">
               {{ __('Created post success') }}
           </div>
       </x-action-message>

       <x-action-message on="updated">
           <div class="box-action-message">
               {{ __('Updated post success') }}
           </div>
       </x-action-message>


       <x-form-section submit='submit'>

           <x-slot name="title">
               {{ __('Post') }}
           </x-slot>

           <x-slot name="description">
               Lorem ipsum dolor sit amet consectetur adipisicing elit. Neque hic voluptates quaerat accusantium a. Est
               voluptate voluptatibus necessitatibus a non iure rerum, nesciunt nisi assumenda quaerat nam incidunt ab.
               Facilis.
           </x-slot>

           @slot('form')

               <div class="col-span-10 sm:col-span-3">
                   <x-label for="">Title</x-label>
                   <x-input type="text" wire:model.live='title' class="w-full" />
                   @error('title')
                       {{ $message }}
                   @enderror
               </div>

               <div class="col-span-10 sm:col-span-3">
                   <x-label for="">Date</x-label>
                   <x-input type="date" wire:model='date' class="w-full" />
                   @error('date')
                       {{ $message }}
                   @enderror
               </div>

               <div class="col-span-10 sm:col-span-3">
                   <x-label for="">Description</x-label>
                   <textarea wire:model='description' class="block w-full"></textarea>
                   @error('description')
                       {{ $message }}
                   @enderror
               </div>

               <div class="col-span-10 sm:col-span-3">
                   <x-label for="">Text</x-label>
                   <textarea wire:model='text' class="block w-full"></textarea>
                   @error('description')
                       {{ $message }}
                   @enderror
               </div>

               <div class="col-span-10 sm:col-span-3">
                   <x-label for="">Posted</x-label>
                   <select class="block w-full" wire:model='posted'>
                       <option value=""></option>
                       <option value="yes">Yes</option>
                       <option value="not">Not</option>
                   </select>
                   @error('posted')
                       {{ $message }}
                   @enderror
               </div>

               <div class="col-span-10 sm:col-span-3">
                   <x-label for="">Type</x-label>
                   <select class="block w-full" wire:model='type'>
                       <option value=""></option>
                       <option value="Advert">Advert</option>
                       <option value="post">Post</option>
                       <option value="course">Course</option>
                       <option value="movie">Movie</option>
                   </select>
                   @error('type')
                       {{ $message }}
                   @enderror
               </div>

               <div class="col-span-10 sm:col-span-3">
                   <x-label for="">Category</x-label>
                   <select class="block w-full" wire:model='category_id'>
                       <option value=""></option>
                       @foreach ($categories as $c)
                           <option value="{{ $c->id }}">{{ $c->title }}</option>
                       @endforeach
                   </select>
                   @error('category')
                       {{ $message }}
                   @enderror
               </div>

               <div class="col-span-10 sm:col-span-3">
                   <x-label for="">Image</x-label>
                   <x-input type="file" wire:model='image' class="w-full" />
                   @error('image')
                       {{ $message }}
                   @enderror

                   @if ($post && $post->image)
                       <img class="w-40 my-3" src="{{ $post->getImageUrl() }}" alt="{{ $post->title }}">
                   @endif
               </div>
           @endslot

           @slot('actions')
               <x-button type="submit">Send</x-button>
           @endslot
       </x-form-section>
   </div>
</div>

wire:blur Updates on tab or field focus loss in form

Video thumbnail

Here is another option that I find very interesting, which is blur:

<flux:input wire:model.blur="title" :label="__('Title')" />

In short, it is very similar to the live one. I also had to, so to speak, place this in the corresponding section in which I spoke to you about the modifiers that we could use, but since I'm looking at it now, I can add it here, not here quickly, so it doesn't matter that much. This is basically the same live that we had before, remember the live issue:

<flux:select class="block w-full" wire:model.live='posted'>

Requests are sent to the server via Livewire every time we write.

On-premises, we won't have any problems, but when that production happens, what I mentioned before in the previous sections, if we had 1,000 users, 1,000 users typing 10 letters in this field, you can do the math. Obviously, this can cause a performance issue on the server. So, that's why, as they say, they invested in Livewire 3. It used to work like this, but it no longer works like this, as you can see, as I mentioned before, and as you can deduce, but this is useful, for example, for certain features of our application.

What does .blur do?

Note that we write it doesn't do anything, so it seems like it doesn't work, but if it loses focus here or we tap on another field, the request is sent, which can be useful for certain functionalities in our Laravel Livewire application.

Conclusion

Livewire completely simplifies form creation: less JavaScript, more productivity. In my day-to-day work, creating or editing a record—like a category with an image—becomes so natural that I almost always reuse the same pattern I showed you here.

If you apply advanced validation, file uploads, and good component organization, you can build professional-level forms without complication.

The next step, using properties in Laravel Livewire.

I agree to receive announcements of interest about this Blog.

We will see how to work the processes of creating and editing in Laravel Livewire through components, which is the initial phase of any CRUD process.

| 👤 Andrés Cruz

🇪🇸 En español