Master-Detail Form in Laravel + jQuery: Create Order With Products

Founder of QuickAdminPanel
It’s pretty easy to create a simple form in Laravel. But it’s harder to make it dynamic – the most common case is parent-child elements, like creating invoice and adding items dynamically in the same form. In this article, we will create a similar example – creating an order and adding products to it.
Database Migrations and Models
First, database structure in Laravel – three migration files:
Schema::create('products', function (Blueprint $table) { $table->increments('id'); $table->string('name'); $table->decimal('price', 15, 2)->nullable(); $table->timestamps(); }); Schema::create('orders', function (Blueprint $table) { $table->increments('id'); $table->string('customer_name'); $table->string('customer_email')->nullable(); $table->timestamps(); }); Schema::create('order_product', function (Blueprint $table) { $table->unsignedInteger('order_id'); $table->foreign('order_id')->references('id')->on('orders'); $table->unsignedInteger('product_id'); $table->foreign('product_id')->references('id')->on('products'); $table->integer('quantity'); });
As you can see, there’s a many-to-many relationship between order and product, and pivot table also contains integer field quantity.
Here’s how it looks in Laravel models.
app/Product.php:
class Product extends Model { protected $fillable = [ 'name', 'price', ]; }
app/Order.php:
class Order extends Model { protected $fillable = [ 'customer_name', 'customer_email', ]; public function products() { return $this->belongsToMany(Product::class)->withPivot(['quantity']); } }
As you can see, we add withPivot([‘quantity’]) to be able to manipulate that field easier with Eloquent, will show that later.
Now, if we run these migrations and then reverse engineer the database with MySQL Workbench, we have this structure:
Create Order Form
Now it’s time to build a form to create order, here’s the screenshot again:
First, Controller method with passing available products from the database.
app/Http/Controllers/Admin/OrdersController.php:
public function create() { $products = Product::all(); return view('admin.orders.create', compact('products')); }
Simple, right? Now, let’s go to the Blade file.
Notice: This form was actually pre-generated by QuickAdminPanel, but you can build your own manually.
So, we have this in resources/views/admin/orders/create.blade.php. I will intentionally skip the customer name/email fields, and focus only on products:
<form action="{{ route("admin.orders.store") }}" method="POST"> @csrf {{-- ... customer name and email fields --}} <div class="card"> <div class="card-header"> Products </div> <div class="card-body"> <table class="table" id="products_table"> <thead> <tr> <th>Product</th> <th>Quantity</th> </tr> </thead> <tbody> <tr id="product0"> <td> <select name="products[]" class="form-control"> <option value="">-- choose product --</option> @foreach ($products as $product) <option value="{{ $product->id }}"> {{ $product->name }} (${{ number_format($product->price, 2) }}) </option> @endforeach </select> </td> <td> <input type="number" name="quantities[]" class="form-control" value="1" /> </td> </tr> <tr id="product1"></tr> </tbody> </table> <div class="row"> <div class="col-md-12"> <button id="add_row" class="btn btn-default pull-left">+ Add Row</button> <button id='delete_row' class="pull-right btn btn-danger">- Delete Row</button> </div> </div> </div> </div> <div> <input class="btn btn-danger" type="submit" value="{{ trans('global.save') }}"> </div> </form>
So, we’re building a table of products here, and to make it dynamic we use approach I’ve found in this Bootstrap Snippet:
- We prefill the first row of the table with all the fields
- We also create new empty row which is invisible to user
- Button “Add row” will then duplicate last row of the table and create one more empty invisible row
- Button “Delete row” will then delete last row of the table
- Every row’s input variables will be array, so for POST result we will have array of products[] and quantities[]
Here’s the jQuery part of this – put it in wherever you have your JavaScript section:
$(document).ready(function(){ let row_number = 1; $("#add_row").click(function(e){ e.preventDefault(); let new_row_number = row_number - 1; $('#product' + row_number).html($('#product' + new_row_number).html()).find('td:first-child'); $('#products_table').append('<tr id="product' + (row_number + 1) + '"></tr>'); row_number++; }); $("#delete_row").click(function(e){ e.preventDefault(); if(row_number > 1){ $("#product" + (row_number - 1)).html(''); row_number--; } }); });
Saving Order and Products Data
Let’s get back to our app/Http/Controllers/Admin/OrdersController.php, its store() method will look like this:
public function store(StoreOrderRequest $request) { $order = Order::create($request->all()); $products = $request->input('products', []); $quantities = $request->input('quantities', []); for ($product=0; $product < count($products); $product++) { if ($products[$product] != '') { $order->products()->attach($products[$product], ['quantity' => $quantities[$product]]); } } return redirect()->route('admin.orders.index'); }
What do we have here?
- First, we save the order’s main data into $order;
- Then, we take products/quantities array, putting empty arrays as values in case they are missing;
- We iterate through products and if product ID is not empty we use the attach() method of many-to-many relationships, adding a value of quantity as extra parameter (read more about many-to-many in my popular article).
And, that’s it with the data saving, order with products is saved in the database!
View Orders with Products in List
Final thing in this article – we have a list of orders, and one of the columns should be products from that order.
How to list those products?
app/Http/Controllers/Admin/OrdersController.php:
public function index() { $orders = Order::with('products')->get(); return view('admin.orders.index', compact('orders')); }
And then – resources/views/admin/orders/index.blade.php – every row of the table should look like this:
@foreach($orders as $order) <tr data-entry-id="{{ $order->id }}"> <td> {{ $order->id ?? '' }} </td> <td> {{ $order->customer_name ?? '' }} </td> <td> {{ $order->customer_email ?? '' }} </td> <td> <ul> @foreach($order->products as $item) <li>{{ $item->name }} ({{ $item->pivot->quantity }} x ${{ $item->price }})</li> @endforeach </ul> </td> <td> {{-- ... buttons ... --}} </td> </tr> @endforeach
As you can see, we’re using $item->pivot->quantity to get that additional column from pivot table.
And, that’s it with this mini-project!
I’ve put the finished version on Github here: https://github.com/LaravelDaily/Laravel-Master-Detail-Form
There you will find some more functionality, like editing the orders, and full QuickAdminPanel-based login/permissions system.
Try our QuickAdminPanel generator!
35 Comments
Leave a Reply to Egzon Cancel reply
Recent Posts
Try our QuickAdminPanel Generator!
How it works:
1. Generate panel online
No coding required, you just choose menu items.
2. Download code & install locally
Install with simple "composer install" and "php artisan migrate".
3. Customize anything!
We give all the code, so you can change anything after download.
Thank you very much for your tutorials, they have been a great help.
I already tested the application of Master-Detail Form, very timely for the development I am doing.
Mr. Povilas, if you allow me, what I do not agree with is that when an Order is removed, it also removes the Product, because that Product may be associated with another or new Order or that already exists. In this case, a verification of the integrity of the data could be made, which when the Order is removed, only the Product is disassociated.
If I am in error please tell me.
Congratulations on your great contribution to the Laravel community.
Hi, I don’t remember I ever told that order removal remove the product, too. Your logic is completely correct, it should be only disassociated.
this is very nice, can you share $order sql statment because i don’t know laravel structure.
[…] article: https://blog.quickadminpanel.com/master-detail-form-in-laravel-jquery-create-order-with-products/ Repository: […]
Hi, great tutorial, just what I needed!
Side note: in your article code you are missing some code on .append. This:
$(‘#products_table’).append(”);
should be this (as per your source code):
$(‘#products_table’).append(”);
Oh, I see, the site cuts it out 🙂
Good catch, didn’t notice that one! Fixed the article.
I did able to successfully implement this, my only problem is that in the detail which I use select2 plugin, the plugin is not working. Do you know how to make it function or implement a search functionality in the product field for those with may product?
can you solve this? i have the same issue
Hi Jun
If you could implement it with select2, could you share. I need to do the same.
Thanks
Thanks for the great tutorial. I have question for this case, can I implement laravel server side validation for each row field?
Hello ! great tutorial, really gave me a huge insight, but there is an error that always appears for no reason it says “Property [produits] does not exist on this collection instance.” whenever I try to echo the information on the view, even though I made sure the index controller does indeed call for the products as you did,i even check the phpmyadmin database and the information does indeed go through the product_id and quantity,
do you have any idea what the reason could be ? is there another way to call for the information with the controller?
Hard to say, maybe you’re missing hasMany(‘products’) in your Model?
Also i have that error
“Property [produits] does not exist on this collection instance.”
I tried to put also hasMany in order model but doesn’t work
check for typo since you are having one here “produits” did you mean “products” or “productIds”
After long search finally found someone with the EXACT same problem. Thanks for sharing!
However i solved it in a little different way, i had the problem parsing data from the table in my controller, so i have the same structure in the front-end as you, but in the back-end i simply used array_combine($products, $quantities) and with that i can loop throught products as below:
foreach($products as $productId => $amount){
}
What do you think of this?
Hello,
It is a great tutorial. I have learnt many things from you. Can you show us how to display all Products when the Blade form returns from the error from Controller?
I have a problem when the validation error in Controller and it returns from the Controller, all Product inputs are lost.
Here’s the Pull Request for the demo repository that solves your problem: https://github.com/LaravelDaily/Laravel-Master-Detail-Form/pull/1
Thank you for your reply.
Thanks a lot. This is the explanation that I need.
What will happen to actual orders if the product price changes?
Hello
If I need to show the product price in a separate input box ( not in select).
How do you manage the function in jquery when it selects any product an fill the input with the corresponding price?
I’ve tried, but only works for first row, when second row is added, doesn’t work anymore.
Thanks!
Hello i use order(master) order_items(details) and in order_items i have product_id (relation from products) and quantity then i have pivot with order_id and order_item_id, I can’t understand what changes I have to make to your tutorial code to make it work in my situation, please note that I’m a beginner with larevel and programming your help would be greatly appreciated, thank you. (I use QAP for develop my software)
Maybe I almost managed to solve, in this case I have to save the OrderItems in the Order controller as well:
public function store(StoreOrderRequest $request)
{
$order = Order::create($request->all());
$products = $request->input(‘products’, []);
$quantities = $request->input(‘quantities’, []);
for ($product=0; $product $products[$product],
‘quantity’ => $quantities[$product]
]);
$order->order_items()->attach($order_item->id);
}
}
return redirect()->route(‘admin.orders.index’);
}
now it misses to fix js to get add row to work properly.
I get an Exception erro like below on the master details form submission
ErrorException
Array to string conversion
can some body help me out please …
did you solve this?
Hi, this tutorial has been very instrumental for me to learn many things in the way i can have my form work in master detail, although i have tried the same on my own and am getting an error when trying to add a row, the main order record is saved, but the products i get ErrorException
Undefined offset: 0
http://127.0.0.1:8000/transactions/store
my products are not saved, and the row is not added, i have done a dd() and i get to see the array has detail on it, but am not sure why am getting this error, kindly assist
Hello
I have been trying to work with the edit form but I am not able to retain old values. Is there any example
Excelente! Mas tenho uma duvida, como ficaria essa ordem caso o produto tivesse produtoas adicionais ?
Good Day,
No matter what I try.. I still get an error
: Undefined array key 2
please help
Hi Povilas, is this features already built-in in Quickadminpanel.com multi-project plan ?
thank you
Hi,
I have replied to you in an email. This is not a feature of QuickAdminPanel, it’s an article on how to achieve it *manually*.
Thank you thank you very much Povilas, this is very good articles and very useful in real business practise, combine with Quickadminpanel, development time cuts sharply
Good Article
I am unable to login even i change Password by using
UPDATE users SET password = PASSWORD(“123”);
Thanks …. very good tutorial …. I was able to adapt it in my project and its works great … but in the edit section when the data was loaded in the view when I click in add row button always add the first colums and now in this case is not adding an empty row is adding the first row with data… please help