Courses

[NEW] Livewire 3 From Scratch: Practical Course

Extract Properties to Form Objects

In this lesson, we will see how to extract all the properties into a Form Object. This is Livewire 3 new way to avoid having a long list of properties inside the Livewire component itself, moving it to a different class.


Creating a Form Object

This is the syntax in Terminal:

php artisan livewire:form PostForm

This command creates a PHP Class in the app/Livewire/Forms directory.


Extracting Properties Into a Form Object

Now that we have created a Form Object, we can move all properties from the initial CreatePost Livewire Component into the PostForm class.

And instead of having many properties in the Livewire component, we will have only one property of FormObject.

The goal is to shorten the Component code and potentially reuse the form logic in multiple Components like Create/Edit.

app/Livewire/Forms/PostForm.php:

use Livewire\Attributes\Rule;
use Livewire\Form;
 
class PostForm extends Form
{
#[Rule('required|min:5')]
public string $title = '';
 
#[Rule('required|min:5')]
public string $body = '';
}

app/Livewire/CreatePost.php:

use App\Livewire\Forms\PostForm;
 
class CreatePost extends Component
{
public PostForm $form;
 
#[Rule('required|min:5')]
public string $title = '';
 
#[Rule('required|min:5')]
public string $body = '';
 
public bool $success = false;
 
public function save(): void
{
$this->validate();
 
Post::create([
'title' => $this->title,
'body' => $this->body,
'title' => $this->form->title,
'body' => $this->form->body,
]);
 
$this->success = true;
 
$this->reset('title', 'body');
$this->reset('form.title', 'form.body');
}
 
public function render(): View
{
return view('livewire.create-post');
}
}

In the Blade file, we need to change the wire:model and validation errors, appending the prefix form. to the properties, as we now have a single $form property in the Component.

resources/views/livewire/create-post.blade.php:

// ...
<div>
<label for="title" class="block font-medium text-sm text-gray-700">Title</label>
<input id="title" wire:model="title" class="block mt-1 w-full border-gray-300 rounded-md shadow-sm" type="text" />
<input id="title" wire:model="form.title" class="block mt-1 w-full border-gray-300 rounded-md shadow-sm" type="text" />
@error('title')
@error('form.title')
<span class="mt-2 text-sm text-red-600">{{ $message }}</span>
@enderror
</div>
 
<div class="mt-4">
<label for="body" class="block font-medium text-sm text-gray-700">Body</label>
<textarea id="body" wire:model="body" class="block mt-1 w-full border-gray-300 rounded-md shadow-sm"></textarea>
<textarea id="body" wire:model="form.body" class="block mt-1 w-full border-gray-300 rounded-md shadow-sm"></textarea>
@error('body')
@error('form.body')
<span class="mt-2 text-sm text-red-600">{{ $message }}</span>
@enderror
</div>
 
// ...

It is also possible to extract the creation logic from the Component to the Form Object. This way, if all properties are used for creating a record, we can pass $this->all() into a creating method.

app/Livewire/Forms/PostForm.php:

use App\Models\Post;
 
class PostForm extends Form
{
// ...
 
public function save(): void
{
Post::create($this->all());
 
$this->reset('title', 'body');
}
}

Then, call the save method from the Form Object in the Livewire component.

app/Livewire/CreatePost.php:

class CreatePost extends Component
{
public PostForm $form;
 
public bool $success = false;
 
public function save(): void
{
$this->validate();
 
Post::create([
'title' => $this->title,
'body' => $this->body,
]);
 
$this->form->save();
 
$this->success = true;
 
$this->reset('form.title', 'form.body');
}
 
public function render(): View
{
return view('livewire.create-post');
}
}

Reusing Form Object for Edit Form

Of course, one of the benefits of the Form Object is to be able to reuse it. So let's reuse it in the Edit form.

First, we need an EditPost Livewire component, which will have an identical form in the Blade file as CreatePost has. Instead of a save() method for the submit, we will have an update().

php artisan make:livewire EditPost

resources/views/livewire/edit-post.blade.php:

// ...
 
<form method="POST" wire:submit="save">
<form method="POST" wire:submit="update">
 
// ...

app/Livewire/EditPost.php:

use App\Livewire\Forms\PostForm;
 
class EditPost extends Component
{
public PostForm $form;
 
public function mount(Post $post): void
{
$this->form->setPost($post);
}
 
public function update(): void
{
$this->validate();
 
$this->form->update();
}
 
public function render(): View
{
return view('livewire.edit-post');
}
}

Here, you can see a method called mount(). It is like a constructor method in Livewire components, initializing all the initial values. Inside that mount(), we call a method setPost() from the Form Object, setting all properties from the record.

The important part is adding a nullable type (question mark) for the Post Model.

app/Livewire/Forms/PostForm.php:

class PostForm extends Form
{
public ?Post $post;
 
#[Rule('required|min:5')]
public string $title = '';
 
#[Rule('required|min:5')]
public string $body = '';
 
public function setPost(Post $post): void
{
$this->post = $post;
 
$this->title = $post->title;
 
$this->body = $post->body;
}
 
public function save(): void
{
Post::create($this->all());
 
$this->reset('title', 'body');
}
 
public function update(): void
{
$this->post->update($this->all());
}
}

And that's it. With just a few lines of code, we reused the PostForm Form Object for the Edit form.

For example, we have this Route in Laravel:

routes/web.php:

Route::get('post/{post}/edit', \App\Livewire\EditPost::class);

And the edit page should work.


Older Livewire v2: Binding Full Model

While reading older tutorials about Livewire 2, you may find a common pattern of binding a full Model to the property in the Edit form example:

use App\Models\Post;
 
class EditPost extends Component
{
public Post $post;
 
public function mount(Post $post): void
{
$this->post = $post;
}
}

In Livewire 3, binding directly to Eloquent models has been disabled in favor of using individual Post Model properties as we did in a previous lesson or extracting to Form Objects discussed above in this lesson.

But because binding directly to Eloquent models was heavily used, this still can be possible in v3, especially useful for people upgrading their projects from v2 to v3. The binding can be re-enabled via the config by setting legacy_model_binding to true.

Publish the config:

php artisan livewire:publish --config

And then in the config file:

config/livewire.php

'legacy_model_binding' => true,
avatar

After creating the PostForm object, when I am creating a new post , I am getting this error

Typed property App\Livewire\CreatePost::$form must not be accessed before initialization

avatar

ok so the reason was that I have not included the

use App\Livewire\Forms\PostForm;

in my CreatePosst Component. The Tutorial did not mention that. Now the issue is solved.

👍 1
avatar

Hi, upgrading to V3. I have a simple form object, ExpenseForm. Inside my form blade, in-between some rows and inside the <form> I call my sub-form <livewire:checks.check-create />. I use the check form in multiple forms and would like to keep the validation for checks separate in its own form object. Right now I’m calling public ExpenseForm $form; public CheckForm $checkform; in my ExpenseCreate livewire component but I’m running into issues when validating. When I submit the form I’d like it to return validation errors for both the expense form and the sub check form. The way I did it in V2 was messy and I was hoping I can accomplish this in a clean V3 way. Thanks! Patryk

avatar

Having sub-forms inside the forms in general is a bad practice, in my opinion. In the past I had so much trouble with such approach, way before Livewire or even Laravel.

It's wrong on the browser level: the browser doesn't fully understand how to structure that mixed data from form inside the form.

So I would suggest to change your approach to avoid this.

avatar

My form is not being reset after this, I have double checked my code, and it is exactly the same as in this tutorial.

avatar

Somethi is conflicting. Are you using breeze? If so then remove alpine. Any console errors?

avatar

Are there any console errors? Are you using any other packages? Like breeze?

avatar

Yes I am using breeze and Console has no errors, just a warning whihc says

unreachable code after return statement ------- livewire.js:5747:

avatar

I have done that but still the same results

avatar

And compiled after removing alpine that comes with breeze?

avatar

How should I compile. I am not that expert, please I have run the command npm run dev but the same result

avatar

Don't use breeze then. Only use livewire and nothing else

👍 1
avatar

ok, so I found out that the fields are actually empty, as trying to save the form again the validation error came up, although the text is present in the fields? I do not know why this is happening?

avatar

Show some code you have maybe them will be able to help somehow. Don't forget to format code using markdown

avatar

use App\Models\Post;

class PostForm extends Form { public function save(): void { Post::create($this->all());

    $this->reset('form.title', 'form.body');
} 

}

the $this->reset('form.title', 'form.body'); here should be $this->reset('title', 'body');

avatar

Thanks. Updated lesson.

avatar

Hi, in V2 i was able to validate array values like this:

protected function rules()
   {
       return [
          'bids.*.amount' => 'required|numeric|regex:/^-?\d+(\.\d{1,2})?$/',
     ];
   } 

I’m confused on how to carry that over to a form object in V3

avatar

You should be able to do the same. Unless theres a

avatar

Hey Povilas,

When refactoring the code to the PostForm class, you can also move the call to $this->validate(); from the CreatePost class to the PostForm class. It makes sense to do the validation in the form class before saving, instead of splitting the validation from the saving, when you'd have to remember to repeat the validation in the update, etc.

avatar

Have you read the whole lesson? Validation is moved to the form obejct :)

avatar

Hey Povilas,

After adding public ?Post $post; to the PostForm I got an error when saving, that the posts table doesn't have a post column. This seems to be caused by using $this->all() in the PostForm save() and update() methods, which returns not only the title and body but also the post property. Using $this->except('post;); in the save and update methods solves this.

avatar

In the function upate for the EditComponent, is there no need to call $this->validate() before updating ?

avatar

If you are using form object then it can be called in it, otherwise you need to call it in the component

avatar

Don't we need a list of posts to be able to implement the edit functionality? And again, I would like o se a route example that hits the edit form.

avatar

We aren't creating a CRUD here, so list isn't needed here. As for the route will add it.

👍 1
avatar

Great tutorial, but one thing I don't understand is how is the validation still working correctly on the edit form?

We are only calling $this->validate(); in the save() method of the CreatePost component. But on the edit form, we are calling update() on the EditPost component which does not have $this->validate(); but actually validation is working properly. How is this happening?

avatar

Hosnetly don't know why validation worked for you as it shoudn't. Maybe you didn't change the submit method in the edit post? Added $this->validate() to the update() method in Form Object.

avatar

I definitely have it set as <form method="POST" wire:submit="update"> so it's not that.

I've since found that only having $this->validate(); in the save() and update() methods of the form object seems to result in things working properly. When I followed this tutorial it seemed to be suggesting that $this->validate(); was called in the individual components and doing that seemed to be causing the unexpected behaviour I described.

But now I've updated my code to be like this and it seems to work more prredictably:

CreatePost.php

    public function save(): void
    {
        $this->form->save();

        $this->success = true; 
    }

EditPost.php

    public function update(): void
    {
        $this->form->update();

        $this->success = true; 
    }

PostForm.php

    public function save(): void 
    {
        $this->validate();
        
        Post::create($this->all());
 
        $this->reset('title', 'body');
    } 

    public function update(): void 
    {
        $this->validate();

        $this->post->update($this->all());
    } 
avatar

So you shouldn't need method="POST". It should just be <form wire:submit="update">. Here's an alert from the docs

"Therules() method doesn't validate on data updates When defining rules via the rules() method, Livewire will ONLY use these validation rules to validate properties when you run $this->validate(). This is different than standard #[Rule] attributes which are applied every time a field is updated via something like wire:model."

I would read the official docs again. https://livewire.laravel.com/docs/validation

👍 1
avatar

Yes, you're right on both points. method="POST" shouldn't be part of the code in this lesson and also it seems as though validation is working without $this->validate() being present because #[Rule] attributes are applied regardless. That's definitely the behaviour I'm seeing.