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,
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
ok so the reason was that I have not included the
in my CreatePosst Component. The Tutorial did not mention that. Now the issue is solved.
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 callingpublic ExpenseForm $form; public CheckForm $checkform;
in myExpenseCreate
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! PatrykHaving 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.
My form is not being reset after this, I have double checked my code, and it is exactly the same as in this tutorial.
Somethi is conflicting. Are you using breeze? If so then remove alpine. Any console errors?
Are there any console errors? Are you using any other packages? Like breeze?
Yes I am using breeze and Console has no errors, just a warning whihc says
unreachable code after return statement ------- livewire.js:5747:
Should read how livewire works and handles livewire the . Check Povilas https://twitter.com/PovilasKorop/status/1696879703187984583?t=uUckV2O_G6V_XMyrGW_QXA&s=19
I have done that but still the same results
And compiled after removing alpine that comes with breeze?
How should I compile. I am not that expert, please I have run the command npm run dev but the same result
Don't use breeze then. Only use livewire and nothing else
ok
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?
Show some code you have maybe them will be able to help somehow. Don't forget to format code using markdown
use App\Models\Post;
class PostForm extends Form { public function save(): void { Post::create($this->all());
}
the $this->reset('form.title', 'form.body'); here should be $this->reset('title', 'body');
Thanks. Updated lesson.
Hi, in V2 i was able to validate array values like this:
I’m confused on how to carry that over to a form object in V3
You should be able to do the same. Unless theres a
Hey Povilas,
When refactoring the code to the
PostForm
class, you can also move the call to$this->validate();
from theCreatePost
class to thePostForm
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.Have you read the whole lesson? Validation is moved to the form obejct :)
Hey Povilas,
After adding
public ?Post $post;
to thePostForm
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 thePostForm
save()
andupdate()
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.In the function
upate
for theEditComponent
, is there no need to call$this->validate()
before updating ?If you are using form object then it can be called in it, otherwise you need to call it in the component
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.
We aren't creating a CRUD here, so list isn't needed here. As for the route will add it.
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 thesave()
method of theCreatePost
component. But on the edit form, we are callingupdate()
on theEditPost
component which does not have$this->validate();
but actually validation is working properly. How is this happening?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 theupdate()
method in Form Object.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 thesave()
andupdate()
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
EditPost.php
PostForm.php
So you shouldn't need
method="POST"
. It should just be<form wire:submit="update">
. Here's an alert from the docsI would read the official docs again. https://livewire.laravel.com/docs/validation
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.