Edit items in Has Many relationship with Laravel Livewire

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"></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"></span>
                <input type="text" name="name" value="" class="input-text" placeholder="Item Name" wire:model="items..name">
            </div>
            @error('items.' . $key . '.name')
            <div class="error md:ml-6"></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..quantity">
            @error('items.' . $key . '.quantity')
            <div class="error"></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..unit_price">
            @error('items.' . $key . '.unit_price')
            <div class="error"></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()">
                <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.

  1. If the id is not set we simply remove the item from collection.
  2. When the id is set, item is and existing item in the db. So we simply mark it as deleted. 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.

  1. If the $item['id'] is empty, this is a new item. Create it and link to the order.
  2. When the $item['id'] is not empty, either we have to delete them from the db if they are marked as deleted, 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!

Post image by wsyperek from Pixabay

Saranga
Saranga A web developer and highly passionate about research and development of web technologies around PHP, HTML, JavaScript and CSS.
comments powered by Disqus