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\Validate;use Livewire\Form; class PostForm extends Form{ #[Validate('required|min:5')] public string $title = ''; #[Validate('required|min:5')] public string $body = '';}
app/Livewire/CreatePost.php:
use App\Livewire\Forms\PostForm; class CreatePost extends Component{ public PostForm $form; #[Validate('required|min:5')] public string $title = ''; #[Validate('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.
Our goal is to have the URL of /posts/[posts.id]/edit
.
Let's generate our second Livewire component.
php artisan make:livewire EditPost
But to use that, we need to prepare the Laravel part: Route and Blade View.
routes/web.php:
// ... Route::view('posts/create', 'posts.create');Route::view('posts/{post}/edit', 'posts.edit');
You could also create a Controller, that's a personal preference. But even without the Controller, Laravel will automatically take care of Route Model Binding from the ID in the URL to the Post model in the Blade View.
Later in the course we will talk about so-called Full-Page Livewire components, which you would be able to use instead of Laravel Controllers.
Next, we create the new Blade edit file: we open the old file posts/create.blade.php
and do File -> Save as
into a new file, with only two changes - the title and the Livewire component name:
resources/views/posts/edit.blade.php:
<x-app-layout> <x-slot name="header"> <h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight"> {{ __('Edit Post') }} </h2> </x-slot> <div class="py-12"> <div class="max-w-7xl mx-auto sm:px-6 lg:px-8"> <div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg"> <div class="p-6 text-gray-900 dark:text-gray-100"> <livewire:edit-post :post="$post" /> </div> </div> </div> </div></x-app-layout>
So, we're loading our new Livewire component edit-post
, passing a parameter $post
to it! We will talk more about passing parameters a bit later, but for now, you see the syntax.
The View file of this new component will have an identical form in the Blade file as CreatePost
has.
The only difference is that instead of a save()
method for the submit, we will have an update()
method.
resources/views/livewire/edit-post.blade.php:
// ... all other code from resources/views/livewire/create-post.blade.php <form method="POST" wire:submit="save"> <form method="POST" wire:submit="update"> // ... all other code from resources/views/livewire/create-post.blade.php
Now, the code of the component itself:
app/Livewire/EditPost.php:
use Livewire\Component;use App\Models\Post;use App\Livewire\Forms\PostForm; class EditPost extends Component{ public PostForm $form; public bool $success = false; public function mount(Post $post): void { $this->form->setPost($post); } public function update(): void { $this->validate(); $this->form->update(); $this->success = true; } 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; #[Validate('required|min:5')] public string $title = ''; #[Validate('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 changes in the code, we reused the PostForm
Form Object for the Edit form:
Source code for this lesson is here on GitHub.
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.
When I open create post page, it shows error: Method Illuminate\Support\Stringable::isUrl does not exist.
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.Can you do a dd($this->all()) before saving into the database and see what you get?
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
Thanks
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.If you want to re-direct back with a custom message. This is my approach:
And in the view just make sure you are displaying the message:
I don't know how to do it with the "public bool $success;" that Povilas mentioned. Once the validation passes I tried:
$this->success = 'Player successfully created.'; instead of "true' but did not work for me.
I hade to use #[Validate('required|min:5')]
Livewire page component layout view not found: [components.layouts.app]
I'm getting this error when I visit the following route,
http://livewire.test/post/1/edit
I'm new to livewire. Can somebody help me?
Check the first lesson. Your layout is in different location.
Thank you so much. It worked.
But I still have a confusion. In loading the posts/create.blade.php it is not requiring the layout. While in edit, it is requiring the layout.
Hi, I have the same problem, but I don't understand where the layout goes. Thanks
Check the official documantation https://livewire.laravel.com/docs/components#layout-files
Hello, great lesson.
Do the edit-post.blade.php and the create-post.blade.php have the same code except for the part wire:submit="save/update"? Is it correct?
If this is correct I feel like we are not reusing the blade parts, only the logic in the server.
It is most likely the difference, but re-using blade files is... well, opinionated. We tend to create different files for create/save actions as it is often with some minor differences down the road. Plus it solves the issue of having bugs due to missing
@if()
cases.Hi. The $success variable is missing in EditPost, if the blade files are the same. Is it correct? Thanks
I solved. You need to launch the "php artisan livewire:layout" command
Using the code as-is throws the error;
Use
Illuminate\Contracts\View\View;
in the App\Livewire\EditPost.php file the same way as the previous Create Posts lesson;