Laravel E-Commerce Application Development – Products Section Part 4

Laravel E-Commerce Application Development – Products Section Part 4

Laravel E-Commerce Application Development ( 27 Lessons )

In this course, you’ll learn how to create an E-Commerce Website from scratch in Laravel. The process has never been easier I’ll take you from the very beginning stages of setting up Laravel till the last steps of adding products to the cart. If you’ve good understanding & experience in PHP & MySQL then this course is for you.

see full series
  1. Laravel E-Commerce Application Development – Introduction
  2. Laravel E-Commerce Application Development – Initial Project Setup
  3. Laravel E-Commerce Application Development – Assets Setup Using Laravel Mix
  4. Laravel E-Commerce Application Development – Admin Model and Migration
  5. Laravel E-Commerce Application Development – Backend Admin Authentication
  6. Laravel E-Commerce Application Development – Base Controller and Repository
  7. Laravel E-Commerce Application Development – Settings Section Part 1
  8. Laravel E-Commerce Application Development – Settings Section Part 2
  9. Laravel E-Commerce Application Development – Categories Section Part 1
  10. Laravel E-Commerce Application Development – Categories Section Part 2
  11. Laravel E-Commerce Application Development – Attributes Section Part 1
  12. Laravel E-Commerce Application Development – Attributes Section Part 2
  13. Laravel E-Commerce Application Development – Attributes Section Part 3
  14. Laravel E-Commerce Application Development – Brands Section
  15. Laravel E-Commerce Application Development – Products Section Part 1
  16. Laravel E-Commerce Application Development – Products Section Part 2
  17. Laravel E-Commerce Application Development – Products Section Part 3
  18. Laravel E-Commerce Application Development – Products Section Part 4
  19. Laravel E-Commerce Application Development – Frontend Login & Registration
  20. Laravel E-Commerce Application Development – Categories Navigation
  21. Laravel E-Commerce Application Development – Catalog Listing
  22. Laravel E-Commerce Application Development – Product Details Page
  23. Laravel E-Commerce Application Development – Shopping Cart
  24. Laravel E-Commerce Application Development – Checkout
  25. Laravel E-Commerce Application Development – Payment Processing
  26. Laravel E-Commerce Application Development – Order Management
  27. Laravel E-Commerce Application Development – Wrap Up

This is part 17 of the Laravel E-Commerce Application Development series. In this part, we will add the product attributes tab to our product edit view.

We will be creating a VueJS component to add the product attributes to the product. There are many ways to handle the product attribute, I will be keeping it simple and use the very basic techniques.

Here is a preview of what we will building today.

Product Attributes
Product Attributes
You will need some good understanding of VueJS to complete this section. I will be skipping some common steps which I assume you will understand.

Some Fixes to Database

Before starting the VueJS component, there is a database fix which I would like you to do. In the previous post about Product Management, we create a pivot table to join the attribute values to product attribute. I want you to delete this table. For that, we will create a new migration for this.

php artisan make:migration drop_attribute_value_product_attribute_table

Once Laravel generates the migration file, update with the below one.

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class DropAttributeValueProductAttributeTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::dropIfExists('attribute_value_product_attribute');
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        //
    }
}

Next, I would like to add some extra fields to product_attributes table, for that run below command to generate a new migration file.

php artisan make:migration alter_product_attributes_table

Open the newly generated migration file and update with the below one.

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class AlterProductAttributesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('product_attributes', function (Blueprint $table) {

            $table->unsignedInteger('attribute_id')->after('id');
            $table->foreign('attribute_id')->references('id')->on('attributes');

            $table->string('value')->after('attribute_id');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::create('product_attributes', function (Blueprint $table) {
            $table->dropColumn('value');
        });
    }
}

In this table we added two extra columns named attribute_id and value.

Now run the below command to make the changes to your database.

php artisan migrate

Now the pivot table should be deleted from your database and you will also find two extra columns in your product_attributes table.

One more change which I would like to make is modifying the relationship for product attributes. Open the ProductAttribute model class and update the attributes() with the below one.

/**
 * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
 */
public function attribute()
{
    return $this->belongsTo(Attribute::class);
}

Also add the attribute_id and value to the $fillable array.

Adding Product Attributes Tab

First thing first, we will add a product tab in our edit.blade.php file same as we did for the images section.

Open the resources/views/admin/products/edit.blade.php file and add the below HTML markup in it.

<li class="nav-item"><a class="nav-link" href="#attributes" data-toggle="tab">Attributes</a></li>

Add this right after the image’s link.

Then add the below new attributes section right after the images div.

<div class="tab-pane" id="attributes">
    <product-attributes :productid="{{ $product->id }}"></product-attributes>
</div>

As you have seen, we added a product-attributes tag which will be our Vue component. Next, we will add the app.js file to this view.

Add below in the scripts section.

<script type="text/javascript" src="{{ asset('backend/js/app.js') }}"></script>

Adding Required Routes

In this section, we will create all the routes for our product attributes section.

Open the admin.php routes file and add below routes in the products group.

// Load attributes on the page load
Route::get('attributes/load', 'Admin\ProductAttributeController@loadAttributes');
// Load product attributes on the page load
Route::post('attributes', 'Admin\ProductAttributeController@productAttributes');
// Load option values for a attribute
Route::post('attributes/values', 'Admin\ProductAttributeController@loadValues');
// Add product attribute to the current product
Route::post('attributes/add', 'Admin\ProductAttributeController@addAttribute');
// Delete product attribute from the current product
Route::post('attributes/delete', 'Admin\ProductAttributeController@deleteAttribute');

I have added the comments to all routes, so you will understand the functionality of each route.

Creating Product Attribute Controller

Our all routes for product attributes are pointing to ProductAttributeController controller, so let’s create it.

php artisan make:controller Admin\ProductAttributeController

Once you have the controller generated, open it and update it with the below one.

namespace App\Http\Controllers\Admin;

use App\Models\Product;
use App\Models\Attribute;
use Illuminate\Http\Request;
use App\Models\ProductAttribute;
use App\Http\Controllers\Controller;

class ProductAttributeController extends Controller
{
    /**
     * @return \Illuminate\Http\JsonResponse
     */
    public function loadAttributes()
    {
        $attributes = Attribute::all();

        return response()->json($attributes);
    }

    /**
     * @param Request $request
     * @return \Illuminate\Http\JsonResponse
     */
    public function productAttributes(Request $request)
    {
        $product = Product::findOrFail($request->id);

        return response()->json($product->attributes);
    }

    /**
     * @param Request $request
     * @return \Illuminate\Http\JsonResponse
     */
    public function loadValues(Request $request)
    {
        $attribute = Attribute::findOrFail($request->id);

        return response()->json($attribute->values);
    }

    /**
     * @param Request $request
     * @return \Illuminate\Http\JsonResponse
     */
    public function addAttribute(Request $request)
    {
        $productAttribute = ProductAttribute::create($request->data);

        if ($productAttribute) {
            return response()->json(['message' => 'Product attribute added successfully.']);
        } else {
            return response()->json(['message' => 'Something went wrong while submitting product attribute.']);
        }
    }

    /**
     * @param Request $request
     * @return \Illuminate\Http\JsonResponse
     */
    public function deleteAttribute(Request $request)
    {
        $productAttribute = ProductAttribute::findOrFail($request->id);
        $productAttribute->delete();

        return response()->json(['status' => 'success', 'message' => 'Product attribute deleted successfully.']);
    }
}

I have added all the required methods for our routes, you can go through as they contain very basic functionality implementation.

Creating VueJs Component and Adding to app.js File

Now, we move on to the VueJs and create a new component. Open the resources/js/app.js file and add the new Vue component in it.

Vue.component('product-attributes', require('./components/ProductAttributes').default);

Next, we will create the ProductAttributes.vue file in components folder.

Compiling JS

Open the terminal and run npm run watch command to compile and watch your file.

Now, the first thing on our attribute tabs, we would like to show all the product attributes attached for the current product.For that the resources/js/components/ProductAttributes.vue file and add the below markup in it.

<template>
    <div>
        <div class="tile">
            <h3 class="tile-title">Product Attributes</h3>
            <div class="tile-body">
                <div class="table-responsive">
                    <table class="table table-sm">
                        <thead>
                        <tr class="text-center">
                            <th>Value</th>
                            <th>Qty</th>
                            <th>Price</th>
                            <th>Action</th>
                        </tr>
                        </thead>
                        <tbody>
                        <tr v-for="pa in productAttributes">
                            <td style="width: 25%" class="text-center">{{ pa.value}}</td>
                            <td style="width: 25%" class="text-center">{{ pa.quantity}}</td>
                            <td style="width: 25%" class="text-center">{{ pa.price}}</td>
                            <td style="width: 25%" class="text-center">
                                <button class="btn btn-sm btn-danger" @click="deleteProductAttribute(pa)">
                                    <i class="fa fa-trash"></i>
                                </button>
                            </td>
                        </tr>
                        </tbody>
                    </table>
                </div>
            </div>
        </div>
    </div>
</template>

<script>
    export default {
        name: "product-attributes",
        props: ['productid'],
        data() {
            return {
                productAttributes: [],
            }
        },
        created: function() {
            this.loadProductAttributes(this.productid);
        },
        methods: {
            loadProductAttributes(id) {
                let _this = this;
                axios.post('/admin/products/attributes', {
                    id: id
                }).then (function(response){
                    _this.productAttributes = response.data;
                }).catch(function (error) {
                    console.log(error);
                });
            },
        }
    }
</script>

In the above code example, we are adding a table to show the product attributes. In the javascript section, we firstly naming our Vue component and setting the props for our productid.

Next, we have define the productAttributes in our data method and set it to an empty array. Next, in the created lifecycle hook of VueJS we are calling the loadProductAttributes() method which will load all the product attributes for our current product.

In the methods, we create the productAttributes() method which is making an Axios call to our route which we created earlier. When we get the response, we simply set the data variable productAttributes to whatever response data we are getting from the backend.

In the table, we are iterating through the productAttributes variable and showing the product attributes.

Next, I would like to create a dropdown to list all attributes in it. For that, we will add the below HTML markup just before the product attributes table.

<div class="tile">
    <h3 class="tile-title">Attributes</h3>
    <hr>
    <div class="tile-body">
        <div class="row">
            <div class="col-md-4">
                <div class="form-group">
                    <label for="parent">Select an Attribute <span class="m-l-5 text-danger"> *</span></label>
                    <select id=parent class="form-control custom-select mt-15" v-model="attribute" @change="selectAttribute(attribute)">
                        <option :value="attribute" v-for="attribute in attributes"> {{ attribute.name }} </option>
                    </select>
                </div>
            </div>
        </div>
    </div>
</div>

For this we have to create another attributes variable and load all the attributes in it. Update the data() method with the below one.

 data() {
return {
    productAttributes: [],
    attributes: [],
    attribute: {},
},

Next, we will add a new method in the methods section of this component.

loadAttributes() {
    let _this = this;
    axios.get('/admin/products/attributes/load').then (function(response){
        _this.attributes = response.data;
    }).catch(function (error) {
        console.log(error);
    });
},

Above method is making a call to backend to get all the attributes, then we set the attributes data variable to the response data. The attribute variable is binded with the select dropdown using the v-model directive.

Next, we will call this method in the created hook of the VueJs component.

created: function() {
    this.loadAttributes();
    this.loadProductAttributes(this.productid);
},

Now, when we have the attributes dropdown loaded with the attribute, we want to load the attribute values which will be done by the selectAttribute() method. Update your date() block of VueJs with the below one.

data() {
    return {
        productAttributes: [],
        attributes: [],
        attribute: {},
        attributeSelected: false,
        attributeValues: [],
        value: {},
        valueSelected: false,
        currentAttributeId: '',
        currentValue: '',
        currentQty: '',
        currentPrice: '',
    }
},

Now add the below methods to the methods block of your file.

selectAttribute(attribute) {
    let _this = this;
    this.currentAttributeId = attribute.id;
    axios.post('/admin/products/attributes/values', {
        id: attribute.id
    }).then (function(response){
        _this.attributeValues = response.data;
    }).catch(function (error) {
        console.log(error);
    });
    this.attributeSelected = true;
},
selectValue(value) {
    this.valueSelected = true;
    this.currentValue = value.value;
    this.currentQty = value.quantity;
    this.currentPrice = value.price;
},
addProductAttribute() {
    if (this.currentQty === null || this.currentPrice === null) {
        this.$swal("Error, Some values are missing.", {
            icon: "error",
        });
    } else {
        let _this = this;
        let data = {
            attribute_id: this.currentAttributeId,
            value:  this.currentValue,
            quantity: this.currentQty,
            price: this.currentPrice,
            product_id: this.productid,
        };

        axios.post('/admin/products/attributes/add', {
            data: data
        }).then (function(response){
            _this.$swal("Success! " + response.data.message, {
                icon: "success",
            });
            _this.currentValue = '';
            _this.currentQty = '';
            _this.currentPrice = '';
            _this.valueSelected = false;
        }).catch(function (error) {
            console.log(error);
        });
        this.loadProductAttributes(this.productid);
    }
},
deleteProductAttribute(pa) {
    let _this = this;
    this.$swal({
        title: "Are you sure?",
        text: "Once deleted, you will not be able to recover this data!",
        icon: "warning",
        buttons: true,
        dangerMode: true,
    }).then((willDelete) => {
        if (willDelete) {
            console.log(pa.id);
            axios.post('/admin/products/attributes/delete', {
                id: pa.id,
            }).then (function(response){
                if (response.data.status === 'success') {
                    _this.$swal("Success! Product attribute has been deleted!", {
                        icon: "success",
                    });
                    this.loadProductAttributes(this.productid);
                } else {
                    _this.$swal("Your Product attribute not deleted!");
                }
            }).catch(function (error) {
                console.log(error);
            });
        } else {
            this.$swal("Action cancelled!");
        }
    });
}

Now, add the below markup between the attributes dropdown block and the product attributes table.

<div class="tile" v-if="attributeSelected">
    <h3 class="tile-title">Add Attributes To Product</h3>
    <div class="row">
        <div class="col-md-4">
            <div class="form-group">
                <label for="values">Select an value <span class="m-l-5 text-danger"> *</span></label>
                <select id=values class="form-control custom-select mt-15" v-model="value" @change="selectValue(value)">
                    <option :value="value" v-for="value in attributeValues"> {{ value.value }} </option>
                </select>
            </div>
        </div>
    </div>
    <div class="row" v-if="valueSelected">
        <div class="col-md-4">
            <div class="form-group">
                <label class="control-label" for="quantity">Quantity</label>
                <input class="form-control" type="number" id="quantity" v-model="currentQty"/>
            </div>
        </div>
        <div class="col-md-4">
            <div class="form-group">
                <label class="control-label" for="price">Price</label>
                <input class="form-control" type="text" id="price" v-model="currentPrice"/>
                <small class="text-danger">This price will be added to the main price of product on frontend.</small>
            </div>
        </div>
        <div class="col-md-12">
            <button class="btn btn-sm btn-primary" @click="addProductAttribute()">
                <i class="fa fa-plus"></i> Add
            </button>
        </div>
    </div>
</div>

In the above JS code and markup, we have another dropdown which shows the attribute values. When we select the value from the dropdown we are calling the selectValue() method which set the current option values data to the temporary variables.

Next, we have another block of markup which we are showing only when a value has been selected from the dropdown based on the valueSelected variable.

In this block, we have two input boxes which are binded with the currentQty and currentPrice. Next we have a button to add the value to product attribute table and on click its triggering the addProductAttribute() method.

Here is the full Vue component content.

<template>
    <div>
        <div class="tile">
            <h3 class="tile-title">Attributes</h3>
            <hr>
            <div class="tile-body">
                <div class="row">
                    <div class="col-md-4">
                        <div class="form-group">
                            <label for="parent">Select an Attribute <span class="m-l-5 text-danger"> *</span></label>
                            <select id=parent class="form-control custom-select mt-15" v-model="attribute" @change="selectAttribute(attribute)">
                                <option :value="attribute" v-for="attribute in attributes"> {{ attribute.name }} </option>
                            </select>
                        </div>
                    </div>
                </div>
            </div>
        </div>
        <div class="tile" v-if="attributeSelected">
            <h3 class="tile-title">Add Attributes To Product</h3>
            <div class="row">
                <div class="col-md-4">
                    <div class="form-group">
                        <label for="values">Select an value <span class="m-l-5 text-danger"> *</span></label>
                        <select id=values class="form-control custom-select mt-15" v-model="value" @change="selectValue(value)">
                            <option :value="value" v-for="value in attributeValues"> {{ value.value }} </option>
                        </select>
                    </div>
                </div>
            </div>
            <div class="row" v-if="valueSelected">
                <div class="col-md-4">
                    <div class="form-group">
                        <label class="control-label" for="quantity">Quantity</label>
                        <input class="form-control" type="number" id="quantity" v-model="currentQty"/>
                    </div>
                </div>
                <div class="col-md-4">
                    <div class="form-group">
                        <label class="control-label" for="price">Price</label>
                        <input class="form-control" type="text" id="price" v-model="currentPrice"/>
                        <small class="text-danger">This price will be added to the main price of product on frontend.</small>
                    </div>
                </div>
                <div class="col-md-12">
                    <button class="btn btn-sm btn-primary" @click="addProductAttribute()">
                        <i class="fa fa-plus"></i> Add
                    </button>
                </div>
            </div>
        </div>
        <div class="tile">
            <h3 class="tile-title">Product Attributes</h3>
            <div class="tile-body">
                <div class="table-responsive">
                    <table class="table table-sm">
                        <thead>
                        <tr class="text-center">
                            <th>Value</th>
                            <th>Qty</th>
                            <th>Price</th>
                            <th>Action</th>
                        </tr>
                        </thead>
                        <tbody>
                        <tr v-for="pa in productAttributes">
                            <td style="width: 25%" class="text-center">{{ pa.value}}</td>
                            <td style="width: 25%" class="text-center">{{ pa.quantity}}</td>
                            <td style="width: 25%" class="text-center">{{ pa.price}}</td>
                            <td style="width: 25%" class="text-center">
                                <button class="btn btn-sm btn-danger" @click="deleteProductAttribute(pa)">
                                    <i class="fa fa-trash"></i>
                                </button>
                            </td>
                        </tr>
                        </tbody>
                    </table>
                </div>
            </div>
        </div>
    </div>
</template>

<script>
    export default {
        name: "product-attributes",
        props: ['productid'],
        data() {
            return {
                productAttributes: [],
                attributes: [],
                attribute: {},
                attributeSelected: false,
                attributeValues: [],
                value: {},
                valueSelected: false,
                currentAttributeId: '',
                currentValue: '',
                currentQty: '',
                currentPrice: '',
            }
        },
        created: function() {
            this.loadAttributes();
            this.loadProductAttributes(this.productid);
        },
        methods: {
            loadAttributes() {
                let _this = this;
                axios.get('/admin/products/attributes/load').then (function(response){
                    _this.attributes = response.data;
                }).catch(function (error) {
                    console.log(error);
                });
            },
            loadProductAttributes(id) {
                let _this = this;
                axios.post('/admin/products/attributes', {
                    id: id
                }).then (function(response){
                    _this.productAttributes = response.data;
                }).catch(function (error) {
                    console.log(error);
                });
            },
            selectAttribute(attribute) {
                let _this = this;
                this.currentAttributeId = attribute.id;
                axios.post('/admin/products/attributes/values', {
                    id: attribute.id
                }).then (function(response){
                    _this.attributeValues = response.data;
                }).catch(function (error) {
                    console.log(error);
                });
                this.attributeSelected = true;
            },
            selectValue(value) {
                this.valueSelected = true;
                this.currentValue = value.value;
                this.currentQty = value.quantity;
                this.currentPrice = value.price;
            },
            addProductAttribute() {
                if (this.currentQty === null || this.currentPrice === null) {
                    this.$swal("Error, Some values are missing.", {
                        icon: "error",
                    });
                } else {
                    let _this = this;
                    let data = {
                        attribute_id: this.currentAttributeId,
                        value:  this.currentValue,
                        quantity: this.currentQty,
                        price: this.currentPrice,
                        product_id: this.productid,
                    };

                    axios.post('/admin/products/attributes/add', {
                        data: data
                    }).then (function(response){
                        _this.$swal("Success! " + response.data.message, {
                            icon: "success",
                        });
                        _this.currentValue = '';
                        _this.currentQty = '';
                        _this.currentPrice = '';
                        _this.valueSelected = false;
                    }).catch(function (error) {
                        console.log(error);
                    });
                    this.loadProductAttributes(this.productid);
                }
            },
            deleteProductAttribute(pa) {
                let _this = this;
                this.$swal({
                    title: "Are you sure?",
                    text: "Once deleted, you will not be able to recover this data!",
                    icon: "warning",
                    buttons: true,
                    dangerMode: true,
                }).then((willDelete) => {
                    if (willDelete) {
                        console.log(pa.id);
                        axios.post('/admin/products/attributes/delete', {
                            id: pa.id,
                        }).then (function(response){
                            if (response.data.status === 'success') {
                                _this.$swal("Success! Product attribute has been deleted!", {
                                    icon: "success",
                                });
                                this.loadProductAttributes(this.productid);
                            } else {
                                _this.$swal("Your Product attribute not deleted!");
                            }
                        }).catch(function (error) {
                            console.log(error);
                        });
                    } else {
                        this.$swal("Action cancelled!");
                    }
                });

            }
        }
    }
</script>

What’s Next

In this post, we completed the Products Section.

In the next post, we will start working on frontend and start populating data on the frontend.

Code Repository

You can find the code base of this series on Laravel eCommerce Application repository.

If you have any question about this post, please leave a comment in the comment box below.

Your little help will keep this site alive and help us to produce quality content for you.