We have made a create page. Now we need a page for editing records. Forms can be made in two ways. The first one, make a new component and add everything, or the second, extract parts in the Form Object.
Separate Component
First, we need a new component and a route mapped to it. Then we can link the edit button to that route.
php artisan make:livewire ProductsEdit
routes/web.php:
Route::get('products', [\App\Http\Controllers\ProductController::class, 'index']);Route::get('products/create', \App\Livewire\ProductsCreate::class)->name('products.create');Route::get('products/{product}/edit', \App\Livewire\ProductsEdit::class)->name('products.edit');
resources/views/livewire/products.blade.php:
<div class="space-y-6">// ... <td> <a href="{{ route('products.edit', $product) }}" class="inline-flex items-center px-4 py-2 bg-gray-800 rounded-md font-semibold text-xs text-white uppercase tracking-widest"> Edit </a> <a wire:click="deleteProduct({{ $product->id }})" onclick="return confirm('Are you sure?') || event.stopImmediatePropagation()" href="#" class="inline-flex items-center px-4 py-2 bg-red-600 rounded-md font-semibold text-xs text-white uppercase tracking-widest"> Delete </a> </td>// ...</div>
The form itself is the same as for the create product.
resources/views/products-edit.blade.php:
<form method="POST" wire:submit="save"> <div> <label for="name" class="block font-medium text-sm text-gray-700">Name</label> <input id="name" class="block mt-1 w-full border-gray-300 rounded-md shadow-sm" type="text" wire:model="name" /> @error('name') <span class="mt-2 text-sm text-red-600">{{ $message }}</span> @enderror </div> <div class="mt-4"> <label for="description" class="block font-medium text-sm text-gray-700">Description</label> <textarea id="description" class="block mt-1 w-full border-gray-300 rounded-md shadow-sm" wire:model="description"></textarea> @error('description') <span class="mt-2 text-sm text-red-600">{{ $message }}</span> @enderror </div> <div class="mt-4"> <label for="category">Category</label> <select wire:model="category_id" name="category" id="category" class="block mt-1 w-full border-gray-300 rounded-md shadow-sm"> <option value="0">-- CHOOSE CATEGORY --</option> @foreach($categories as $id => $category) <option value="{{ $id }}">{{ $category }}</option> @endforeach </select> @error('category_id') <span class="mt-2 text-sm text-red-600">{{ $message }}</span> @enderror </div> <button class="mt-4 px-4 py-2 bg-gray-800 rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700"> Save Product </button></form>
In the Livewire components class, first, we need to assign all the properties to values from the product. Because we are using Route Model Binding, we can accept Product parameter in the mount
method and assign properties.
app/Livewire/ProductsEdit.php:
use App\Models\Product; class ProductsEdit extends Component{ #[Validate('required|min:3')] public string $name = ''; #[Validate('required|min:3')] public string $description = ''; #[Validate('required|exists:categories,id', as: 'category')] public int $category_id; public Collection $categories; public function mount(Product $product): void { $this->name = $product->name; $this->description = $product->description; $this->category_id = $product->category_id; $this->categories = Category::pluck('name', 'id'); } // ...}
After visiting the edit page, we see a filled form with the product information.
We must first save the product ID into a property to be able to save the updated product. Also, we will lock this property so that it can't be tampered with.
app/Livewire/ProductsEdit.php:
use Livewire\Attributes\Locked; class ProductsEdit extends Component{ #[Locked] public int $productId; #[Validate('required|min:3')] public string $name = ''; #[Validate('required|min:3')] public string $description = ''; #[Validate('required|exists:categories,id', as: 'category')] public int $category_id; public Collection $categories; public function mount(Product $product): void { $this->productId = $product->id; $this->name = $product->name; $this->description = $product->description; $this->category_id = $product->category_id; $this->categories = Category::pluck('name', 'id'); } // ...}
And now, we can update the product in the save
method.
app/Livewire/ProductsEdit.php:
class ProductsEdit extends Component{ // ... public function save(): void { $this->validate(); Product::where('id', $this->productId)->update($this->only(['name', 'description', 'category_id'])); $this->redirect('/products'); } // ...}
Re-use Parts Using the Form Object
We can extract some parts into a Form Object. This way, there will be less code duplication. First, we need to create a Form Object.
php artisan livewire:form ProductsForm
Next, we need to move public properties to the Form Object. The save
method is optional, but I will move it to the Form Object. First, we will update the ProductsCreate
Livewire component.
app/Livewire/ProductsCreate.php:
use App\Livewire\Forms\ProductsForm; class ProductsCreate extends Component{ public ProductsForm $form; #[Validate('required|min:3')] public string $name = ''; #[Validate('required|min:3')] public string $description = ''; #[Validate('required|exists:categories,id', as: 'category')] public int $category_id; public Collection $categories; // ... public function save(): void { $this->form->save(); $this->validate(); Product::create($this->only(['name', 'description', 'category_id'])); $this->redirect('/products'); } // ...}
app/Livewire/Forms/ProductsForm.php:
use Livewire\Attributes\Validate;use Livewire\Form; class ProductsForm extends Form{ #[Validate('required|min:3')] public string $name = ''; #[Validate('required|min:3')] public string $description = ''; #[Validate('required|exists:categories,id', as: 'category')] public int $category_id = 0; public function save(): void { $this->validate(); Product::create($this->all()); }}
And, of course, we need to update wire:model
in the Blade file to have form.
. That form
comes from naming Form Object in the Livewire component.
resources/views/livewire/products-create.blade.php:
<form method="POST" wire:submit="save"> <div> <label for="name" class="block font-medium text-sm text-gray-700">Name</label> <input id="name" class="block mt-1 w-full border-gray-300 rounded-md shadow-sm" type="text" wire:model="form.name" /> @error('form.name') <span class="mt-2 text-sm text-red-600">{{ $message }}</span> @enderror </div> <div class="mt-4"> <label for="description" class="block font-medium text-sm text-gray-700">Description</label> <textarea id="description" class="block mt-1 w-full border-gray-300 rounded-md shadow-sm" wire:model="form.description"></textarea> @error('form.description') <span class="mt-2 text-sm text-red-600">{{ $message }}</span> @enderror </div> <div class="mt-4"> <label for="category">Category</label> <select wire:model="form.category_id" name="category" id="category" class="block mt-1 w-full border-gray-300 rounded-md shadow-sm"> <option value="0">-- CHOOSE CATEGORY --</option> @foreach($categories as $id => $category) <option value="{{ $id }}">{{ $category }}</option> @endforeach </select> @error('form.category_id') <span class="mt-2 text-sm text-red-600">{{ $message }}</span> @enderror </div> <button class="mt-4 px-4 py-2 bg-gray-800 rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700"> Save Product </button></form>
Great! The create product page should be working. Now let's move to the edit page.
app/Livewire/ProductsEdit.php:
use App\Livewire\Forms\ProductsForm; class ProductsEdit extends Component{ public ProductsForm $form; #[Locked] public int $productId; #[Validate('required|min:3')] public string $name = ''; #[Validate('required|min:3')] public string $description = ''; #[Validate('required|exists:categories,id', as: 'category')] public int $category_id; public Collection $categories; public function mount(Product $product): void { $this->form->setProduct($product); $this->productId = $product->id; $this->name = $product->name; $this->description = $product->description; $this->category_id = $product->category_id; $this->categories = Category::pluck('name', 'id'); } public function save(): void { $this->form->update(); $this->validate(); Product::where('id', $this->productId)->update($this->only(['name', 'description', 'category_id'])); $this->redirect('/products'); } public function render(): View { return view('livewire.products-edit'); return view('livewire.products-create'); }}
As you can see from the ProductEdit
Livewire component, we replaced all public properties with one ProductsForm
Form Object. Then in the mount
method, instead of setting all the properties it is done using setProduct
method, which accepts the product Model and is placed inside the Form Object. The same is true for the save
method, all the update logic is inside the Form Object. And last, we can re-use the Blade file.
So now, let's update the Form Object.
app/Livewire/Forms/ProductsForm.php:
use App\Models\Product; class ProductsForm extends Form{ public ?Product $product; #[Validate('required|min:3')] public string $name = ''; #[Validate('required|min:3')] public string $description = ''; #[Validate('required|exists:categories,id', as: 'category')] public int $category_id = 0; public function setProduct(Product $product): void { $this->product = $product; $this->name = $product->name; $this->description = $product->description; $this->category_id = $product->category_id; } public function save(): void { $this->validate(); Product::create($this->all()); } public function update(): void { $this->validate(); $this->product->update($this->all()); } }
And that's it. Both forms are now using the ProductsForm
Form Object.
What is the significance of changing the render to the create, vs edit? Why did we do that?
To use the same blade file. You can name it whatever you want
makes perfect sense - reducing the number of files to decrease complexity
But why to make ProductsCreate and ProductsEdit component, cant we just make livewire component called ProductsForm and both create and edit logic to there?
You can do it this way
What way?
you can check the quiz livewire course which was just updated to livewire v3 and uses one component for the
Where did the "Lock logic" went when you refactored the Edit page into the Form page?
Because in the form object we are binding to the model the id of it is automatically locked. This is mentioned in the Livewire
Hi, when I try to use the same form to create and edit an user I have the following issue.
The code wire:click="edit({{ $user->id }})" do not bring the value to the input, I've to use ' ' between ('{{ $user->id }}').
Why this happens?
Should work without
' '
. This is how it is in the docs also https://livewire.laravel.com/docs/actions#passing-parametersCannot assign Illuminate\Support\Collection to property App\Livewire\ProductsEdit::$categories of type App\Livewire\Collection
What should i do?
Import the collection
Thank you. it worked
Can I use the same blade file for creating and editing in Class-based Volt components?
Never used volt. You should just try
Lets say I have a checkout page that allows the customer to submit multiple address's if there is more than 1 of something (qty). How can I assign a public array variable to multiple wire:model inputs? Feels like something like this should work, but it doesnt. Ive tried a lot & ran out of ideas.
wire:model.lazy="form.address[0]" wire:model.lazy="form.address[1]"
Im not working with validation for testing.
Figured it out Ill keep it here, incase there is anyone else like me. In blade or livewire not sure, you gotta assign array keys with a period apparently sooo wire:model.lazy="form.address.1"