32 |
33 |
34 |
--------------------------------------------------------------------------------
/src/components/NavBar.vue:
--------------------------------------------------------------------------------
1 |
2 |
38 |
39 |
40 |
56 |
57 |
69 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | 
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 | 
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 | 
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 |
138 |
139 |
140 |
141 |
142 |
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 | 
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 |
284 |
285 |
All Products
286 |
{{length}} products
287 |
288 |
289 |
290 |
Name
291 |
Description
292 |
Price
293 |
294 |
295 |
296 |
297 |
298 |
{{product.name}}
299 |
{{product.description}}
300 |
${{product.price}}
301 |
302 |
303 |
304 |
305 |
306 |
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 | 
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 |
370 |
371 |
Your Cart
372 |
373 | Your cart is empty!
374 | Go shopping
375 |
376 |
377 |
378 |
379 |
Name
380 |
Price
381 |
Quantity
382 |
383 |
384 |
385 |
386 |
{{ p.name }}
387 |
${{ p.price }}
388 |
{{ p.quantity }}
389 |
390 |
391 |
Total:
392 |
393 |
${{ total }}
394 |
395 |
396 |
397 |
398 |
399 |
400 |
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 |
--------------------------------------------------------------------------------