Courses

Practical Livewire 3: Order Management System Step-by-Step

Order Create/Edit with Pikaday

In this lesson, we will create a Livewire component for creating and editing orders. We will reuse some logic from before lessons, like select2 component or Pikaday, so not everything new will be new here.

working order form

Again, let's start this lesson by creating the Livewire component, Route Model binding Order and we will add a frontend layout with hard-coded data. Next, as this is a new component we need to register a route for it and make the Create and Edit buttons work. Also, to bind the input to the $order property we need validation rules, so let's also add them now.

php artisan make:livewire OrderForm

routes/web.php:

Route::middleware('auth')->group(function () {
Route::get('categories', CategoriesList::class)->name('categories.index');
 
Route::get('products', ProductsList::class)->name('products.index');
Route::get('products/create', ProductForm::class)->name('products.create');
Route::get('products/{product}', ProductForm::class)->name('products.edit');
 
Route::get('orders', OrdersList::class)->name('orders.index');
Route::get('orders/create', OrderForm::class)->name('orders.create');
Route::get('orders/{order}', OrderForm::class)->name('orders.edit');
 
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
});

resources/views/livewire/orders-list.blade.php:

<a href="{{ route('orders.create') }}" class="inline-flex items-center px-4 py-2 text-xs font-semibold tracking-widest text-white uppercase bg-gray-800 rounded-md border border-transparent hover:bg-gray-700">
Create Order
</a>
 
// ...
 
<a href="{{ route('orders.edit', $order) }}" class="inline-flex items-center px-4 py-2 text-xs font-semibold tracking-widest text-white uppercase bg-gray-800 rounded-md border border-transparent hover:bg-gray-700">
Edit
</a>

app/Livewire/OrderForm.php:

use App\Models\Order;
use Livewire\Component;
use Illuminate\Contracts\View\View;
 
class OrderForm extends Component
{
public ?Product $product = null;
 
public string $name = '';
public string $description = '';
public ?float $price;
public ?int $country_id;
 
public function mount(Order $order): void
{
if (! is_null($this->order)) {
$this->order = $order;
$this->user_id = $this->order->user_id;
$this->order_date = $this->order->order_date;
$this->subtotal = $this->order->subtotal;
$this->taxes = $this->order->taxes;
$this->total = $this->order->total;
}
}
 
public function render(): View
{
return view('livewire.order-form');
}
 
public function rules(): array
{
return [
'user_id' => ['required', 'integer', 'exists:users,id'],
'order_date' => ['required', 'date'],
'subtotal' => ['required', 'numeric'],
'taxes' => ['required', 'numeric'],
'total' => ['required', 'numeric'],
'orderProducts' => ['array']
];
}
}

Now, because we will use the Select2 component in this form as well as we used it already in the Products form, we need to load Users the same way into $listsForFields. And because we will allow select products to add into order, let's load all Products the same way into public property $allProducts. Last thing, we will show in the form taxes percent, and because we will use this value to calculate total price and taxes, we will set it in public property and assign it in the mount() method.

app/Livewire/OrderForm.php:

use App\Models\Product;
use Illuminate\Support\Collection;
 
class OrderForm extends Component
{
public ?Order $order = null;
 
public ?int $user_id;
public string $order_date = '';
public int $subtotal = 0;
public int $taxes = 0;
public int $total = 0;
 
public Collection $allProducts;
 
public array $listsForFields = [];
 
public int $taxesPercent = 0;
 
public function mount(Order $order): void
{
$this->initListsForFields();
 
if (! is_null($this->order)) {
$this->order = $order;
$this->user_id = $this->order->user_id;
$this->order_date = $this->order->order_date;
$this->subtotal = $this->order->subtotal;
$this->taxes = $this->order->taxes;
$this->total = $this->order->total;
}
 
$this->taxesPercent = config('app.orders.taxes');
}
 
public function render(): View
{
return view('livewire.order-form');
}
 
public function rules(): array
{
return [
'user_id' => ['required', 'integer', 'exists:users,id'],
'order_date' => ['required', 'date'],
'subtotal' => ['required', 'numeric'],
'taxes' => ['required', 'numeric'],
'total' => ['required', 'numeric'],
'orderProducts' => ['array']
];
}
 
protected function initListsForFields(): void
{
$this->listsForFields['users'] = User::pluck('name', 'id')->toArray();
 
$this->allProducts = Product::all();
}
}

And below is the form template with hard-coded values:

resources/views/livewire/order-form.blade.php:

<div>
<x-slot name="header">
<h2 class="text-xl font-semibold leading-tight text-gray-800">
Create/Edit Order
</h2>
</x-slot>
 
<div class="py-12">
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
<div class="overflow-hidden bg-white shadow-sm sm:rounded-lg">
<div class="p-6 bg-white border-b border-gray-200">
 
<form wire:submit.prevent="save">
@csrf
 
<div>
<x-input-label class="mb-1" for="country" :value="__('Customer')" />
 
<x-select2 class="mt-1" id="country" name="country" :options="$this->listsForFields['users']" wire:model="user_id" :selectedOptions="$user_id" />
<x-input-error :messages="$errors->get('user_id')" class="mt-2" />
</div>
 
<div class="mt-4">
<x-input-label class="mb-1" for="order_date" :value="__('Order date')" />
 
<input x-data
x-init="new Pikaday({ field: $el, format: 'MM/DD/YYYY' })"
type="text"
id="order_date"
wire:model.blur="order_date"
autocomplete="off"
class="w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50" />
<x-input-error :messages="$errors->get('order_date')" class="mt-2" />
</div>
 
{{-- Order Products --}}
<table class="mt-4 min-w-full border divide-y divide-gray-200">
<thead>
<th class="px-6 py-3 text-left bg-gray-50">
<span class="text-xs font-medium tracking-wider leading-4 text-gray-500 uppercase">Product</span>
</th>
<th class="px-6 py-3 text-left bg-gray-50">
<span class="text-xs font-medium tracking-wider leading-4 text-gray-500 uppercase">Quantity</span>
</th>
<th class="px-6 py-3 w-56 text-left bg-gray-50"></th>
</thead>
<tbody class="bg-white divide-y divide-gray-200 divide-solid">
<tr>
<td class="px-6 py-4 text-sm leading-5 text-gray-900 whitespace-no-wrap">
Product Name
</td>
<td class="px-6 py-4 text-sm leading-5 text-gray-900 whitespace-no-wrap">
Product Price
</td>
<td>
<x-primary-button>
Edit
</x-primary-button>
<button class="px-4 py-2 ml-1 text-xs text-red-500 uppercase bg-red-200 rounded-md border border-transparent hover:text-red-700 hover:bg-red-300">
Delete
</button>
</td>
</tr>
</tbody>
</table>
<div class="mt-3">
<x-primary-button wire:click="addProduct">+ Add Product</x-primary-button>
</div>
{{-- End Order Products --}}
 
<div class="flex justify-end">
<table>
<tr>
<th class="text-left p-2">Subtotal</th>
<td class="p-2">${{ number_format($subtotal / 100, 2) }}</td>
</tr>
<tr class="text-left border-t border-gray-300">
<th class="p-2">Taxes ({{ $taxesPercent }}%)</th>
<td class="p-2">
${{ number_format($taxes / 100, 2) }}
</td>
</tr>
<tr class="text-left border-t border-gray-300">
<th class="p-2">Total</th>
<td class="p-2">${{ number_format($total / 100, 2) }}</td>
</tr>
</table>
</div>
 
<div class="mt-4">
<x-primary-button type="submit">
Save
</x-primary-button>
</div>
</form>
 
</div>
</div>
</div>
</div>
</div>
 
@push('js')
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/pikaday/pikaday.js"></script>
@endpush

After visiting create or edit page you should see a similar view:

hard coded order form

First, let's start by setting if the form is for creation or editing. We'll do it in the same way as we did in the product form by setting the $editing property. Also, if we are creating, we will set the order date to today.

app/Livewire/OrderForm.php:

class OrderForm extends Component
{
public Order $order;
 
public Collection $allProducts;
 
public bool $editing = false;
 
public array $listsForFields = [];
 
public int $taxesPercent = 0;
 
public function mount(Order $order): void
{
if (! is_null($this->order)) {
$this->editing = true;
$this->order = $order;
$this->user_id = $this->order->user_id;
$this->order_date = $this->order->order_date;
$this->subtotal = $this->order->subtotal;
$this->taxes = $this->order->taxes;
$this->total = $this->order->total;
} else {
$this->order_date = today();
}
 
$this->initListsForFields();
 
$this->taxesPercent = config('app.orders.taxes');
}
 
// ...
}

resources/views/livewire/order-form.blade.php:

<x-slot name="header">
<h2 class="text-xl font-semibold leading-tight text-gray-800">
Create/Edit Order
{{ $editing ? 'Edit Order' : 'Create Order' }}
</h2>
</x-slot>

Now, when pressing the Add Product button let's show the form. We need to save all products which are assigned to order and for this, we will add a new property $orderProducts. When this button is pressed addProduct() method will be called. In that method, we need to check if any products aren't saved yet, and add a new product with default values to the $orderProducts array list.

app/Livewire/OrderForm.php:

class OrderForm extends Component
{
// ...
 
public array $orderProducts = [];
 
// ...
 
public function addProduct(): void
{
foreach ($this->orderProducts as $key => $product) {
if (!$product['is_saved']) {
$this->addError('orderProducts.' . $key, 'This line must be saved before creating a new one.');
return;
}
}
 
$this->orderProducts[] = [
'product_id' => '',
'quantity' => 1,
'is_saved' => false,
'product_name' => '',
'product_price' => 0
];
}
 
// ...
}

And for the frontend part, we need to check if $product['is_saved'] is true then we just show values, and if it's false then we show the input to select a product. Also, the same goes for the Edit and Save buttons. We only want to show the Edit button for products that are saved and Save for the product which currently is being added or edited. Replace hard-coded table body with the code below:

resources/views/livewire/order-form.blade.php:

@forelse($orderProducts as $index => $orderProduct)
<tr>
<td class="px-6 py-4 text-sm leading-5 text-gray-900 whitespace-no-wrap">
@if($orderProduct['is_saved'])
<input type="hidden" name="orderProducts[{{$index}}][product_id]" wire:model="orderProducts.{{$index}}.product_id" />
@if($orderProduct['product_name'] && $orderProduct['product_price'])
{{ $orderProduct['product_name'] }}
(${{ number_format($orderProduct['product_price'] / 100, 2) }})
@endif
@else
<select name="orderProducts[{{ $index }}][product_id]" class="focus:outline-none w-full border {{ $errors->has('$orderProducts.' . $index) ? 'border-red-500' : 'border-indigo-500' }} rounded-md p-1" wire:model.live="orderProducts.{{ $index }}.product_id">
<option value="">-- choose product --</option>
@foreach ($this->allProducts as $product)
<option value="{{ $product->id }}">
{{ $product->name }}
(${{ number_format($product->price / 100, 2) }})
</option>
@endforeach
</select>
@error('orderProducts.' . $index)
<em class="text-sm text-red-500">
{{ $message }}
</em>
@enderror
@endif
</td>
<td class="px-6 py-4 text-sm leading-5 text-gray-900 whitespace-no-wrap">
@if($orderProduct['is_saved'])
<input type="hidden" name="orderProducts[{{$index}}][quantity]" wire:model="orderProducts.{{$index}}.quantity" />
{{ $orderProduct['quantity'] }}
@else
<input type="number" step="1" name="orderProducts[{{$index}}][quantity]" class="p-1 w-full rounded-md border border-indigo-500 focus:outline-none" wire:model="orderProducts.{{$index}}.quantity" />
@endif
</td>
<td class="px-6 py-4 text-sm leading-5 text-gray-900 whitespace-no-wrap">
@if($orderProduct['is_saved'])
<x-primary-button wire:click="editProduct({{$index}})">
Edit
</x-primary-button>
@elseif($orderProduct['product_id'])
<x-primary-button wire:click="saveProduct({{$index}})">
Save
</x-primary-button>
@endif
<button class="px-4 py-2 ml-1 text-xs text-red-500 uppercase bg-red-200 rounded-md border border-transparent hover:text-red-700 hover:bg-red-300" wire:click="removeProduct({{$index}})">
Delete
</button>
</td>
</tr>
@empty
<tr>
<td colspan="3" class="px-6 py-4 text-sm leading-5 text-gray-900 whitespace-no-wrap">
Start adding products to order.
</td>
</tr>
@endforelse

After visiting create order page you should see a page similar to below:

create an order with empty products

And after clicking Add Product and selecting the product you should see:

order form after clicking add product

If you try to press two times Add Product button you will receive an error.

error when product isnt saved

Now, let's save the product to the order. The Save button calls the saveProduct() method which accepts the key value of the $orderProducts array. In the saveProduct() method first, we need to reset all errors, then using Laravel Collections we find the product we selected from all products, which we saved earlier in the $allProducts property. Then all that's left is to set appropriate values.

app/Livewire/OrderForm.php:

class OrderForm extends Component
{
// ...
public function saveProduct($index): void
{
$this->resetErrorBag();
$product = $this->allProducts->find($this->orderProducts[$index]['product_id']);
$this->orderProducts[$index]['product_name'] = $product->name;
$this->orderProducts[$index]['product_price'] = $product->price;
$this->orderProducts[$index]['is_saved'] = true;
}
// ...
}

saved product to order

The Edit button will call the editProduct() method which also needs to accept the key value of the $orderProducts array. And in this method first, we need to check if there are any unsaved products. If all products are saved we just need to set is_saved to false for the product we will be editing.

app/Livewire/OrderForm.php:

class OrderForm extends Component
{
// ...
 
public function editProduct($index): void
{
foreach ($this->orderProducts as $key => $invoiceProduct) {
if (!$invoiceProduct['is_saved']) {
$this->addError('$this->orderProducts.' . $key, 'This line must be saved before editing another.');
return;
}
}
 
$this->orderProducts[$index]['is_saved'] = false;
}
 
// ...
}

To remove the product from the list, after pressing the Delete button we will call the removeProduct() method and pass the key value of the $orderProducts array. In that method, we just need to remove the product from the $orderProducts array list and reset the keys.

app/Livewire/OrderForm.php:

class OrderForm extends Component
{
// ...
public function removeProduct($index): void
{
unset($this->orderProducts[$index]);
$this->orderProducts = array_values($this->orderProducts);
}
// ...
}

Before saving the order, first let's calculate the values of Subtotal, Taxes, and Total. We'll do it in the render() method. We just go through every product that is added to the order and do math calculations.

app/Livewire/OrderForm.php:

class OrderForm extends Component
{
// ...
 
public function render(): View
{
$this->subtotal = 0;
 
foreach ($this->orderProducts as $orderProduct) {
if ($orderProduct['is_saved'] && $orderProduct['product_price'] && $orderProduct['quantity']) {
$this->subtotal += $orderProduct['product_price'] * $orderProduct['quantity'];
}
}
 
$this->total = $this->subtotal * (1 + $this->taxesPercent / 100);
$this->taxes = $this->total - $this->subtotal;
 
return view('livewire.order-form');
}
// ...
}

Now if you will add a product to the order everything will be calculated.

order calculations

When saving the order itself besides the obvious validating form, we need to do set the correct date format, and then we can save the order. After saving the order, we need to sync products. To do that, first, we need to make a valid array and then we can pass it into sync().

app/Livewire/OrderForm.php:

use Carbon\Carbon;
 
class OrderForm extends Component
{
// ...
 
public function save(): void
{
$this->validate();
 
$this->order_date = Carbon::parse($this->order_date)->format('Y-m-d');
 
if (is_null($this->order)) {
$this->order = Order::create($this->only('user_id', 'order_date', 'subtotal', 'taxes', 'total'));
} else {
$this->order->update($this->only('user_id', 'order_date', 'subtotal', 'taxes', 'total'));
}
 
$products = [];
 
foreach ($this->orderProducts as $product) {
$products[$product['product_id']] = ['price' => $product['product_price'], 'quantity' => $product['quantity']];
}
 
$this->order->products()->sync($products);
 
$this->redirect(route('orders.index'));
}
 
// ...
}

Now, if you would visit the edit page for the newly created order, you would see that there are no products, we saved them, right? Well, we need to load all order products into the $orderProducts property. This needs to be done in the mount() method in the check if the order exists.

app/Livewire/OrderForm.php:

class OrderForm extends Component
{
// ...
 
public function mount(Order $order): void
{
if (! is_null($this->order)) {
$this->editing = true;
 
$this->order = $order;
$this->user_id = $this->order->user_id;
$this->order_date = $this->order->order_date;
$this->subtotal = $this->order->subtotal;
$this->taxes = $this->order->taxes;
$this->total = $this->order->total;
 
foreach ($this->order->products()->get() as $product) {
$this->orderProducts[] = [
'product_id' => $product->id,
'quantity' => $product->pivot->quantity,
'product_name' => $product->name,
'product_price' => $product->pivot->price,
'is_saved' => true,
];
}
} else {
$this->order_date = today();
}
 
$this->initListsForFields();
 
$this->taxesPercent = config('app.orders.taxes');
}
// ...
}

Now it works as expected and our form is completed.

edit order form

Previous: Orders Table: Filtering and Ordering
avatar

Everything fine but EDIT order.

I keep getting an error: > A non-numeric value encountered

in the render function view line:

$this->order->subtotal += $orderProduct['product_price'] * $orderProduct['quantity'];

Creating is working fine. I have dd() for checking no nulls and everything is ok and also checked the table in MySQL.

Any ideas, please?

👍 1
avatar

Hard to "blindly" debug it for you, any of those three variables could be "non-numeric" technically. Could you var_dump() all of those separately and see the values?

avatar

I understand, of course. I have dd(productPrice) inside OrderForm.php and when creating it returns the product value of 44 as 4400 but when I try to edit that order, it returns an array with dots and commas, as "4,400.00". I think I will have to check if isNumeric and if it is not convert it all the time. By the way, here in Spain, using Euros, I usually put the price as float with 2 decimal number. I understood you advice to use integer and use the two last digits as decimals. In which way is this better, please?

Thanks in advance for your help and advice.

avatar

Technically speaking, dealing with money is even more complicated, I suggest you read this in-depth article: Dealing With Money in Laravel/PHP: Best Practices

Regarding that it comes with dots and commas, this is interesting, for some reason it didn't happen during my testing. If you are unable to debug it yourself, you could push your code to GitHub and invite me (username PovilasKorop), I may take a look but only in a week or so.

avatar

Everything fine but when EDIT order in all orders page. it show A non-numeric value encountered error

$this->order->subtotal += $orderProduct['product_price'] * $orderProduct['quantity'];

in render function

avatar

i download your code from github it works fine when i order two product then i try two edit from product list it shows the same error please check A non-numeric value encountered

avatar
You can use Markdown
avatar

some of orders edit working and some orders edit showing error A non-numeric value encountered in your github code please check

avatar

Can you help to debug find those "some"? How exactly we can reproduce this: just by clicking around on random orders edit?

avatar

Dear sir, please check i shared a screen recording of the error. uploded on youtube link is unlisted. this is the link of the video

https://youtu.be/0_DGOIUJnhg

avatar

order price under six digit edit working but when it crossed seven digit or above for example like $57,356.30, $30,233.06, $96,285.75 it shows the A non-numeric value encountered error

avatar

Great, thanks! Now the error is clear, we need to debug and fix from our side, will update on this.

avatar

You can check this commit to see what was changed. Hope it helps other :)

avatar
You can use Markdown
avatar
Enrique De Jesus Robledo Camacho

if you try to edit an order adding a product that all ready exists in the order an save it, this last product wont get saved

avatar
You can use Markdown
avatar

Updating a product is only validated frontend. How can this be achieved also backend?

avatar

Add validation rules. What exactly you wamt to validate?

avatar

Both the products id and the products quantity. Just good practice. I don't know how to achieve this since the rules method is already "taken" by the Order. But I am eager to learn.

avatar

If you mean that product id should exist then there is exists validation rule. As for quantity depends what you want. If you have some stock then the rule should be Less Than Or Equal.

avatar

Thanks. I am familiar with the rules. But it's not that. Instead, it's that the product is "dynamic" when it hits the saveProduct() method. So, how do I validate and how do I send any error back to the view from a failed product validation? Also the form should show any passed values (can old method be used?)

avatar

With livewire old isn't needed. When you hit in this saveProduct() call the $this->validate() and livewire will validate with your rules.

avatar

So, I do that and add this line to rules(): 'quantity' => ['required', 'numeric', 'min:3'], (min:3 doesn't make sence here, it is just demonstrate that your sugestion doesn't work. ) Now, storing a quantity of 2 is still fine. I belive it is more complicated than that and therefor I ask for help. Let's aggree that a form shall have backend validation.

avatar

Without full code cannot help. Come to discord and try asking there.

avatar

You just have to add these line of code to see it fail...

avatar
You can use Markdown
avatar

The first code block in the tutorial for order-form.blade.php contains this line;

<x-select2 class="mt-1" id="country" name="country" :options="$this->listsForFields['users']" wire:model="user_id" />

It is missing the data binding for selectedOptions and will throw an unresolvable dependency error, replace with this;

<x-select2 class="mt-1" id="country" name="country" :options="$this->listsForFields['users']" wire:model="user_id" :selectedOptions="$user_id" />
avatar
You can use Markdown
avatar
You can use Markdown