Backend DevelopmentFrontend DevelopmentVueJs Tutorials

Building Ecommerce Website With Lumen Laravel And Nuxtjs 5: Admin Categories Display

Building Ecommerce Website With PHP Lumen Laravel And Nuxtjs

We will complete the last part by displaying the categories in Nuxtjs admin panel, and connecting this with the web services we have built in Lumen.

 

 

Using the skills we have learned about Vuejs and Nuxtjs we will build the required pages, components and layout to be able to display our categories. We will use axios as our http wrapper, of course nuxt comes with @nuxt/axios module already injected in every component, we already used this when we implemented the JWT authentication in part2.

 

Preparing Pages

Let’s prepare the pages need for the categories module, this will also apply for the other modules we will implement in future parts.

Be sure to always run “run npm dev” in the root Nuxtjs project.

Then go to admin/pages/ and create category/ directory and inside this directory create these files:

  • index.vue
  • create.vue
  • _edit.vue

Then i have every page with these snippets as shown below:

admin/pages/category/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>

            <CategoryFilter></CategoryFilter>

          </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>Parent</th>
                    <th>Featured</th>
                    <th>Options</th>
                  </tr>
                </thead>
                <tbody>
                  <CategoryRow></CategoryRow>
                </tbody>
              </table>
            </div>
            <Pagination></Pagination>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
    import Loader from "../../components/helpers/loader";
    import statusMessages from "../../components/helpers/statusMessages";
    import CategoryRow from "../../components/category-components/Row";
    import Pagination from "../../components/helpers/Pagination";
    import CategoryFilter from "../../components/category-components/CategoryFilter";

    export default {
        name: "index",
        middleware: "auth",
        components: {
          CategoryFilter,
          Pagination,
          CategoryRow,
          Loader,
          statusMessages
        },
        computed: {
        },
        methods: {
          
        }
    }
</script>

<style scoped>

</style>

admin/pages/category/create.vue

<template>
  <div class="main-content">
      <div class="section__content section__content--p30">
        <div class="container-fluid">

          <loader></loader>
          <status-messages></status-messages>
          
           <form method="post" action="#" @submit.prevent="save()">
            <div class="row">

              <div class="col-lg-6">
                <div class="card">
                  <div class="card-header">
                    <strong>Create New Category</strong>
                  </div>
                  <div class="card-body card-block">
                    <div class="form-group">
                      <label for="title" class=" form-control-label">Title</label>
                      <input type="text" id="title" name="title" placeholder="Enter category title" class="form-control">
                    </div>
                    <div class="form-group">
                      <label for="parent_id" class=" form-control-label">Parent</label>
                      <select id="parent_id" name="parent_id" class="form-control">

                      </select>
                    </div>
                  </div>
                </div>
              </div>
              <div class="col-lg-6">
                <div class="card">
                  <div class="card-body card-block">
                    <div class="form-group">
                      <label for="description" class=" form-control-label">Description</label>
                      <textarea id="description" name="description" class="form-control"></textarea>
                    </div>
                    <div class="form-group">
                      <label for="featured" class=" form-control-label">Is Featured</label>
                      <select id="featured" name="featured" class="form-control">
                        <option value="0">No</option>
                        <option value="1">Yes</option>
                      </select>
                    </div>
                  </div>
                </div>
              </div>

              <div class="col-lg-12">
                <div class="card">
                  <div class="card-header">
                    <strong>Features</strong>
                  </div>
                  <div class="card-body card-block">
                    <CategoryFeatures></CategoryFeatures>
                  </div>
                </div>
              </div>

            </div>
            <div class="row">
                <button type="submit" class="btn btn-lg btn-info btn-block">
                  <i class="fa fa-save fa-lg"></i> Save
                </button>
            </div>
          </form>
        </div>
      </div>
  </div>
</template>

<script>
    import statusMessages from "../../components/helpers/statusMessages";
    import Loader from "../../components/helpers/loader";
    import CategoryFeatures from "../../components/category-components/CategoryFeatures";

    export default {
        name: "create",
        middleware: "auth",
        components: {
          CategoryFeatures,
          Loader,
          statusMessages
        },
        computed: {
        },
      methods: {
          save() {
          }
      },
       mounted() {
       }
    }
</script>

<style scoped>

</style>

admin/pages/category/_edit.vue

<template>
  <div class="main-content">
    <div class="section__content section__content--p30">
      <div class="container-fluid">
        
        <loader></loader>
        <status-messages></status-messages>
        
         <form method="post" action="#" @submit.prevent="update()">
          <div class="row">

            <div class="col-lg-6">
              <div class="card">
                <div class="card-header">
                  <strong>Update Category #{{this.id}}</strong>
                </div>
                <div class="card-body card-block">
                  <div class="form-group">
                    <label for="title" class=" form-control-label">Title</label>
                    <input type="text" id="title" name="title" placeholder="Enter category title" class="form-control">
                  </div>
                  <div class="form-group">
                    <label for="parent_id" class=" form-control-label">Parent</label>
                    <select id="parent_id" name="parent_id" class="form-control">

                    </select>
                  </div>
                </div>
              </div>
            </div>
            <div class="col-lg-6">
              <div class="card">
                <div class="card-body card-block">
                  <div class="form-group">
                    <label for="description" class=" form-control-label">Description</label>
                    <textarea id="description" name="description" class="form-control"></textarea>
                  </div>
                   <div class="form-group">
                      <label for="featured" class=" form-control-label">Is Featured</label>
                      <select id="featured" name="featured" class="form-control">
                        <option value="0">No</option>
                        <option value="1">Yes</option>
                      </select>
                    </div>
                </div>
              </div>
            </div>

            <div class="col-lg-12">
              <div class="card">
                <div class="card-header">
                  <strong>Features</strong>
                </div>
                <div class="card-body card-block">
                  <CategoryFeatures></CategoryFeatures>
                </div>
              </div>
            </div>

          </div>
          <div class="row">
            <button type="submit" class="btn btn-lg btn-info btn-block">
              <i class="fa fa-save fa-lg"></i> Update
            </button>
          </div>
        </form>
      </div>
    </div>
  </div>
</template>

<script>
    import statusMessages from "../../components/helpers/statusMessages";
    import Loader from "../../components/helpers/loader";
    import CategoryFeatures from "../../components/category-components/CategoryFeatures";

    export default {
        name: "edit",
        middleware: "auth",
        components: {
          CategoryFeatures,
          Loader,
          statusMessages
        },
        data() {
          return {
            id: ''
          }
        },
      computed: {
      },
      methods: {
        update() {
          
        }
      },
      mounted() {
          if(this.$route.params.edit) {
            this.id = this.$route.params.edit;
          }
      }
    }
</script>

<style scoped>

</style>

So you may notice these elements the <loader>, <status-messages>, <Pagination>. These are helper components that we will use across the overall project and there is also components that relate to this module only like <CategoryFilter> and <CategoryRow>

So let’s create these component files and will return to them shortly after we talk about each page.

Go to admin/components and create these directories:

  • /helpers
  • /category-components

 

Then go to the /helpers directory and create these components:

components/helpers/loader.vue

<template>
  <div>
    <div class="lds-roller">
      <div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div>
    </div>
  </div>
</template>

<script>
    export default {
        name: "loader"
    }
</script>

<style scoped>
  .lds-roller {
    display: inline-block;
    position: absolute;
    width: 80px;
    height: 80px;
    top: 16%;
    left: 46%;
    z-index: 9999;
  }
  .lds-roller div {
    animation: lds-roller 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
    transform-origin: 40px 40px;
  }
  .lds-roller div:after {
    content: " ";
    display: block;
    position: absolute;
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: #000;
    margin: -4px 0 0 -4px;
  }
  .lds-roller div:nth-child(1) {
    animation-delay: -0.036s;
  }
  .lds-roller div:nth-child(1):after {
    top: 63px;
    left: 63px;
  }
  .lds-roller div:nth-child(2) {
    animation-delay: -0.072s;
  }
  .lds-roller div:nth-child(2):after {
    top: 68px;
    left: 56px;
  }
  .lds-roller div:nth-child(3) {
    animation-delay: -0.108s;
  }
  .lds-roller div:nth-child(3):after {
    top: 71px;
    left: 48px;
  }
  .lds-roller div:nth-child(4) {
    animation-delay: -0.144s;
  }
  .lds-roller div:nth-child(4):after {
    top: 72px;
    left: 40px;
  }
  .lds-roller div:nth-child(5) {
    animation-delay: -0.18s;
  }
  .lds-roller div:nth-child(5):after {
    top: 71px;
    left: 32px;
  }
  .lds-roller div:nth-child(6) {
    animation-delay: -0.216s;
  }
  .lds-roller div:nth-child(6):after {
    top: 68px;
    left: 24px;
  }
  .lds-roller div:nth-child(7) {
    animation-delay: -0.252s;
  }
  .lds-roller div:nth-child(7):after {
    top: 63px;
    left: 17px;
  }
  .lds-roller div:nth-child(8) {
    animation-delay: -0.288s;
  }
  .lds-roller div:nth-child(8):after {
    top: 56px;
    left: 12px;
  }
  @keyframes lds-roller {
    0% {
      transform: rotate(0deg);
    }
    100% {
      transform: rotate(360deg);
    }
  }
</style>

components/helpers/statusMessages.vue

<template>
  <div>
    <div class="sufee-alert alert with-close alert-success alert-dismissible">
      <span class="badge badge-pill badge-success">Success</span>
      
    </div>

    <div class="sufee-alert alert with-close alert-danger alert-dismissible">
      <span class="badge badge-pill badge-success">Error</span>

    </div>
  </div>
</template>

<script>
    export default {
        name: "statusMessages",
      mounted() {
      }
    }
</script>

<style scoped>

</style>

components/helpers/Pagination.vue

<template>
  <nav v-if="showPaginator()">
    <ul class="pagination">
      <li>
        <a class="page-link" href="#">First</a>
      </li>

      <li class="page-item">
        <a class="page-link" href="#">
          <span aria-hidden="true">&laquo;</span>
        </a>
      </li>

      <li class="page-item">
        <a class="page-link" href="#">
          <span aria-hidden="true">&raquo;</span>
        </a>
      </li>

      <li class="page-item">
        <a class="page-link" href="#">Last</a>
      </li>
    </ul>
  </nav>
</template>

<script>
    export default {
        name: "Pagination",
        methods: {
          showPaginator() {
            return true;
          }
        },
        mounted() {

        }
    }
</script>

<style scoped>

</style>

Also in the /category-components directory create these components:

components/category-components/CategoryFilter.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">
      </div>
      <div class="col-3">
        <input type="text" class="form-control" placeholder="Title" name="title">
      </div>
      <div class="col-3">
        <select class="form-control" name="parent_id" >
          <option value="">select parent</option>
        </select>
      </div>

      <div class="col-5">
        <nuxt-link to="/category/create" class="btn btn-success pull-right"><i class="fa fa-plus-square"></i> Create</nuxt-link>
      </div>
    </div>
  </form>
</template>

<script>
    export default {
        name: "CategoryFilter",
        methods: {
          
        }
    }
</script>

<style scoped>

</style>

components/category-components/Row.vue

<template>
  <tr>
    <td></td>
    <td></td>
    <td></td>
    <td></td>
    <td>
      <a href="#" class="btn btn-info btn-sm"><i class="fa fa-edit"></i></nuxt-link>
      <a href="#" class="btn btn-danger btn-sm"><i class="fa fa-remove"></i></a>
    </td>
  </tr>
</template>

<script>
    export default {
        name: "CategoryRow",
        methods: {
          
        }
    }
</script>

<style scoped>

</style>

components/category-components/CategoryFeatures.vue

<template>
  <div>
      <div class="row" style="margin-bottom: 5px;">
        <div class="col-lg-12">
          <button type="button" class="btn btn-success btn-sm pull-right" @click="addNewFeature()" style="margin-top:5px"><i class="fa fa-plus-square"></i> Add new Feature</button>
        </div>
      </div>
      <div>
      <div class="row" style="margin-bottom: 5px;">
        <div class="col-lg-4">
          <input type="text" name="field_title[]" class="form-control" placeholder="Feature title" />
        </div>
        <div class="col-lg-4">
          <select name="field_type[]" class="form-control" >
            <option value="1">Text</option>
            <option value="2">Color</option>
          </select>
        </div>
        <div class="col-lg-4">
          <a href="#" class="btn btn-sm btn-danger"><i class="fa fa-remove"></i></a>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
    export default {
        name: "CategoryFeatures",
        computed: {
          
        },
        methods: {
          addNewFeature() {
            
          }
        },
        mounted() {

        }
    }
</script>

<style scoped>

</style>

 

Once you created these pages and components Nuxtjs automatically will create the routes for us for example the routes will be /category, /category/create, /category/{id}. The _edit.vue page indicates that this is a dynamic route meaning that i can pass id value like 1,2,3 etc. Let’s update the NavMenu component and add a link to the category page.

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>
      <a href="#">
        <i class="fas fa-television"></i>Brands</a>
    </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>

Now you can navigate easily from the sidebar to the category pages. In the next section we will focus on each page but first we need to add the webservices, and the Vuex store.

 

Category Webservices

To make dealing with the webservices really cool i will add them to a separate file and then exporting them so that our code becomes more organized. For this purpose create admin/api/ directory, then inside this create category.js

admin/api/category.js

const CategoryApi = {
  getCategoriesHtmlTree: (axios, exceptId = null) => {
    const except = exceptId?'?except_id=' + exceptId:'';

    return axios.$get('/api/category/htmltree' + except);
  },
  create: (axios, payload) => {
    return axios.$post('/api/category', 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/category?' + payload_arr.join("&"));
  },
  delete: (axios, id) => {
    return axios.$delete('/api/category/' + id);
  },
  show: (axios, id) => {
    return axios.$get('/api/category/' + id);
  },
  update(axios, payload, id) {
    return axios.$put('/api/category/' + id, payload);
  }
};

export {CategoryApi};

The above code represent all crud api endpoints for the webservices we implemented in the previous part in Lumen. For each endpoint i pass the axios instance and other arguments depending on each web service and each webservice return a promise so that we can consume.

 

Vuex Store

As we already know that Nuxt comes with vuex store already injected and resides in the store/ directory and come with two flavors, the first is the classic mode which allows you to add your all application store in single file,

The other kind is to use module based store by creating a file for each store, for example we will create a category.js file for the categories. Nuxt automatically converts these files modules internally.

At first i will create a shared store that represent generic messages used in the overall application so inside admin/store create shared.js

admin/store/shared.js

export const state = () => ({
  status_messages: {
    showLoading: false,
    success_message: '',
    error_message: '',
    validation_errors: []
  }
});

export const mutations = {
  setStatusMessageParameter(state, value) {
    state.status_messages[value.key] = value.val;
  },
  resetStatusMessagesParameters(state) {
    state.status_messages = {
      showLoading: false,
      success_message: '',
      error_message: '',
      validation_errors: []
    };
  },
};

As you see here i added the store as an arrow function that contains object for the store data. In Nuxt the mutations, actions, and getters is an object like you see i added two mutations, setStatusMessageParameter() that updated single parameter in status_messages variable, and resetStatusMessagesParameters() to reset back the status_messages.

To access any data in the store in any component like this:

this.$store.state.shared.status_messages.showLoading

To commit any mutation in components like this:

this.$store.commit('shared/resetStatusMessagesParameter')

 

Next create admin/store/category.js with the below code:

import { CategoryApi } from '../api/category';

export const state = () => ({
  category: {
    title: '',
    parent_id: '',
    description: '',
    featured: 0
  },
  categoryHtmlTree: '',
  features: [

  ]
});

export const mutations = {
  setTitle(state, title) {
    state.category.title = title;
  },
  setParentId(state, parent_id) {
    state.category.parent_id = parent_id;
  },
  setDescription(state, description) {
    state.category.description = description;
  },
  setFeatured(state, featured) {
    state.category.featured = featured;
  },
  setCategoryHtmlTree(state, tree) {
    state.categoryHtmlTree = tree;
  },
  resetCategory(state) {
    state.category = {
        title: '',
        parent_id: '',
        description: '',
        featured: 0
    };
    state.features = [];
  },
  setFeatures(state, value) {
    state.features = value;
  },
  addToFeatures(state, row) {
    state.features.push(row);
  },
  removeFromFeature(state, id) {
    state.features = [...state.features.filter(item => item.id !== id)];
  },
  updateFeatureTitle(state, payload) {
    let features = [...state.features];
    features.map(feature => {
      if(feature.id == payload.id) {
        feature.field_title = payload.value;
      }
    });

    state.features = features;
  },
  updateFeatureType(state, payload) {
    let features = [...state.features];
    features.map(feature => {
      if(feature.id == payload.id) {
        feature.field_type = payload.value;
      }
    });

    state.features = features;
  }
};

export const actions = {
  
};

The state object contains the category object, we will use this object in the create and edit screens later to bind with the v-model. categoryHtmlTree will be used in select dropdown to display category tree, we will retrieved this tree when we create the actions below. features represent an array of category features, we will use this in the create and edit screens.

The mutations is strait forward we add a mutation for every state variable like setTitle(), setParentId(), updateFeatureTitle(). Now we will move to implement each page functionality.

 

Create New Category

To create new category we need to update the category store and add two actions in the actions object:

getCategoryHtmlTree({commit}, exceptId = null) {
    CategoryApi.getCategoriesHtmlTree(this.$axios, exceptId).then(response => {
      let html = '<option value="">none</option>';
      html += response;
      commit('setCategoryHtmlTree', html);
    });
  },
  create({commit}, payload) {

    commit('shared/resetStatusMessagesParameters', null, {root: true});
    commit('shared/setStatusMessageParameter', {key: 'showLoading', val: true}, {root: true});

    const dataToSend = payload.data;
    dataToSend.features = payload.features;

    CategoryApi.create(this.$axios, dataToSend).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});
      }

      setTimeout(() => {
        payload.router.push('/category');
      }, 2000);

    }).catch(err => {
      commit('shared/setStatusMessageParameter', {key: 'showLoading', val: false}, {root: true});
      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});
        }
      }
    }).finally(() => {
      setTimeout(() => {
        commit('shared/resetStatusMessagesParameters', null, {root: true});
        commit('resetCategory');
      }, 3000);
    });
  }

The getCategoryHtmlTree() action retreives the category tree by calling the getCategoriesHtmlTree() api and then commiting the result with setCategoryHtmlTree() which in turn updates the store state variable categoryHtmlTree.

The other action create() fired when submitting the create form we will see below responsible for the create category process. At first we commit the shared store resetStatusMessagesParameters, and setStatusMessagesParameter to show loading spinner. Then we call the create() api, upon success or failure we hide the loading spinner by commiting back showLoading to false, then we set status messages to either success or failure. Then we redirect the user to the list categories screen.

 

Now open pages/category/create.vue and update it as follows:

<template>
  <div class="main-content">
      <div class="section__content section__content--p30">
        <div class="container-fluid">
          <loader></loader>
          <status-messages></status-messages>
          <form method="post" action="#" @submit.prevent="save()">
            <div class="row">

              <div class="col-lg-6">
                <div class="card">
                  <div class="card-header">
                    <strong>Create New Category</strong>
                  </div>
                  <div class="card-body card-block">
                    <div class="form-group">
                      <label for="title" class=" form-control-label">Title</label>
                      <input type="text" id="title" name="title" placeholder="Enter category title" class="form-control" v-model="title">
                    </div>
                    <div class="form-group">
                      <label for="parent_id" class=" form-control-label">Parent</label>
                      <select id="parent_id" name="parent_id" class="form-control" v-model="parent_id" v-html="categoryTree">

                      </select>
                    </div>
                  </div>
                </div>
              </div>
              <div class="col-lg-6">
                <div class="card">
                  <div class="card-body card-block">
                    <div class="form-group">
                      <label for="description" class=" form-control-label">Description</label>
                      <textarea id="description" name="description" class="form-control" v-model="description"></textarea>
                    </div>
                    <div class="form-group">
                      <label for="featured" class=" form-control-label">Is Featured</label>
                      <select id="featured" name="featured" class="form-control" v-model="featured">
                        <option value="0">No</option>
                        <option value="1">Yes</option>
                      </select>
                    </div>
                  </div>
                </div>
              </div>

              <div class="col-lg-12">
                <div class="card">
                  <div class="card-header">
                    <strong>Features</strong>
                  </div>
                  <div class="card-body card-block">
                    <CategoryFeatures></CategoryFeatures>
                  </div>
                </div>
              </div>

            </div>
            <div class="row">
                <button type="submit" class="btn btn-lg btn-info btn-block">
                  <i class="fa fa-save fa-lg"></i> Save
                </button>
            </div>
          </form>
        </div>
      </div>
  </div>
</template>

<script>
    import statusMessages from "../../components/helpers/statusMessages";
    import Loader from "../../components/helpers/loader";
    import CategoryFeatures from "../../components/category-components/CategoryFeatures";

    export default {
        name: "create",
        middleware: "auth",
        components: {
          CategoryFeatures,
          Loader,
          statusMessages
        },
        fetch() {
          this.$store.dispatch('category/getCategoryHtmlTree');
        },
        computed: {
          title: {
            set(title) {
              this.$store.commit('category/setTitle', title);
            },
            get() {
              return this.$store.state.category.category.title;
            }
          },
          parent_id: {
            set(parent_id) {
              this.$store.commit('category/setParentId', parent_id);
            },
            get() {
              return this.$store.state.category.category.parent_id;
            }
          },
          description: {
            set(description) {
              this.$store.commit('category/setDescription', description);
            },
            get() {
              return this.$store.state.category.category.description;
            }
          },
          featured: {
            set(featured) {
              this.$store.commit('category/setFeatured', featured);
            },
            get() {
              return this.$store.state.category.category.featured;
            }
          },
          categoryTree() {
            return this.$store.state.category.categoryHtmlTree;
          }
        },
      methods: {
          save() {
            this.$store.dispatch('category/create', {data: this.$store.state.category.category,
              features: this.$store.state.category.features,
              router: this.$router
            });
          }
      },
       mounted() {
          this.$store.commit('category/resetCategory');
       }
    }
</script>

<style scoped>

</style>

Nuxt provide us with new hooks like the fetch() hook you see above, this hook invoked automatically by Nuxt before mounting the component, it’s useful to fetch data from the server before component mount, in this case we are dispatching getCategoryHtmlTree() action to load the categories tree.

In the computed property we set some form properties like the title(), description(), it’s supposed you are already familiar with such computed properties and how it work.

In the mounted() hook we commit resetCategory() mutation to reset category data, this is useful when you access the create category screen for the first time.

The save() method triggers dispatch the create() action passing in the data, features, and the router.

 

Now we need to update the partial components and populate it with real data:

admin/components/helpers/loader.vue

<template>
  <div v-if="this.$store.state.shared.status_messages.showLoading">
    <div class="lds-roller">
      <div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div>
    </div>
  </div>
</template>

admin/components/helpers/statusMessages.vue

<template>
  <div>
    <div class="sufee-alert alert with-close alert-success alert-dismissible"
         v-if="this.$store.state.shared.status_messages.success_message != ''">
      <span class="badge badge-pill badge-success">Success</span>
      {{ this.$store.state.shared.status_messages.success_message }}
    </div>

    <div class="sufee-alert alert with-close alert-danger alert-dismissible" v-if="this.$store.state.shared.status_messages.error_message != ''">
      <span class="badge badge-pill badge-success">Error</span>
      {{ this.$store.state.shared.status_messages.error_message }}
    </div>

    <div class="sufee-alert alert with-close alert-danger alert-dismissible" v-if="this.$store.state.shared.status_messages.validation_errors.length">
      <ul v-for="(error, index) in this.$store.state.shared.status_messages.validation_errors" :key="index">
        <li>{{ error }}</li>
      </ul>
    </div>
  </div>
</template>

<script>
    export default {
        name: "statusMessages",
      mounted() {
      }
    }
</script>

<style scoped>

</style>

admin/components/category-components/CategoryFeatures.vue

<template>
  <div>
      <div class="row" style="margin-bottom: 5px;">
        <div class="col-lg-12">
          <button type="button" class="btn btn-success btn-sm pull-right" @click="addNewFeature()" style="margin-top:5px"><i class="fa fa-plus-square"></i> Add new Feature</button>
        </div>
      </div>
      <div>
      <div class="row" v-for="feature in this.features" :key="feature.id" style="margin-bottom: 5px;">
        <div class="col-lg-4">
          <input type="text" name="field_title[]" class="form-control" @change="updateFieldTitle($event, feature.id)" :value="feature.field_title" placeholder="Feature title" />
        </div>
        <div class="col-lg-4">
          <select name="field_type[]" class="form-control" @change="updateFieldType($event, feature.id)" :value="feature.field_type">
            <option value="1">Text</option>
            <option value="2">Color</option>
          </select>
        </div>
        <div class="col-lg-4">
          <a href="#" class="btn btn-sm btn-danger" @click.prevent="removeFeature(feature.id)"><i class="fa fa-remove"></i></a>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
    export default {
        name: "CategoryFeatures",
        computed: {
          features: {
            get() {
              return this.$store.state.category.features;
            }
          }
        },
        methods: {
          addNewFeature() {
            this.$store.commit('category/addToFeatures', {
              id: Math.floor(Math.random() * 1000), field_title: '', field_type: 1, category_id: ''
            });
          },
          removeFeature(id) {
            this.$store.commit('category/removeFromFeature', id);
          },
          updateFieldTitle(e, id) {
            this.$store.commit('category/updateFeatureTitle', {id, value: e.target.value});
          },
          updateFieldType(e, id) {
            this.$store.commit('category/updateFeatureType', {id, value: e.target.value});
          }
        },
        mounted() {

        }
    }
</script>

<style scoped>

</style>

That’s great at this point you can navigate to http://localhost:4000/category/create and experiment to add some categories.

 

 

Update Categories

Let’s add the update functionality the same way as the create first update the store to add the update() action:

Open admin/store/category.js and add these actions inside the actions object:

showCategory({commit}, id) {
    commit('shared/resetStatusMessagesParameters', null, {root: true});
    commit('shared/setStatusMessageParameter', {key: 'showLoading', val: true}, {root: true});

    CategoryApi.show(this.$axios, id).then(response => {
      commit('shared/setStatusMessageParameter', {key: 'showLoading', val: false}, {root: true});

      if(response.category) {
        commit('setTitle', response.category.title);
        commit('setDescription', response.category.description);
        commit('setParentId', response.category.parent_id);
        commit('setFeatured', response.category.featured);

        // set features
        if(response.category.features) {
           commit('setFeatures', response.category.features);
        }
      }

    }).catch(err => {
      console.log(err);
    });
  },
  updateCategory({commit, router}, payload) {
    commit('shared/resetStatusMessagesParameters', null, {root: true});
    commit('shared/setStatusMessageParameter', {key: 'showLoading', val: true}, {root: true});

    CategoryApi.update(this.$axios, payload.data, payload.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});
      }

      setTimeout(() => {
        payload.router.push('/category');
      }, 2000);

    }).catch(err => {
      commit('shared/setStatusMessageParameter', {key: 'showLoading', val: false}, {root: true});
      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});
        }
      }
    }).finally(() => {
      setTimeout(() => {
        commit('shared/resetStatusMessagesParameters', null, {root: true});
        commit('resetCategory');
      }, 3000);
    });
  }

The first action showCategory() used to retreive the category by id, this is to populate the edit form before updating, the second action updateCategory() used for when submitting the form, this action is the same as the create action so i don’t re-explain it and i will move on to edit page.

Open admin/pages/category/_edit.vue and update as follows:

<template>
  <div class="main-content">
    <div class="section__content section__content--p30">
      <div class="container-fluid">
        <loader></loader>
        <status-messages></status-messages>
        <form method="post" action="#" @submit.prevent="update()">
          <div class="row">

            <div class="col-lg-6">
              <div class="card">
                <div class="card-header">
                  <strong>Update Category #{{this.id}}</strong>
                </div>
                <div class="card-body card-block">
                  <div class="form-group">
                    <label for="title" class=" form-control-label">Title</label>
                    <input type="text" id="title" name="title" placeholder="Enter category title" class="form-control" v-model="title">
                  </div>
                  <div class="form-group">
                    <label for="parent_id" class=" form-control-label">Parent</label>
                    <select id="parent_id" name="parent_id" class="form-control" v-model="parent_id" v-html="categoryTree">

                    </select>
                  </div>
                </div>
              </div>
            </div>
            <div class="col-lg-6">
              <div class="card">
                <div class="card-body card-block">
                  <div class="form-group">
                    <label for="description" class=" form-control-label">Description</label>
                    <textarea id="description" name="description" class="form-control" v-model="description"></textarea>
                  </div>
                  <div class="form-group">
                    <label for="featured" class=" form-control-label">Is Featured</label>
                    <select id="featured" name="featured" class="form-control" v-model="featured">
                      <option value="0">No</option>
                      <option value="1">Yes</option>
                    </select>
                  </div>
                </div>
              </div>
            </div>

            <div class="col-lg-12">
              <div class="card">
                <div class="card-header">
                  <strong>Features</strong>
                </div>
                <div class="card-body card-block">
                  <CategoryFeatures></CategoryFeatures>
                </div>
              </div>
            </div>

          </div>
          <div class="row">
            <button type="submit" class="btn btn-lg btn-info btn-block">
              <i class="fa fa-save fa-lg"></i> Update
            </button>
          </div>
        </form>
      </div>
    </div>
  </div>
</template>

<script>
    import statusMessages from "../../components/helpers/statusMessages";
    import Loader from "../../components/helpers/loader";
    import CategoryFeatures from "../../components/category-components/CategoryFeatures";

    export default {
        name: "edit",
        middleware: "auth",
        components: {
          CategoryFeatures,
          Loader,
          statusMessages
        },
        data() {
          return {
            id: ''
          }
        },
        fetch() {
          this.$store.dispatch('category/getCategoryHtmlTree', this.$route.params.edit);
        },
      computed: {
        title: {
          set(title) {
            this.$store.commit('category/setTitle', title);
          },
          get() {
            return this.$store.state.category.category.title;
          }
        },
        parent_id: {
          set(parent_id) {
            this.$store.commit('category/setParentId', parent_id);
          },
          get() {
            return this.$store.state.category.category.parent_id;
          }
        },
        description: {
          set(description) {
            this.$store.commit('category/setDescription', description);
          },
          get() {
            return this.$store.state.category.category.description;
          }
        },
        featured: {
          set(featured) {
            this.$store.commit('category/setFeatured', featured);
          },
          get() {
            return this.$store.state.category.category.featured;
          }
        },
        categoryTree() {
          return this.$store.state.category.categoryHtmlTree;
        }
      },
      methods: {
        update() {
          this.$store.dispatch('category/updateCategory', {
            data: this.$store.state.category.category,
            features: this.$store.state.category.features,
            id: this.id,
            router: this.$router
          });
        }
      },
      mounted() {
          if(this.$route.params.edit) {
            this.id = this.$route.params.edit;
            this.$store.dispatch('category/showCategory', this.$route.params.edit);
          }
      }
    }
</script>

<style scoped>

</style>

As you see this page is much like the create page, note that in the mounted() hook we check for id param coming from url and use this to dispatch the showCategory() action to fetch the category by which in turn fill the form fields with data.

 

Listing All Categories

The last thing we will implement in this part is to display all categories, in pages/category/index.vue.

Let’s update the store first to add some extra state variables, actions and mutations:

import { CategoryApi } from '../api/category';

export const state = () => ({
  category: {
    title: '',
    parent_id: '',
    description: '',
    featured: 0
  },
  categoryHtmlTree: '',
  features: [
  ],
  filterData: {
    id: '',
    title: '',
    parent_id: ''
  },
  page: 1,
  categoryList: {}
});

export const mutations = {
  setTitle(state, title) {
    state.category.title = title;
  },
  setParentId(state, parent_id) {
    state.category.parent_id = parent_id;
  },
  setDescription(state, description) {
    state.category.description = description;
  },
  setFeatured(state, featured) {
    state.category.featured = featured;
  },
  setCategoryHtmlTree(state, tree) {
    state.categoryHtmlTree = tree;
  },
  resetCategory(state) {
    state.category = {
        title: '',
        parent_id: '',
        description: '',
        featured: 0
    };
    state.features = [];
  },
  setFeatures(state, value) {
    state.features = value;
  },
  addToFeatures(state, row) {
    state.features.push(row);
  },
  removeFromFeature(state, id) {
    state.features = [...state.features.filter(item => item.id !== id)];
  },
  updateFeatureTitle(state, payload) {
    let features = [...state.features];
    features.map(feature => {
      if(feature.id == payload.id) {
        feature.field_title = payload.value;
      }
    });

    state.features = features;
  },
  updateFeatureType(state, payload) {
    let features = [...state.features];
    features.map(feature => {
      if(feature.id == payload.id) {
        feature.field_type = payload.value;
      }
    });

    state.features = features;
  },
  setCategoryList(state, category_list) {
    state.categoryList = category_list;
  },
  setPage(state, page) {
    state.page = page;
  },
  setFilterData(state, value) {
    state.filterData[value.key] = value.val;
  }
};

export const actions = {
  getCategoryHtmlTree({commit}, exceptId = null) {
    CategoryApi.getCategoriesHtmlTree(this.$axios, exceptId).then(response => {
      let html = '<option value="">none</option>';
      html += response;
      commit('setCategoryHtmlTree', html);
    });
  },
  create({commit}, payload) {

    commit('shared/resetStatusMessagesParameters', null, {root: true});
    commit('shared/setStatusMessageParameter', {key: 'showLoading', val: true}, {root: true});

    const dataToSend = payload.data;
    dataToSend.features = payload.features;

    CategoryApi.create(this.$axios, dataToSend).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});
      }

      setTimeout(() => {
        payload.router.push('/category');
      }, 2000);

    }).catch(err => {
      commit('shared/setStatusMessageParameter', {key: 'showLoading', val: false}, {root: true});
      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});
        }
      }
    }).finally(() => {
      setTimeout(() => {
        commit('shared/resetStatusMessagesParameters', null, {root: true});
        commit('resetCategory');
      }, 3000);
    });
  },
  listCategories({commit}, payload = null) {
    CategoryApi.list(this.$axios, payload).then(response => {
      commit('setCategoryList', response.categories);
    });
  },
  deleteCategory({commit, state}, id) {
    commit('shared/resetStatusMessagesParameters', null, {root: true});
    commit('shared/setStatusMessageParameter', {key: 'showLoading', val: true}, {root: true});

    CategoryApi.delete(this.$axios, id).then(response => {
      commit('shared/setStatusMessageParameter', {key: 'showLoading', val: false}, {root: true});
      if(response.success) {

        let categoryList = {...state.categoryList};
        categoryList.data = categoryList.data.filter(item => item.id !== id);

        commit('setCategoryList', categoryList);
        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);
    });
  },
  showCategory({commit}, id) {
    commit('shared/resetStatusMessagesParameters', null, {root: true});
    commit('shared/setStatusMessageParameter', {key: 'showLoading', val: true}, {root: true});

    CategoryApi.show(this.$axios, id).then(response => {
      commit('shared/setStatusMessageParameter', {key: 'showLoading', val: false}, {root: true});

      if(response.category) {
        commit('setTitle', response.category.title);
        commit('setDescription', response.category.description);
        commit('setParentId', response.category.parent_id);
        commit('setFeatured', response.category.featured);

        // set features
        if(response.category.features) {
           commit('setFeatures', response.category.features);
        }
      }

    }).catch(err => {
      console.log(err);
    });
  },
  updateCategory({commit}, payload) {
    commit('shared/resetStatusMessagesParameters', null, {root: true});
    commit('shared/setStatusMessageParameter', {key: 'showLoading', val: true}, {root: true});

    CategoryApi.update(this.$axios, payload.data, payload.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});
      }

      setTimeout(() => {
        payload.router.push('/category');
      }, 2000);

    }).catch(err => {
      commit('shared/setStatusMessageParameter', {key: 'showLoading', val: false}, {root: true});
      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});
        }
      }
    }).finally(() => {
      setTimeout(() => {
        commit('shared/resetStatusMessagesParameters', null, {root: true});
        commit('resetCategory');
      }, 3000);
    });
  }
};

Here i added the filterData, categoryList, and the page. The filterData used in the filter form, the categoryList represent our category list coming from lumen and the page represent the current page used when paginating the results.

 

Update admin/pages/category/index.vue as follows:

<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>

            <CategoryFilter v-on:Filtering="handleFiltering"></CategoryFilter>

          </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>Parent</th>
                    <th>Featured</th>
                    <th>Options</th>
                  </tr>
                </thead>
                <tbody>
                  <CategoryRow v-if="categoryList.data" v-for="category of categoryList.data" v-bind:category="category" v-bind:key="category.id" v-on:removeCategory="removeCategory"></CategoryRow>
                </tbody>
              </table>
            </div>
            <Pagination :data="categoryList" v-if="categoryList.data" v-on:handlePagination="handlePagination"></Pagination>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
    import Loader from "../../components/helpers/loader";
    import statusMessages from "../../components/helpers/statusMessages";
    import CategoryRow from "../../components/category-components/Row";
    import Pagination from "../../components/helpers/Pagination";
    import CategoryFilter from "../../components/category-components/CategoryFilter";

    export default {
        name: "index",
        middleware: "auth",
        components: {
          CategoryFilter,
          Pagination,
          CategoryRow,
          Loader,
          statusMessages
        },
        fetch() {
          this.$store.dispatch('category/getCategoryHtmlTree');
          this.$store.dispatch('category/listCategories');
        },
        computed: {
          categoryList() {
            return this.$store.state.category.categoryList;
          }
        },
        methods: {
          handlePagination(page_number) {
            this.$store.commit('category/setPage', page_number);

            this.$store.dispatch('category/listCategories', this.getPayload());
          },
          handleFiltering(field, value) {
            this.$store.commit('category/setFilterData', {key: field, val: value});
            this.$store.commit('category/setPage', 1);

            this.$store.dispatch('category/listCategories', this.getPayload());
          },
          getPayload() {
            let payload = {};
            for(let field in this.$store.state.category.filterData) {
              if(this.$store.state.category.filterData.hasOwnProperty(field) && this.$store.state.category.filterData[field] !== '')
                payload['filter_by_' + field] = this.$store.state.category.filterData[field];
            }
            payload.page = this.$store.state.category.page;

            return payload;
          },
          removeCategory(id) {
            if(confirm("Are you sure?")) {
              this.$store.dispatch('category/deleteCategory', id);
            }
          }
        }
    }
</script>

<style scoped>

</style>

Don’t be shooked with this code, it’s too big but it’s too easy. The <CategoryFilter> takes a custom event “Filtering” which fires when manipulating filtering, if you aren’t familiar with vuejs custom events i suggest you read more about them.

Next we display all categories in the table which use another component <CategoryRow> which takes a prop which is the category coming from the v-for and display it. Also we aet a custom event here used when clicking the remove button to remove the category.

The <Pagination> component is a generic component used to display the pagination links, this component takes the a prop the result set, and custom event that called when clicking on any of the pagination links.

admin/components/category-components/CategoryFilter.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.category.filterData.id">
      </div>
      <div class="col-3">
        <input type="text" class="form-control" placeholder="Title" name="title" @change="handleFiltering($event)" :value="this.$store.state.category.filterData.title">
      </div>
      <div class="col-3">
        <select class="form-control" v-html="this.$store.state.category.categoryHtmlTree" name="parent_id" @change="handleFiltering($event)" :value="this.$store.state.category.filterData.parent_id">
          <option value="">select parent</option>
        </select>
      </div>

      <div class="col-5">
        <nuxt-link to="/category/create" class="btn btn-success pull-right"><i class="fa fa-plus-square"></i> Create</nuxt-link>
      </div>
    </div>
  </form>
</template>

<script>
    export default {
        name: "CategoryFilter",
        methods: {
          handleFiltering(event) {
            this.$emit('Filtering', event.target.name, event.target.value);
          }
        }
    }
</script>

<style scoped>

</style>

admin/components/category-components/Row.vue

<template>
  <tr>
    <td>{{ this.category.id }}</td>
    <td>{{ this.category.title }}</td>
    <td>{{ this.category.parent?this.category.parent.title:"none" }}</td>
    <td>{{ this.category.featured == 1 ? 'Yes' : 'No' }}</td>
    <td>
      <nuxt-link :to="'/category/' + this.category.id" class="btn btn-info btn-sm"><i class="fa fa-edit"></i></nuxt-link>
      <a href="#" class="btn btn-danger btn-sm" @click.prevent="removeCategory(category.id)"><i class="fa fa-remove"></i></a>
    </td>
  </tr>
</template>

<script>
    export default {
        name: "CategoryRow",
        props: ['category'],
        methods: {
          removeCategory(categoryId) {
            this.$emit('removeCategory', categoryId);
          }
        }
    }
</script>

<style scoped>

</style>

admin/components/helpers/Pagination.vue

<template>
  <nav v-if="showPaginator()">
    <ul class="pagination">
      <li :class="'page-item' + (data.current_page == 1?' active':'')">
        <a class="page-link" href="#" @click.prevent="displayPage(1)">First</a>
      </li>

      <li class="page-item" v-if="data.current_page > 1">
        <a class="page-link" href="#" @click.prevent="displayPage(data.current_page - 1)">
          <span aria-hidden="true">&laquo;</span>
        </a>
      </li>

      <li :class="'page-item' + (data.current_page == page?' active':'')" v-for="page in numPages()" :key="page">
        <a class="page-link" href="#" @click.prevent="displayPage(page)">{{ page }}</a>
      </li>

      <li class="page-item" v-if="data.current_page < data.last_page">
        <a class="page-link" href="#" @click.prevent="displayPage(data.current_page + 1)">
          <span aria-hidden="true">&raquo;</span>
        </a>
      </li>

      <li :class="'page-item' + (data.current_page == data.last_page?' active':'')">
        <a class="page-link" href="#" @click.prevent="displayPage(data.last_page)">Last</a>
      </li>
    </ul>
  </nav>
</template>

<script>
    export default {
        name: "Pagination",
        props: ["data"],
        methods: {
          numPages() {
            return Math.ceil(this.data.total / this.data.per_page);
          },
          showPaginator() {
            if(this.numPages() > 1) {
              return true;
            }

            return false;
          },
          displayPage(page_number) {
              this.$emit("handlePagination", page_number);
          }
        },
        mounted() {

        }
    }
</script>

<style scoped>

</style>

Now the category module is completed try to create some categories, edit them and see all categories.

 

 

Continue to Part6: Brands CRUD & Display

 

5 1 vote
Article Rating

What's your reaction?

Excited
0
Happy
2
Not Sure
0
Confused
0

You may also like

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments