Back to Course |
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,