We have in the previous part that we displayed the categories in Nuxtjs admin panel, in this part we will handle the brands CRUD in Lumen and in the later on we will display it in the admin panel.
As we did with the categories CRUD in lumen let’s create another controller for our product brands, this controller will be so easy, just go to the lumen project and create BrandsController.php in app/Http/Controllers/ and add the below code:
<?php namespace App\Http\Controllers; use App\Models\Brand; use Illuminate\Http\Request; use Illuminate\Support\Facades\Validator; class BrandsController extends Controller { public function __construct() { $this->middleware('super_admin_check:store-update-destroy'); } public function index(Request $request) { $brands = $this->filterAndResponse($request); return response()->json(['brands' => $brands], 200); } public function store(Request $request) { $validator = Validator::make($request->only('title'), [ 'title' => 'required' ]); if ($validator->fails()) { return response()->json(['success' => 0, 'message' => 'Please fix these errors', 'errors' => $validator->errors()], 500); } $brand = Brand::create($request->all()); return response()->json(['success' => 1, 'message' => 'Created successfully', 'brand' => $brand], 201); } public function show($id) { $brand = Brand::findOrFail($id); return response()->json(['brand' => $brand], 200); } public function update(Request $request, $id) { $brand = Brand::findOrFail($id); $validator = Validator::make($request->only('title'), [ 'title' => 'required' ]); if ($validator->fails()) { return response()->json(['success' => 0, 'message' => 'Please fix these errors', 'errors' => $validator->errors()], 500); } $brand->title = $request->input('title'); $brand->save(); return response()->json(['success' => 1, 'message' => 'Updated successfully', 'brand' => $brand], 200); } public function destroy($id) { $brand = Brand::findOrFail($id); $brand->delete(); return response()->json(['success' => 1, 'message' => 'Deleted successfully'], 200); } /** * @param Request $request */ protected function filterAndResponse(Request $request) { $query = Brand::whereRaw("1=1"); if($request->has('all')) { return $query->get(); } if ($request->id) { $query->where('id', $request->id); } if ($request->title) { $query->where('title', 'like', "%" . $request->title . "%"); } $brands = $query->paginate(10); return $brands; } }
As you see the BrandsController is the same as the CategoriesController so i don’t go into the explanation of it and i will add the routes for this controller in routes/web.php
routes/web.php
<?php $router->get('/', function () use ($router) { return $router->app->version(); }); $router->group(['prefix' => 'api'], function () use ($router) { $router->post('/login', 'Auth\\LoginController@login'); $router->post('/register', 'Auth\\RegisterController@register'); $router->group(['prefix' => 'category'], function () use ($router) { $router->get('/', 'CategoriesController@index'); $router->get('/htmltree', 'CategoriesController@getCategoryHtmlTree'); $router->get('/{id}', 'CategoriesController@show'); }); $router->group(['prefix' => 'brand'], function () use ($router) { $router->get('/', 'BrandsController@index'); $router->get('/{id}', 'BrandsController@show'); }); $router->group(['middleware' => 'auth:api'], function () use ($router) { $router->get('/me', 'Auth\\LoginController@userDetails'); $router->get('/logout', 'Auth\\LoginController@logout'); $router->get('/check-login', 'Auth\\LoginController@checkLogin'); $router->group(['prefix' => 'category'], function () use ($router) { $router->post('/', 'CategoriesController@store'); $router->put('/{id}', 'CategoriesController@update'); $router->delete('/{id}', 'CategoriesController@destroy'); }); $router->group(['prefix' => 'brand'], function () use ($router) { $router->post('/', 'BrandsController@store'); $router->put('/{id}', 'BrandsController@update'); $router->delete('/{id}', 'BrandsController@destroy'); }); }); });
Displaying Brands In Nuxt Admin Panel
Now go to the Nuxtjs project in the admin/ folder, we will start by creating the brand services in admin/api/ folder.
admin/api/brand.js
const BrandApi = { create: (axios, payload) => { return axios.$post('/api/brand', payload); }, list: (axios, payload = null) => { let payload_arr = []; if(payload) { for(let key in payload) { payload_arr.push(key +"=" + payload[key]); } } return axios.$get('/api/brand?' + payload_arr.join("&")); }, getAll: (axios) => { return axios.$get('/api/brand?all=1'); }, delete: (axios, id) => { return axios.$delete('/api/brand/' + id); }, show: (axios, id) => { return axios.$get('/api/brand/' + id); }, update(axios, payload, id) { return axios.$put('/api/brand/' + id, payload); } }; export {BrandApi};
This is the basic services for the CRUD operations needed to manipulate brands. Next we need to create a store for our brands.
In the admin/store directory create brand.js with the following code snippet:
import { BrandApi } from '../api/brand'; export const state = () => ({ brand: { title: '', id: '' }, filterData: { id: '', title: '', }, page: 1, brandList: {}, allBrands: [] // used to fill brand dropdown in product create screen }); export const mutations = { setTitle(state, title) { state.brand.title = title; }, setId(state, id) { state.brand.id = id; }, resetBrand(state) { state.brand = { title: '', id: '' }; }, setBrandList(state, data) { state.brandList = data; }, setPage(state, page) { state.page = page; }, setFilterData(state, value) { state.filterData[value.key] = value.val; }, setAllBrands(state, value) { state.allBrands = value; } }; export const actions = { create({commit, dispatch, state}, options) { }, listBrands({commit}, filterData = null) { }, getAllBrands({commit}) { }, deleteBrand({commit, state}, id) { }, showBrand({commit}, payload) { }, updateBrand({commit, dispatch, state}, options) { } };
This store is so simple you may notice that i added brandList and allBrands. These two keys are different the first used to store brand list to be displayed in the index page and the second used in dropdowns in pages like create and edit product, we will return back to the store but now let’s create the pages like index, add, edit.
In this case i will create only one page which is the index page, for the create and update pages i will do this in bootstrap modal because the brand have only one field, the title.
Inside admin/pages directory create brand/ folder and add index.vue page.
/admin/pages/brand/index.vue
<template> <div class="main-content"> <div class="section__content section__content--p30"> <div class="container-fluid"> <div class="row"> <div class="col-md-12"> <loader></loader> <status-messages></status-messages> <BrandFilter v-on:Filtering="handleFiltering" v-on:createBrand="createBrand"></BrandFilter> </div> <div class="col-md-12"> <!-- DATA TABLE--> <div class="table-responsive m-b-40"> <table class="table table-borderless table-data3"> <thead> <tr> <th>#</th> <th>Title</th> <th>Options</th> </tr> </thead> <tbody> <BrandRow v-if="brandList.data" v-for="brand of brandList.data" v-bind:brand="brand" v-bind:key="brand.id" v-on:removeBrand="removeBrand" v-on:editBrand="editBrand"></BrandRow> </tbody> </table> </div> <Pagination :data="brandList" v-if="brandList.data" v-on:handlePagination="handlePagination"></Pagination> </div> </div> </div> </div> <FormModal></FormModal> </div> </template> <script> import Loader from "../../components/helpers/loader"; import statusMessages from "../../components/helpers/statusMessages"; import BrandRow from "../../components/brand-components/BrandRow"; import Pagination from "../../components/helpers/Pagination"; import BrandFilter from "../../components/brand-components/BrandFilter"; import FormModal from "../../components/brand-components/FormModal"; export default { name: "index", middleware: "auth", components: { FormModal, BrandFilter, Pagination, BrandRow, Loader, statusMessages }, fetch() { this.$store.dispatch('brand/listBrands'); }, computed: { brandList() { return this.$store.state.brand.brandList; } }, methods: { handlePagination(page_number) { this.$store.commit('brand/setPage', page_number); this.$store.dispatch('brand/listBrands', this.getPayload()); }, handleFiltering(field, value) { this.$store.commit('brand/setFilterData', {key: field, val: value}); this.$store.commit('brand/setPage', 1); this.$store.dispatch('brand/listBrands', this.getPayload()); }, getPayload() { let payload = {}; for(let field in this.$store.state.brand.filterData) { if(this.$store.state.brand.filterData.hasOwnProperty(field) && this.$store.state.brand.filterData[field] !== '') payload[field] = this.$store.state.brand.filterData[field]; } payload.page = this.$store.state.brand.page; return payload; }, removeBrand(id) { if(confirm("Are you sure?")) { this.$store.dispatch('brand/deleteBrand', id); } }, createBrand() { this.$store.commit('brand/resetBrand'); // some jquery code $(".brand-form-modal").modal("show"); }, editBrand(id) { this.$store.dispatch('brand/showBrand', {id, onSuccess: () => $(".brand-form-modal").modal("show")}); } } } </script> <style scoped> </style>
/admin/components/brand-components/BrandFilter.vue
<template> <form method="get" action="#" style="margin-bottom: 10px"> <h4>Filter</h4> <div class="row"> <div class="col-1"> <input type="text" class="form-control" placeholder="Id" name="id" @change="handleFiltering($event)" :value="this.$store.state.brand.filterData.id" /> </div> <div class="col-3"> <input type="text" class="form-control" placeholder="Title" name="title" @change="handleFiltering($event)" :value="this.$store.state.brand.filterData.title" /> </div> <div class="col-8"> <a href="#" class="btn btn-success pull-right" @click.prevent="createBrand()"><i class="fa fa-plus-square"></i> Create</a> </div> </div> </form> </template> <script> export default { name: "BrandFilter", methods: { handleFiltering(event) { this.$emit('Filtering', event.target.name, event.target.value); }, createBrand() { this.$emit('createBrand'); } } } </script> <style scoped> </style>
/admin/components/brand-components/BrandRow.vue
<template> <tr> <td>{{ this.brand.id }}</td> <td>{{ this.brand.title }}</td> <td> <a href="#" class="btn btn-info btn-sm" @click.prevent="editBrand(brand.id)"><i class="fa fa-edit"></i></a> <a href="#" class="btn btn-danger btn-sm" @click.prevent="removeBrand(brand.id)"><i class="fa fa-remove"></i></a> </td> </tr> </template> <script> export default { name: "BrandRow", props: ['brand'], methods: { editBrand(id) { this.$emit('editBrand', id); }, removeBrand(id) { this.$emit('removeBrand', id); } } } </script> <style scoped> </style>
/admin/components/brand-components/BrandForm.vue
<template> <div class="modal brand-form-modal" tabindex="-1" role="dialog"> <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title">{{ this.$store.state.brand.brand.id == ''?'Create Brand' : 'Edit Brand #' + this.$store.state.brand.brand.id }}</h5> <button type="button" class="close" data-dismiss="modal" aria-label="Close" @click="dismiss()"> <span aria-hidden="true">×</span> </button> </div> <div class="modal-body"> <loader></loader> <status-messages></status-messages> <form method="post" id="brand-form" @submit.prevent="submit()"> <div class="form-group"> <label for="title">Title</label> <input type="text" name="title" id="title" class="form-control" v-model="title" /> </div> </form> </div> <div class="modal-footer"> <button type="submit" form="brand-form" class="btn btn-primary">Save</button> <button type="button" class="btn btn-secondary" data-dismiss="modal" @click="dismiss()">Close</button> </div> </div> </div> </div> </template> <script> import StatusMessages from "../helpers/statusMessages"; import Loader from "../helpers/loader"; export default { name: "FormModal", components: { Loader, StatusMessages }, computed: { title: { set(title) { this.$store.commit('brand/setTitle', title); }, get() { return this.$store.state.brand.brand.title; } } }, methods: { dismiss() { $(".brand-form-modal").modal("hide"); this.$store.commit('brand/resetBrand'); }, submit() { } } } </script> <style scoped> </style>
Update /admin/components/partials/NavMenu.vue
<template> <ul :class="ulclass"> <li :class="{active: this.$route.path === '/'}"> <nuxt-link to="/"> <i class="fas fa-tachometer-alt"></i>Dashboard </nuxt-link> </li> <li :class="{active: this.$route.path.indexOf('category') !== -1}"> <nuxt-link to="/category"> <i class="fas fa-list"></i>Categories </nuxt-link> </li> <li :class="{active: this.$route.path.indexOf('brand') !== -1}"> <nuxt-link to="/brand"> <i class="fas fa-television"></i>Brands </nuxt-link> </li> <li> <a href="#"> <i class="fas fa-shopping-cart"></i>Products </a> </li> <li> <a href="#"> <i class="fas fa-gears"></i>Orders</a> </li> <li> <a href="#"> <i class="fas fa-gears"></i>Pending Orders</a> </li> <li> <a href="#"> <i class="fas fa-users"></i>Users</a> </li> <li> <a href="#"> <i class="fas fa-sign-out-alt"></i>Logout</a> </li> </ul> </template> <script> export default { name: "nav-menu", props: ['ulclass'], mounted() { } } </script> <style scoped> </style>
As you see the index page is pretty similar to the index page for the categories we created in the previous part. The page displays all brands and we use some partial components like <BrandFilter/>, <BrandRow/> and <FormModal/>.The <Pagination /> is a shard component which we used in categories.
The <FormModal /> component is a bootstrap modal to create and update brands, it contains just the title field. Let’s move to handle the various store actions.
Updating Store Actions
In /admin/store/brand.js in update the actions like this:
export const actions = { create({commit, dispatch, state}, options) { commit('shared/resetStatusMessagesParameters', null, {root: true}); commit('shared/setStatusMessageParameter', {key: 'showLoading', val: true}, {root: true}); const payload = { title: state.brand.title }; BrandApi.create(this.$axios, payload).then(response => { commit('shared/setStatusMessageParameter', {key: 'showLoading', val: false}, {root: true}); if(response.success) { commit('shared/setStatusMessageParameter', {key: 'success_message', val: response.message}, {root: true}); options.onSuccess(); dispatch('listBrands'); } }).catch(err => { commit('shared/setStatusMessageParameter', {key: 'showLoading', val: false}, {root: true}); dispatch('handleError', err); }).finally(() => { setTimeout(() => { commit('shared/resetStatusMessagesParameters', null, {root: true}); commit('resetBrand'); }, 3000); }); }, listBrands({commit}, filterData = null) { BrandApi.list(this.$axios, filterData).then(response => { commit('setBrandList', response.brands); }).catch(err => { console.log(err); }); }, getAllBrands({commit}) { BrandApi.getAll(this.$axios).then(response => { commit('setAllBrands', response.brands); }).catch(err => { console.log(err); }); }, deleteBrand({commit, state}, id) { commit('shared/resetStatusMessagesParameters', null, {root: true}); commit('shared/setStatusMessageParameter', {key: 'showLoading', val: true}, {root: true}); BrandApi.delete(this.$axios, id).then(response => { commit('shared/setStatusMessageParameter', {key: 'showLoading', val: false}, {root: true}); if(response.success) { let brandList = {...state.brandList}; brandList.data = brandList.data.filter(item => item.id !== id); commit('setBrandList', brandList); commit('shared/setStatusMessageParameter', {key: 'success_message', val: response.message}, {root: true}); } }).catch(err => { console.log(err); }).finally(() => { setTimeout(() => { commit('shared/resetStatusMessagesParameters', null, {root: true}); }, 3000); }); }, showBrand({commit}, payload) { commit('shared/resetStatusMessagesParameters', null, {root: true}); commit('shared/setStatusMessageParameter', {key: 'showLoading', val: true}, {root: true}); BrandApi.show(this.$axios, payload.id).then(response => { commit('shared/setStatusMessageParameter', {key: 'showLoading', val: false}, {root: true}); if(response.brand) { commit('setTitle', response.brand.title); commit('setId', response.brand.id); payload.onSuccess(); } }).catch(err => { console.log(err); }); }, updateBrand({commit, dispatch, state}, options) { commit('shared/resetStatusMessagesParameters', null, {root: true}); commit('shared/setStatusMessageParameter', {key: 'showLoading', val: true}, {root: true}); const payload = { title: state.brand.title }; BrandApi.update(this.$axios, payload, state.brand.id).then(response => { commit('shared/setStatusMessageParameter', {key: 'showLoading', val: false}, {root: true}); if(response.success) { commit('shared/setStatusMessageParameter', {key: 'success_message', val: response.message}, {root: true}); options.onSuccess(); dispatch('listBrands'); } }).catch(err => { commit('shared/setStatusMessageParameter', {key: 'showLoading', val: false}, {root: true}); dispatch('handleError', err); }).finally(() => { setTimeout(() => { commit('shared/resetStatusMessagesParameters', null, {root: true}); commit('resetBrand'); }, 3000); }); }, handleError({commit}, err) { if(err.response.data) { commit('shared/setStatusMessageParameter', {key: 'error_message', val: err.response.data.message}, {root: true}); if(err.response.data.errors) { let errors = []; for(let key in err.response.data.errors) { errors.push(err.response.data.errors[key][0]); } commit('shared/setStatusMessageParameter', {key: 'validation_errors', val: errors}, {root: true}); } } } };
In the above code the actions for the CRUD operations (Create, Read, Update, Delete). The listBrands() action used to list all brands paginated by calling the list() api and commiting the result to setBrandList mutation. While the getAllBrands() mutation fetch all brands as any array of results of fill dropdown in create and edit product page as we will see in previous tutorials.
The create() and update() actions used when submitting the form to create or update a brand. I follow the same concept we did in the categories actions. on successful creation we dispatch action listBrands() to reload the brands and then we call a callback function onSuccess(), this callback contains code like closing form modal.
Note the handleError() action is like a helper action to display valiation errors, and error messages, this action called by the create() and update() actions.
To complete the puzzle update the FormModal component like shown below:
/admin/components/brand-components/FormModal.vue
<template> <div class="modal brand-form-modal" tabindex="-1" role="dialog"> <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title">{{ this.$store.state.brand.brand.id == ''?'Create Brand' : 'Edit Brand #' + this.$store.state.brand.brand.id }}</h5> <button type="button" class="close" data-dismiss="modal" aria-label="Close" @click="dismiss()"> <span aria-hidden="true">×</span> </button> </div> <div class="modal-body"> <loader></loader> <status-messages></status-messages> <form method="post" id="brand-form" @submit.prevent="submit()"> <div class="form-group"> <label for="title">Title</label> <input type="text" name="title" id="title" class="form-control" v-model="title" /> </div> </form> </div> <div class="modal-footer"> <button type="submit" form="brand-form" class="btn btn-primary">Save</button> <button type="button" class="btn btn-secondary" data-dismiss="modal" @click="dismiss()">Close</button> </div> </div> </div> </div> </template> <script> import StatusMessages from "../helpers/statusMessages"; import Loader from "../helpers/loader"; export default { name: "FormModal", components: { Loader, StatusMessages }, computed: { title: { set(title) { this.$store.commit('brand/setTitle', title); }, get() { return this.$store.state.brand.brand.title; } } }, methods: { dismiss() { $(".brand-form-modal").modal("hide"); this.$store.commit('brand/resetBrand'); }, submit() { if(this.$store.state.brand.brand.id === '') { this.$store.dispatch('brand/create', {onSuccess: () => this.dismiss()}); } else { this.$store.dispatch('brand/updateBrand', {onSuccess: () => this.dismiss()}); } } } } </script> <style scoped> </style>
Here in submit() function we check if the id is empty then we decide if this a create or update operation. On calling dismiss() we close the modal and reset the brand object by calling this.$store,commit(‘brand/resetBrand’).
To see this in action run
npm run dev
And go to the brands page and add some brands.
Continue to Part7: Products CRUD