Member preview

Build a shopping cart with Vue.js and Element UI (no Vuex)

In this tutorial we build a shopping cart with straight up Vue 2 (no Vuex) and style it using the Element UI vue component toolkit.
Screenshot of the app we’re building. Source code

In the last tutorial we built a shopping cart application using Vuejs, Vuex and Bulma for styling. In this tutorial we’re going to build almost the same shopping cart but without Vuex. We’ll use the Element UI toolkit of Vue for styling instead of Bulma to try out something new.

The Vue.js only code is simpler to reason about and does the same functionality. Vuex has its place but I wrote this tutorial to demonstrate that it’s not always necessary.

tl;dr — 🐙 source code, 🛒 demo

🐊 Alternative approaches

Originally, I saw that the “EventBus” pattern is popular for Vue.js applications not using Vuex. The most clear tutorial I’ve found on describing an Event Bus is from alligator.io: https://alligator.io/vuejs/global-event-bus/

EventBuses use events to broadcast changes and update components. I’m going to use a similar pattern but call it a store cause it sounds more accurate. The store will hold the information about the shopping cart and the list of products/

🦄 Create an app

Fire up the vue-cli tool to build a website. We’ll use the webpack-simple generator. Go through the setup options and select Yes that we want SASS.

$ vue init webpack-simple vue-shopping-cart
$ cd vue-shopping-cart
$ npm i
$ npm install element-ui -S

After installing dependencies we can start the webpack server with npm run dev. The Element UI framework is for styling.

Element UI makes things a bit more complicated in terms of setup. You can follow the official docs or copy and paste the webpack.config.js file to get the appropriate loaders.

🐂 Create the Store

I put this file in src/store/store.js

import Vue from 'vue';
import products from '../products'
export const Store = new Vue({
data() {
return {
products,
cart: []
};
},
computed: {
totalCost(){
return this.cart.reduce((accum, product) => {
return accum + product.details.price * product.quantity
}, 0)
}
},
methods: {
addToCart(product){
const locationInCart = this.cart.findIndex(p => {
return p.details.sku === product.sku
})
  if(locationInCart === -1){
this.cart.push({
details: product,
quantity: 1
})
} else {
this.cart[locationInCart].quantity++
}
},
removeFromCart(sku){
const locationInCart = this.cart.findIndex(p => {
return p.details.sku === sku
})
     if(this.cart[locationInCart].quantity <= 1){
this.cart.splice(locationInCart, 1)
} else {
this.cart[locationInCart].quantity--
}
}
}
});

The store Vue instance has a place to hold our products and an empty array for the chopping cart. The products data is a src/products.json file that looks like:

[
{ "id": 1, "name": "Tshirt", "price": 3050, "quantity": 2, "sku": 1, "images": ["https://cdn.shopify.com/s/files/1/2415/9707/products/Screen_Shot_2017-10-02_at_10.18.11_AM_grande.png?v=1506964811"] },
{ "id": 2, "name": "Hat", "price": 2000, "quantity": 10, "sku": 2, "images": ["https://cdn.shopify.com/s/files/1/2415/9707/products/Screen_Shot_2017-10-02_at_10.23.01_AM_grande.png"] },
{ "id": 3, "name": "Stickers", "price": 550, "quantity": 5, "sku": 3, "images": ["https://cdn.shopify.com/s/files/1/2415/9707/products/logo_grande.png?v=1506964542"] }
]

The methods of the store add to the cart and update and subtract the quantity from the products if a product of that type is already in the cart. This way we can easily display information about the product and quickly modify the quantity.

What the shopping cart will look like

🦑 The Product List

The Product List component uses Element built in components to display a card with an image for each product. There is some duplication with the methods. This is where a Vuex-style helper like ...mapActions comes in handy. That said we’re saving a lot of code by not doing it the Vuex way so there’s the tradeoff.

<template>
<el-row>
<el-col :span="12" v-for="(product, index) in products" :key="product.sku">
<el-card>
<img :src="product.images[0]" class="image">
<span>{{ product.name }}</span>
<span>{{ product.price | currency }}</span>
<div class="bottom clearfix">
<el-button type="info" @click='addToCart(product)'>Add to cart</el-button>
</div>
</el-card>
</el-col>
</el-row>
</template>
<script>
import {Store} from '../store/Store'
export default {
data() {
return {
products: Store.$data.products
};
},
methods: {
addToCart(product){
Store.addToCart(product)
}
}
}
</script>
<style>
.image {
width: 100%;
display: block;
}
</style>

We use$data to access items in the store. In the shopping cart we’ll have to take advantage of computed properties so that they can be updated based on user actions. The sku stands for Stock Keeping Unit and is unique for each product.

Product List card, thanks to Element UI and a JSON file

We’ll also write a currency filter which we’ll keep in the src/main.js file:

import Vue from 'vue'
import App from './App.vue'
import Element from 'element-ui'
import locale from 'element-ui/lib/locale/lang/en'
import 'element-ui/lib/theme-default/index.css'
import NavBar from './components/NavBar.vue'
import ProductList from './components/ProductList.vue'
import ShoppingCart from './components/ShoppingCart.vue'
// Services
Vue.use(Element, { locale })
// Components
Vue.component('NavBar', NavBar)
Vue.component('ProductList', ProductList)
Vue.component('ShoppingCart', ShoppingCart)
// Filters
Vue.filter('currency', function (value) {
return '$' + parseFloat(value/100).toFixed(2);
});
new Vue({
el: '#app',
render: h => h(App)
})

As you can see here we register the Element service and declare that we’re speaking English as our locale. The price information we stored in cents so we divide 100.

🐨 The Shopping Cart

The shopping cart was a bit tricky, taking advantage of the Element table component.

<template>
<el-table
:data="cart"
stripe
style="width: 100%">
<el-table-column
prop="details.name"
label="Item Name"></el-table-column>
<el-table-column
label="Price">
<template scope='scope'>
{{ scope.row.details.price | currency }}
</template>
</el-table-column>
<el-table-column
prop="quantity"
label="Quantity"></el-table-column>
<el-table-column
label="">
<template scope="scope">
<el-button type="success" icon="plus" @click='addToCart(scope.row.details)' size="mini"></el-button>
<el-button type="danger" icon="minus" @click='removeFromCart(scope.row.details.sku)' size="mini"></el-button>
</template>
</el-table-column>
</el-table>
</template>
<script>
import {Store} from '../store/Store'
export default {
computed: {
cart(){
return Store.$data.cart
}
},
methods: {
removeFromCart(sku){
Store.removeFromCart(sku)
},
addToCart(product){
Store.addToCart(product);
}
}
}
</script>

The scope part is documented in the custom column template section. I didn’t dig too deep into the summary row option and instead list our total in the src/App.vue file:

<template>
<div id="app">
<nav-bar></nav-bar>
<el-row :gutter="20">
<el-col :span="16">
<h1>Product List</h1>
<product-list></product-list>
</el-col>
<el-col :span="8">
<h1>Shopping Cart</h1>
<shopping-cart></shopping-cart>
<p><b>Total Cost: {{ totalCost | currency }}</b></p>
</el-col>
</el-row>

</div>
</template>
<script>
import {Store} from './store/Store'
export default {
name: 'app',
computed: {
totalCost(){
return Store.totalCost
}
}
}
</script>

In the App.vue file we are calling a computed property in the store and another computed property to watch for changes. This might not be performant in massive applications but overall it gets the job done and works for something small like this. Calling the computed method twice seems less than ideal. Perhaps this is where the EventBus pattern could excel further.

🦋 Conclusion

Overall that covers our simple shopping cart application. We built it using only Vue.js in a simple, straightforward way and did not need to incorporate Vuex. Element UI is cool in that it uses Vue components but abstracts a lot of the implementation. This can be good until you need to dig in and make CSS changes. I think for the future I’m going to stick to using Bulma for main styling and only pull in Element components specifically as needed. This pattern of using a Vue instance as a store might not be best for Facebook-scale JS applications but gets the job done for what most developers build. It’s also cool that you could set up a Vuex store right next to it and manage some of your state that way and nothing would break.