├── .babelrc ├── .gitignore ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── src ├── App.vue ├── assets │ └── logo.png ├── components │ ├── Cart.vue │ ├── NavBar.vue │ └── Products.vue ├── main.js └── store │ ├── index.js │ └── mutation-types.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { "modules": false }], 4 | "stage-3" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log 5 | yarn-error.log 6 | 7 | # Editor directories and files 8 | .idea 9 | *.suo 10 | *.ntvs* 11 | *.njsproj 12 | *.sln 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vuex-shopping-cart 2 | 3 | > A shopping cart built with Vue 2 and Vuex 4 | 5 | > Full tutorial [on Medium](https://medium.com/employbl/build-a-shopping-cart-with-vue-2-and-vuex-5d58b93c513f) 6 | 7 | > For an example of a Vue.js shopping cart that does NOT use Vuex check [this tutorial](https://medium.com/@connorleech/build-a-shopping-cart-with-vue-js-and-element-ui-no-vuex-54682e9df5cd) and [repo](https://github.com/connor11528/vue-shopping-cart) 8 | 9 | ## Vuex Tutorial 10 | 11 | In a [previous tutorial](http://connorleech.info/blog/Build-a-Task-List-with-Laravel-5-and-Vue-2/) we went through the basics of setting up a todo application with Vue.js and Laravel. In this tutorial we will build out a shopping cart application with Vue.js and Vuex. Vuex is a state management library for Vue.js. This means that it helps us keep application state in sync across multiple components and routes through a single source of truth, called a store. In our shopping cart application users will be able to view products, add them to their cart and view the total cost of their order. We will handle routing on the frontend using the official vue-router package. 12 | 13 | ### Why Vuex and what the heck is state management? 14 | 15 | ![](https://raw.githubusercontent.com/vuejs/vuex/dev/docs/en/images/vuex.png) 16 | 17 | Vuex is maintained by the Vue.js core team and brands itself as centralized state management for Vue.js. It is similar to Redux or Flux, application development techniques pioneered by Facebook and the React.js community. If you are unfamiliar with these libraries, not to worry! The Vuex library is for maintaining a global Store that knows about all of the state within our application. In the long run this can make development easier because state is modified and collected from one place instead of many. These changes are then propogated down to the individual components primarily through [computed properties](https://vuejs.org/v2/guide/computed.html). 18 | 19 | The source code for the Vuex shopping cart application is available [here](https://github.com/connor11528/vuex-shopping-cart) 20 | 21 | ![](https://vuejs.org/images/state.png) 22 | 23 | Vuex adds some additional complexity upfront but in the long term can make our application development easier, cleaner and faster. If you write client side tests for your application having a single state tree through Vuex can also make the application simpler to reason about. 24 | 25 | ### Alternative approaches 26 | 27 | Without Vuex, you could communicate state across components using events and passing properties as in the diagram below. This works well for simple parent to child communication and chances are you already use it in your Vue.js applications! 28 | 29 | ![](https://vuejs.org/images/props-events.png) 30 | 31 | A more comprehensive approach to state management from the ground up would be to create a store object, attach it to the global namespace and read/write from that. 32 | 33 | ``` 34 | window.store = { tasks: [{ id: 1, title: 'Learn Vuex' }]}; 35 | 36 | const vmA = new Vue({ 37 | computed: { 38 | todos(){ 39 | return store.tasks 40 | } 41 | }, 42 | methods: { 43 | addTodo(newTodo){ 44 | store.tasks.push(newTodo) 45 | } 46 | } 47 | }) 48 | 49 | const vmB = new Vue({ 50 | computed: { 51 | todos(){ 52 | return store.tasks 53 | } 54 | }, 55 | }) 56 | ``` 57 | 58 | Vuex builds on top of this starting point and adds new concepts like getters, mutations and actions. Let's take a look at how it works by building our shopping cart application. 59 | 60 | ### Create a new Vue app 61 | 62 | The first step is to use the Vue CLI that is maintained by the core team. We're going to use the [webpack-simple](https://github.com/vuejs-templates/webpack-simple) scaffold. 63 | 64 | ``` 65 | $ vue init webpack-simple vuex-shopping-cart 66 | ``` 67 | 68 | Once you have generated a new application, modify the **.babelrc** file like so: 69 | 70 | ``` 71 | "presets": [ 72 | ["env", { "modules": false }], 73 | "stage-3" 74 | ] 75 | ``` 76 | 77 | This gives us the ability to use the ES6 spread operator in our code. You can read more about the spec [here](https://babeljs.io/docs/plugins/preset-stage-3/). 78 | 79 | Install some packages that we will need for the build and the application. 80 | 81 | ``` 82 | $ npm i vuex vue-router --save 83 | $ npm i babel-preset-stage-3 style-loader babel-plugin-transform-object-rest-spread --save-dev 84 | ``` 85 | 86 | The babel stuff isn't strictly required but can make our lives easier down the line. I generally try not to concern myself with webpack and build stuff but in this case it helps. 87 | 88 | We're going to use Bulma for styling so add that with font awesome to **index.html**. 89 | 90 | ``` 91 | 92 | 93 | ``` 94 | 95 | ### Build the application 96 | 97 | Now that we have the application generated let's build out the rest of the shopping cart. 98 | 99 | The starting point for our application is in **src/main.js**. 100 | 101 | ``` 102 | import Vue from 'vue' 103 | import VueRouter from 'vue-router' 104 | import store from './store/index.js' 105 | import App from './App.vue' 106 | import Products from './components/Products.vue' 107 | import Cart from './components/Cart.vue' 108 | 109 | Vue.use(VueRouter) 110 | 111 | // Define routes 112 | const routes = [ 113 | { path: '/', component: Products }, 114 | { path: '/cart', component: Cart } 115 | ] 116 | 117 | // Register routes 118 | const router = new VueRouter({ 119 | routes 120 | }) 121 | 122 | new Vue({ 123 | el: '#app', 124 | render: h => h(App), 125 | router, 126 | store 127 | }) 128 | ``` 129 | 130 | In this file we add code to register our routes and components. We also attach the store to the global Vue app. If you are unfamiliar with ES6 syntax, `store` in an object is the same as `store: store`. 131 | 132 | In our main App.vue component we will register the base template for our Vue application complete with the NavBar. 133 | 134 | **src/App.vue** 135 | 136 | ``` 137 | 143 | 144 | 157 | ``` 158 | 159 | The product and cart pages will render in the `` element. We can come back to the NavBar a little later, but the full source code for it is available [here](https://github.com/connor11528/vuex-shopping-cart/blob/master/src/components/NavBar.vue). 160 | 161 | ### Mutations 162 | 163 | Now that we have our application scaffold set up, create a new folder called **src/store** that will have an **index.js** and a **mutation-types.js** file. The mutation types file defines what mutations can occur within our application: 164 | 165 | ``` 166 | export const ADD_TO_CART = 'ADD_TO_CART' 167 | ``` 168 | 169 | What is a [mutation](https://vuex.vuejs.org/en/mutations.html) you ask? Mutations change the state of our application. In this case the one thing our application does is modifies the cart state to feature new products as customers add them. Mutations must be synchronous, so no ajax calls belong in mutations. 170 | 171 | We have the **mutation-types.js** file so that our mutations are defined in one place and we can see at a glance all the mutations to the state our app can possibly make. 172 | 173 | In the **src/store/index.js** define our mutations and register our store: 174 | 175 | ``` 176 | import Vue from 'vue' 177 | import Vuex from 'vuex' 178 | import * as types from './mutation-types' 179 | 180 | Vue.use(Vuex) 181 | 182 | const debug = process.env.NODE_ENV !== 'production' 183 | 184 | // mutations 185 | const mutations = { 186 | 187 | [types.ADD_TO_CART] (state, { id }) { 188 | const record = state.added.find(p => p.id === id) 189 | 190 | if (!record) { 191 | state.added.push({ 192 | id, 193 | quantity: 1 194 | }) 195 | } else { 196 | record.quantity++ 197 | } 198 | } 199 | } 200 | 201 | // one store for entire application 202 | export default new Vuex.Store({ 203 | state, 204 | strict: debug, 205 | getters, 206 | actions, 207 | mutations 208 | }) 209 | ``` 210 | 211 | ### State, Getters and Actions 212 | 213 | In the above code we are missing values for `store`, `getters` and `actions`. Those are defined below: 214 | 215 | ``` 216 | // initial state 217 | const state = { 218 | added: [], 219 | all: [ 220 | { 221 | id: 'cc919e21-ae5b-5e1f-d023-c40ee669520c', 222 | name: 'COBOL 101 vintage', 223 | description: 'Learn COBOL with this vintage programming book', 224 | price: 399 225 | }, 226 | { 227 | id: 'bcd755a6-9a19-94e1-0a5d-426c0303454f', 228 | name: 'Sharp C2719 curved TV', 229 | description: 'Watch TV like never before with the brand new curved screen technology', 230 | price: 1995 231 | }, 232 | { 233 | id: '727026b7-7f2f-c5a0-ace9-cc227e686b8e', 234 | name: 'Remmington X mechanical keyboard', 235 | description: 'Excellent for gaming and typing, this Remmington X keyboard ' + 236 | 'features tactile, clicky switches for speed and accuracy', 237 | price: 595 238 | } 239 | ] 240 | } 241 | 242 | // getters 243 | const getters = { 244 | allProducts: state => state.all, // would need action/mutation if data fetched async 245 | getNumberOfProducts: state => (state.all) ? state.all.length : 0, 246 | cartProducts: state => { 247 | return state.added.map(({ id, quantity }) => { 248 | const product = state.all.find(p => p.id === id) 249 | 250 | return { 251 | name: product.name, 252 | price: product.price, 253 | quantity 254 | } 255 | }) 256 | } 257 | } 258 | 259 | // actions 260 | const actions = { 261 | addToCart({ commit }, product){ 262 | commit(types.ADD_TO_CART, { 263 | id: product.id 264 | }) 265 | } 266 | } 267 | ``` 268 | 269 | To start with are instantiating a global state with three products and setting our cart (`added`) to an empty array. If you wanted to load these products in via an ajax call we would require additional mutations to add in products to the state. For the sake of simplicity we are setting up the state with our products statically defined. 270 | 271 | Getters are for accessing state values. The [official docs](https://vuex.vuejs.org/en/getters.html) define getters as similar to computed properties for stores. In the above code we have a getter to access the products, the number of products and the products within our cart. The `map` function creates an array specifically for the `added` key that retains information about the quantity of a particular product the customer would like to order. 272 | 273 | ![](https://image.ibb.co/iAagG5/Screen_Shot_2017_09_14_at_10_31_14_AM.png) 274 | 275 | ### Vue.js Components 276 | 277 | Now that we have defined our routes and store it's time to build out the components for our application. In the **src/components** folder add **Cart.vue**, **Products.vue** and **NavBar.vue** files. 278 | 279 | The Products component will render a list of the products for sale and feature "Add to Cart" buttons for customers to buy the products. 280 | 281 | **src/components/Product.vue** 282 | ``` 283 | 307 | 308 | 322 | ``` 323 | 324 | The `mapGetters` function pairs keys to the results of the getter functions. This means that in our local component state their are `products` and `length` values that map to our global store. These values are reactive so that if our state updates within another component these values will also be updates. The `mapActions` helper maps local methods to the actions we defined within our store. 325 | 326 | We can see the value of this when we define our NavBar. In the NavBar we want to show the number of products the user currently has in their cart. To do this we will read from the same state as the products component. Instead of communicating state and forth between siblings and children they read from a single source of truth, the store. 327 | 328 | ![](https://image.ibb.co/cz1P3k/Screen_Shot_2017_09_14_at_10_42_24_AM.png) 329 | 330 | The Javascript for the NavBar looks like this: 331 | 332 | **src/components/NavBar.vue** 333 | ``` 334 | import { mapGetters } from 'vuex' 335 | 336 | export default { 337 | computed: { 338 | itemsInCart(){ 339 | let cart = this.$store.getters.cartProducts; 340 | return cart.reduce((accum, item) => accum + item.quantity, 0) 341 | } 342 | } 343 | } 344 | ``` 345 | 346 | And the relevant HTML within the Vue template: 347 | 348 | ``` 349 | 361 | ``` 362 | The `itemsInCart` value draws its data from the store and the get cartProducts call. We make modifications to the data in our computed property so that we display the current number of items the customer would like to purchase. The classes here add in styling from the Bulma CSS library and the `` components render anchor tags for navigation. 363 | 364 | Finally, we have the Cart component for rendering the items the user would like to buy and the total cost for their order. 365 | 366 | **src/components/Cart.vue** 367 | 368 | ``` 369 | 401 | 402 | 423 | ``` 424 | 425 | Here we use computed properties with the mapGetters helper function to bind the `added` value of the store to `products` in the local component scope. We read this one to our calculate our `total` in a value local to this component but that derives its information from the global store. Lastly, we add a method for checkout that shows an alert for the total amount of money the user owes. If you are interested in building out real life payment processing you can view [this tutorial](https://codeburst.io/standing-on-the-shoulders-of-giants-node-js-vue-2-stripe-heroku-and-amazon-s3-c6fe03ee1118) 426 | 427 | ### Conclusion 428 | 429 | Vuex is a state management package for Vue.js that handles modifications to the state tree within a single source of truth called aptly the State. The State can only be modified with Mutators which must be syncronous functions. To run asynchronous functions or perform other tasks we can define Actions which can be called from components and ultimately call Mutators. We can access the State values within components through Getter functions. The `mapGetters` and `mapActions` can simplify our component definitions. 430 | 431 | Though Vuex brings in many new concepts to Vue.js application development it can be very helpful in managing complex application state. It is not necessary or required for many Vue.js applications but learning knowing about it can make you a better, more productive developer. 432 | 433 | 434 | ## Further resources and groups 435 | 436 | Vue.js SF - https://www.meetup.com/VuejsSF/ 437 | 438 | Vue.sf - https://www.meetup.com/vue-sf/ 439 | 440 | Vue Medium publication - https://medium.com/js-dojo 441 | 442 | ## Tools 443 | 444 | Caching - https://github.com/superwf/vuex-cache 445 | 446 | Persist State - https://github.com/robinvdvleuten/vuex-persistedstate 447 | 448 | UI Toolkit - https://github.com/ElemeFE/element 449 | 450 | 451 | ## Build Setup 452 | 453 | ``` bash 454 | # install dependencies 455 | npm install 456 | 457 | # serve with hot reload at localhost:8080 458 | npm run dev 459 | 460 | # build for production with minification 461 | npm run build 462 | ``` 463 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | vuex-shopping-cart 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuex-shopping-cart", 3 | "description": "a shopping cart built with vue and vuex", 4 | "version": "1.0.0", 5 | "author": "Connor Leech ", 6 | "private": true, 7 | "scripts": { 8 | "dev": "cross-env NODE_ENV=development webpack-dev-server --open --hot", 9 | "build": "cross-env NODE_ENV=production webpack --progress --hide-modules" 10 | }, 11 | "dependencies": { 12 | "vue": "^2.3.3", 13 | "vue-router": "^2.7.0", 14 | "vuex": "^2.4.0" 15 | }, 16 | "devDependencies": { 17 | "babel-core": "^6.0.0", 18 | "babel-loader": "^6.0.0", 19 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 20 | "babel-preset-env": "^1.5.1", 21 | "babel-preset-stage-3": "^6.24.1", 22 | "cross-env": "^3.0.0", 23 | "css-loader": "^0.25.0", 24 | "file-loader": "^0.9.0", 25 | "node-sass": "^4.5.3", 26 | "sass-loader": "^5.0.1", 27 | "style-loader": "^0.18.2", 28 | "vue-loader": "^12.1.0", 29 | "vue-template-compiler": "^2.3.3", 30 | "webpack": "^2.6.1", 31 | "webpack-dev-server": "^2.4.5" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 21 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/connor11528/vuex-shopping-cart/2351de9278344d60ec676d714eab34417782fc8f/src/assets/logo.png -------------------------------------------------------------------------------- /src/components/Cart.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | -------------------------------------------------------------------------------- /src/components/NavBar.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 56 | 57 | 69 | -------------------------------------------------------------------------------- /src/components/Products.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 40 | 41 | 44 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | import store from './store/index.js' 4 | import App from './App.vue' 5 | import Products from './components/Products.vue' 6 | import Cart from './components/Cart.vue' 7 | 8 | Vue.use(VueRouter) 9 | 10 | // Define routes 11 | const routes = [ 12 | { path: '/', component: Products }, 13 | { path: '/cart', component: Cart } 14 | ] 15 | 16 | // Register routes 17 | const router = new VueRouter({ 18 | routes 19 | }) 20 | 21 | new Vue({ 22 | el: '#app', 23 | render: h => h(App), 24 | router, 25 | store 26 | }) 27 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import * as types from './mutation-types' 4 | 5 | Vue.use(Vuex) 6 | 7 | const debug = process.env.NODE_ENV !== 'production' 8 | 9 | // initial state 10 | const state = { 11 | added: [], 12 | all: [ 13 | { 14 | id: 'cc919e21-ae5b-5e1f-d023-c40ee669520c', 15 | name: 'COBOL 101 vintage', 16 | description: 'Learn COBOL with this vintage programming book', 17 | price: 399 18 | }, 19 | { 20 | id: 'bcd755a6-9a19-94e1-0a5d-426c0303454f', 21 | name: 'Sharp C2719 curved TV', 22 | description: 'Watch TV like never before with the brand new curved screen technology', 23 | price: 1995 24 | }, 25 | { 26 | id: '727026b7-7f2f-c5a0-ace9-cc227e686b8e', 27 | name: 'Remmington X mechanical keyboard', 28 | description: 'Excellent for gaming and typing, this Remmington X keyboard ' + 29 | 'features tactile, clicky switches for speed and accuracy', 30 | price: 595 31 | } 32 | ] 33 | } 34 | 35 | // getters 36 | const getters = { 37 | allProducts: state => state.all, // would need action/mutation if data fetched async 38 | getNumberOfProducts: state => (state.all) ? state.all.length : 0, 39 | cartProducts: state => { 40 | return state.added.map(({ id, quantity }) => { 41 | const product = state.all.find(p => p.id === id) 42 | 43 | return { 44 | name: product.name, 45 | price: product.price, 46 | quantity 47 | } 48 | }) 49 | } 50 | } 51 | 52 | // actions 53 | const actions = { 54 | addToCart({ commit }, product){ 55 | commit(types.ADD_TO_CART, { 56 | id: product.id 57 | }) 58 | } 59 | } 60 | 61 | // mutations 62 | const mutations = { 63 | 64 | [types.ADD_TO_CART] (state, { id }) { 65 | const record = state.added.find(p => p.id === id) 66 | 67 | if (!record) { 68 | state.added.push({ 69 | id, 70 | quantity: 1 71 | }) 72 | } else { 73 | record.quantity++ 74 | } 75 | } 76 | } 77 | 78 | // one store for entire application 79 | export default new Vuex.Store({ 80 | state, 81 | strict: debug, 82 | getters, 83 | actions, 84 | mutations 85 | }) 86 | -------------------------------------------------------------------------------- /src/store/mutation-types.js: -------------------------------------------------------------------------------- 1 | export const ADD_TO_CART = 'ADD_TO_CART' -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var webpack = require('webpack') 3 | 4 | module.exports = { 5 | entry: './src/main.js', 6 | output: { 7 | path: path.resolve(__dirname, './dist'), 8 | publicPath: '/dist/', 9 | filename: 'build.js' 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.vue$/, 15 | loader: 'vue-loader', 16 | options: { 17 | loaders: { 18 | // Since sass-loader (weirdly) has SCSS as its default parse mode, we map 19 | // the "scss" and "sass" values for the lang attribute to the right configs here. 20 | // other preprocessors should work out of the box, no loader config like this necessary. 21 | 'scss': 'vue-style-loader!css-loader!sass-loader', 22 | 'sass': 'vue-style-loader!css-loader!sass-loader?indentedSyntax' 23 | } 24 | // other vue-loader options go here 25 | } 26 | }, 27 | { 28 | test: /\.js$/, 29 | loader: 'babel-loader', 30 | exclude: /node_modules/ 31 | }, 32 | { 33 | test: /\.(png|jpg|gif|svg)$/, 34 | loader: 'file-loader', 35 | options: { 36 | name: '[name].[ext]?[hash]' 37 | } 38 | } 39 | ] 40 | }, 41 | resolve: { 42 | alias: { 43 | 'vue$': 'vue/dist/vue.esm.js' 44 | } 45 | }, 46 | devServer: { 47 | historyApiFallback: true, 48 | noInfo: true 49 | }, 50 | performance: { 51 | hints: false 52 | }, 53 | devtool: '#eval-source-map' 54 | } 55 | 56 | if (process.env.NODE_ENV === 'production') { 57 | module.exports.devtool = '#source-map' 58 | // http://vue-loader.vuejs.org/en/workflow/production.html 59 | module.exports.plugins = (module.exports.plugins || []).concat([ 60 | new webpack.DefinePlugin({ 61 | 'process.env': { 62 | NODE_ENV: '"production"' 63 | } 64 | }), 65 | new webpack.optimize.UglifyJsPlugin({ 66 | sourceMap: true, 67 | compress: { 68 | warnings: false 69 | } 70 | }), 71 | new webpack.LoaderOptionsPlugin({ 72 | minimize: true 73 | }) 74 | ]) 75 | } 76 | --------------------------------------------------------------------------------