DEV Community

Mark Anderson
Mark Anderson

Posted on

Vuex Shopping Cart

I initially wrote a shopping cart that pushed every cart transaction to the server. This works well but upon completion of the project, I felt a client-based shopping cart system would be fun to write. It also offered a project to learn Vuex. I'll stick the technical first and then cover my thoughts on the pros and cons of client-side vs server storage for the cart.

I'll start by covering a few design requirements.

* Ability to use compound keys.
* Ability to handle the quantity
* Ability to return items and quantity to a server in Array
* Require no additional libraries beyond vuex

The Code Layout

I think were code lives are as important for maintenance as the layout of the code within the file. To this end, I wanted to break the shopping cart into an individual module and contain it within a single file. This is imported into the primary store file and used as a module. This seems to follow the Vuejs style. Since persistence and shared mutations are not part of the cart directly but design details they can be managed in the store which is the appropriate place I believe.

The Cart Code

Keys

The cartKeys is an array of strings that contain the keys that will be found on cart items. This allows us to pass the keys for deleting and when putting together the final items list to pass to the server. I often run into a need to handle compound keys, especially when modernizing incrementally.

Adding Keys

When adding we check to see if a like item exists first. If the item already exists we increment the number of items. If the item doesn't exist we add quantity to the item and push it to the cart items array.

Deleting Keys

We pass in the keys of the items to be deleted and the number of items to be deleted. If this leaves a quantity less than one we remove the entire item from the cart items array.

Other

I added an easy way clear the cart and get an count of items in the cart. I feel item count is more important then quantity but this code should be easy enough to customize.


export const ShoppingCartModule = {
    state: {
        cartKeys: [],
        cartItems: []
    },
    mutations: {
        cartKeys(state, keys) {
            state.cartKeys = keys;
        },
        addItem(state, newItem) {
            let found = -1;
            for (const cartIndex in state.cartItems) {
                const item = state.cartItems[cartIndex];
                let allMatch = true;
                for (const keyIndex in state.cartKeys) {
                    const keyValue = state.cartKeys[keyIndex];
                    if (item[keyValue] != newItem[keyValue]) {
                        allMatch = false;
                        break;
                    }
                }
                if (allMatch == true) {
                    found = cartIndex;
                    break;
                }
            }
            if (found > -1) {
                state.cartItems[found].quantity += newItem.quantity || 1;
            } else {
                if (newItem['quantity'] == undefined) {
                    newItem.quantity = 1;
                }
                state.cartItems.push(newItem);
            }
        },
        delItem(state, params) {
            let found = -1;
            for (const cartIndex in state.cartItems) {
                const item = state.cartItems[cartIndex];
                let allMatch = true;
                for (const keyIndex in state.cartKeys) {
                    const keyValue = state.cartKeys[keyIndex];
                    if (item[keyValue] != params.itemKeys[keyValue]) {
                        allMatch = false;
                        break;
                    }
                }
                if (allMatch == true) {
                    found = cartIndex;
                    break;
                }
            }
            if (found > -1) {
                state.cartItems[found].quantity -= params.quantity;
                if (state.cartItems[found].quantity < 1) {
                    state.cartItems.splice(found,1)
                }
            } 

        },
        empty(state) {
            state.cartItems = [];
        }
    },
    actions: {
        addItem(context, item) {
            context.commit(item);
        },
        delItem(context, params) {
            context.commit(params);
        },
        cartKeys(context, keys) {
            context.commit(keys)
        },
        empty(context) {
            context.commit()
        }
    },
    getters: {
        itemCount: state => {
            return state.cartItems.length;
        },
        cart: state => {
            return state.cartItems;
        },
        cartKeys: state => {
            return state.cartKeys;
        },
        itemKeys: state => {
            let results = [];
            for (const cartIndex in state.cartItems) {
                const item = state.cartItems[cartIndex];
                let keyRecord = {quantity: item.quantity};
                for (const keyIndex in state.cartKeys) {
                    const keyValue = state.cartKeys[keyIndex];
                    keyRecord[keyValue] = item[keyValue];
                }
                results.push(keyRecord);
            }
            return results;   
        }
    }
}

The Store File

I chose to use a module for code speperation. I didn't namespace because I like the ability to have things available at the root level. If my store methods were to create a collision I'd most likely namespace the shopping cart module but am not able to say for sure.

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

import {ShoppingCartModule} from "./shopping_cart_module";

export default new Vuex.Store({
    modules: {
      store: ShoppingCartModule
    }
})

The Main File

The one thing of note hear s the es6-promise polyfill. This is required for IE.

import Vue from 'vue'
import store from './store'
import 'es6-promise/auto'

import App from './App.vue'

Vue.config.productionTip = false

new Vue({
  store,
  render: h => h(App)
}).$mount('#app')

The App.vue File

This is a simple page to show how it works. The submit keys button would make a call to get the keys in the propper format to send to the server. This would be the entire cart and possible the only cart related call made. I would do this before accepting any money etc so you won't loose the items to go with a payment. I'll post some code with the cart entirerly maintaind on the server short of a cart ID next.

<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <h1>Welcome to Your Vue.js App</h1>
    <button v-on:click="add('One')">Add Item One</button>
    <button v-on:click="add('Two')">Add Item Two</button>
    <button v-on:click="del('One', 2)">Del 2 Item Ones</button>
    <button v-on:click="del('Two')">Del Item Two</button>
    <button v-on:click="itemKeys()">Submit Keys</button>
    <h3>Cart</h3>
    <pre>{{cart}}</pre>
    <h3>Keys</h3>
    <pre>{{cartKeys}}</pre>
  </div>
</template>

<script>
export default {
  name: "sample",
  data() {
    return {
      cart: this.cart = this.$store.getters.cart,
      cartKeys: ['name','store'],
      store: 'Gunnison'
    }
  },
  created() {
    this.$store.commit("cartKeys",this.cartKeys);
  },
  methods: {
    add(name) {
      let item = {
        name: name,
        store: this.store,
        price: 12.34,
        descripiton: 'This is the item you need to be a contributing member of society.'
      }
      this.$store.commit("addItem", item);

    },
    del(name, quantity = 1) {
      this.$store.commit("delItem", {
        itemKeys: {store: this.store, name: name}, 
        quantity: quantity
        }
      );
    },
    itemKeys() {
      alert(JSON.stringify(this.$store.getters.itemKeys));
      this.$store.commit('empty')
      this.cart = this.$store.getters.cart;
    }
  }
}
</script>

Thoughts

I found this a fun exercise and believe I now have a reasonable understanding of using vuex. This approach has some risky areas and possibility of loosing data if not expanded. I would generally make sure if taking payment that I have a stored version of the cart first. This would mean the addition of code to maintain a cart ID on the server for those who get to the very last step and then want to make a change. Is this a concern you share?

This approach greatly reduces the number of calls to the server but increases the complexity of the data sent and inserted into storage. If your are working with an object database this approach could require little data manipulation on the server. If tracking inventory, marking sold, etc you will need to parse the data anyway so possible not a large savings. This moves the complexity to the front end which seems risky compaired to a server where you can run the tests. Who knows what browser a client will use, but this intentially uses standard calls. Where do you believe the complexity should exists?

Is this a better approach than pushing all cart storage into server storage? I think like most things in life it depends on your specific use case. I will use this approach for web sites where people want to tag/track items within a session, which is how I believe shopping carts should work. This approach should limit the number of incomplete shopping cart stored and needing cleaned up but that is a simple enough matter. Overall the question of location is to complex to answer without lookig and the system delivering the content and the volume of traffic.

Hopefully someone finds this fun and interesting as well. I'll share the version that uses server cart storage if asked.

Top comments (2)

Collapse
 
manhdat107 profile image
Vu Manh Dat

nice article, but I have a little problem, it's when I trying to open a new tab, state of vueX can not keep old value (item have been put into the cart). can you help me with how to do that?

Collapse
 
nosrednakram profile image
Mark Anderson

You will need to add github.com/robinvdvleuten/vuex-per... if you want it to persist through re-loads and tabs.