Edit items in Has Many relationship with Laravel Livewire

I’m new to Livewire and still trying to figure out how to do certain things I used to do with the .blade files. It was bit confusing how to edit and save the data in a relationship along with the main entity inside a Livewire component. This is how I got it resolved.
I’m developing an order management application. Each order has one or more order items. The Order Component takes care of creating and editing the orders. Let’s start with adding the required properties.
<?php
namespace App\Http\Livewire;
use App\Models\Order as OrderModel;
use App\Models\OrderItem;
use Illuminate\Support\Collection;
use Livewire\Component;
class Order extends Component
{
public $header = 'New Order';
public $order;
public $items;
public function render()
{
return view('livewire.order');
}
// rest of the component
}
Here $order
holds our Order object and $items
is a
collection of order items.
Initializing Data
The next thing is to initialize the component data. If an id
is
passed, Order Component should load the respective order from
the database, or start with a new order otherwise.
public function mount(OrderModel $order)
{
$this->order = $order;
if ($this->order->id) {
$this->header = $this->order->customer_id . ' ' . $this->order->customer_name;
}
$this->items = new Collection();
if ($this->order->orderItems->isNotEmpty()) {
foreach ($this->order->orderItems as $item) {
$this->addItem([
'id' => $item->id,
'name' => $item->name,
'quantity' => $item->quantity,
'unit_price' => $item->unit_price,
'order_id' => $item->order_id,
'deleted' => false,
]);
}
} else {
// if empty, start with one item
$this->addItem();
}
}
Note that, I had to call my Order model OrderModel
because otherwise
it makes a name conflict with the component class name, which is also called
Order
.
The most important thing here to notice is that we are converting the
OrderItem
objects to arrays. You only need to extract editable
fields and id
into the array. Note that I have added an extra field
deleted
which we will need to mark deleted items. This is useful
when we save the items back to the database.
Editing & Validating
Editing the properties of the order entity is
straightforward, For example, you may use the following code snippet to edit
customer_name
property of the Order. Notice that
how it displays the validation errors right below the input element.
<div class="mb-2">
<label for="customer-name" class="label">Customer Name</label>
<input type="text" name="order[customer_name]" value="" wire:model="order.customer_name" class="input-text" id="customer-name">
@error('order.customer_name')
<div class="error">{{ $message }}</div>
@enderror
</div>
Editing order items is not that easy. I started by adding the necessary validation rules to the Order Component.
protected $rules = [
'order.customer_id' => 'required|string|max:255',
'order.customer_name' => 'required|string|max:255',
'order.customer_phone' => 'required|string|max:255',
'order.delivery_address' => 'required|string',
'items.*.name' => 'required',
'items.*.quantity' => 'required',
'items.*.unit_price' => 'required',
];
protected $messages = [
'items.*.name.required' => 'Item name is required.',
'items.*.quantity.required' => 'Quantity is required.',
'items.*.unit_price.required' => 'Unit price is required.',
];
We have multiple items per order, so the validation rule should work for each
item in the collection. Note in the above code how this can be achieved by writing
the validation rule like items.*.name.required
.
Every time something is changed, it should be validated. We can use the
updated()
event handler of the component to do this task.
public function updated($propertyName)
{
$this->validateOnly($propertyName);
}
Input Fields
Each item in the items
collection needs a set of input fields for
editing their properties. However we don’t need to show the input fields for
deleted items. In order to get only the active (non-deleted) items, we can use
a computed property.
public function getActiveItemsProperty()
{
return $this->items->filter(function($item) {
return !isset($item['deleted']) || !$item['deleted'];
});
}
Then, the input fields can be generated using this computed property in a loop.
<div class="grid grid-cols-5 md:grid-cols-6 gap-2">
@if ($this->activeItems->isNotEmpty())
@foreach($this->activeItems as $key => $item)
<div class="col-span-6 md:col-span-3">
<div class="md:flex w-full">
<span class="inline-block align-middle text-gray-500 pr-3 mt-2">{{ $key + 1 }}</span>
<input type="text" name="name" value="" class="input-text" placeholder="Item Name" wire:model="items.{{$key}}.name">
</div>
@error('items.' . $key . '.name')
<div class="error md:ml-6">{{ $message }}</div>
@enderror
</div>
<div class="col-span-2 md:col-span-1">
<input type="text" name="quantity" value="" class="input-text" placeholder="Quantity" wire:model="items.{{$key}}.quantity">
@error('items.' . $key . '.quantity')
<div class="error">{{ $message }}</div>
@enderror
</div>
<div class="col-span-2 md:col-span-1">
<input type="text" name="unit_price" value="" class="input-text" placeholder="Unit Price" wire:model="items.{{$key}}.unit_price">
@error('items.' . $key . '.unit_price')
<div class="error">{{ $message }}</div>
@enderror
</div>
<div class="col-span-1 md:col-span-1 text-center">
<button type="button" name="button" class="btn inline-flex" wire:click="removeItem({{$key}})">
<span class="w-6 h-6 mr-1 text-red-800">
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
</span>
</button>
</div>
@endforeach
@endif
</div>
Note how it shows the validation errors right below the respective input field. Also every row has a Remove button to remove the particular item from the collection.
Add New Item
Adding a new item to the items
collection can be done with a button click.
<button wire:click="addItem">Add More</button>
Here is the respective function in component.
public function addItem($data = null)
{
if ($data == null) {
$data = [
'id' => null,
'name' => '',
'quantity' => '0',
'unit_price' => '0.00',
'order_id' => null,
'deleted' => false,
];
}
$this->items->add($data);
}
We add a new item to the items
collection with the default values
allowing the user to adjust them necessarily. So each time the
Add More button is pressed, a new items is added to the list
and the respective input fields are displayed on the form. Though we add items
they are not saved to the database yet. We come to that later.
Remove Items
Here is the removeItem()
function, which removes an item from the
items
collection.
public function removeItem($key)
{
if (empty($this->items[$key]['id'])) {
// This is a new item, simply remove it
$this->items->forget($key);
} else {
// This is an item loaded from db, mark as delete
$item = $this->items[$key];
$item['deleted'] = true;
$this->items[$key] = $item;
}
}
We have to deal with two types of items, new and existing.
- If the
id
is not set we simply remove the item from collection. - When the
id
is set, item is and existing item in the db. So we simply mark it asdeleted
. The actual deletion would take place when the items are saved to database later.
Importantly, still we are not saving anything to the database. We only mark the deleted items at this point with the intention of actually removing them from the database later.
Saving Data
Here comes the final but most important part, saving data to database. All input
fields are enclosed withing a <form>
tag which calls the
save()
function on submit.
<form class="" action="" method="post" wire:submit.prevent="save">
<!-- all input fields -->
</form>
And, here is our save()
function.
public function save()
{
$this->validate();
$this->order->user_id = auth()->id();
$this->order->save();
if ($this->order) {
if (count($this->items) > 0) {
foreach ($this->items as $key => $item) {
if (empty($item['id'])) {
$this->order->orderItems()->create($item);
} else {
if (!empty($item['deleted']) && $item['deleted']) {
OrderItem::delete($item['id']);
} else {
OrderItem::where('id', $item['id'])->update([
'name' => $item['name'],
'quantity' => $item['quantity'],
'unit_price' => $item['unit_price'],
]);
}
}
}
}
}
}
Note how it processes the items in items
collection.
- If the
$item['id']
is empty, this is a new item. Create it and link to the order. - When the
$item['id']
is not empty, either we have to delete them from the db if they are marked asdeleted
, or, update the details.
And, that’s all. The collection of items is nicely edited and updated this way. This might not be the only way to edit and update a list of items in a Has Many relationship with Livewire. Share how you did it in the comments.
Thank you!