Courses

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\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,
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.

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

When I open create post page, it shows error: Method Illuminate\Support\Stringable::isUrl does not exist.

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

Should read how livewire works and handles livewire the . Check Povilas https://twitter.com/PovilasKorop/status/1696879703187984583?t=uUckV2O_G6V_XMyrGW_QXA&s=19

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

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');

👍 2
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

Can you do a dd($this->all()) before saving into the database and see what you get?

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.

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

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.

avatar

If you want to re-direct back with a custom message. This is my approach:

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

	$this->form->store();

	session()->flash('message', 'Player successfully created.');
}

And in the view just make sure you are displaying the message:

@if(session('message'))
	<div class="mb-3 alert bg-green-500 bg-opacity-50 text-white font-bold rounded-md p-3">
				{{ session('message') }}
	</div>
@endif

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.

avatar

I hade to use #[Validate('required|min:5')]

avatar

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?

avatar

Check the first lesson. Your layout is in different location.

avatar

Thank you so much. It worked.

avatar

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.

avatar

Hi, I have the same problem, but I don't understand where the layout goes. Thanks

avatar

Check the official documantation https://livewire.laravel.com/docs/components#layout-files

avatar

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.

avatar

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.

avatar

Hi. The $success variable is missing in EditPost, if the blade files are the same. Is it correct? Thanks

avatar

I solved. You need to launch the "php artisan livewire:layout" command