Browse Source

shopping cart example

Evan You 9 years ago
parent
commit
a116240c6a

+ 21 - 0
examples/shopping-cart/api/shop.js

@@ -0,0 +1,21 @@
+/**
+ * Mocking client-server processing
+ */
+const _products = [
+  {"id": 1, "title": "iPad 4 Mini", "price": 500.01, "inventory": 2},
+  {"id": 2, "title": "H&M T-Shirt White", "price": 10.99, "inventory": 10},
+  {"id": 3, "title": "Charli XCX - Sucker CD", "price": 19.99, "inventory": 5}
+]
+
+export default {
+  getProducts (cb) {
+    setTimeout(() => cb(_products), 100)
+  },
+
+  buyProducts (products, cb, errorCb) {
+    setTimeout(() => {
+      // simulate random checkout failure.
+      Math.random() > 0.5 ? cb() : errorCb()
+    }, 100)
+  }
+}

+ 19 - 0
examples/shopping-cart/components/App.vue

@@ -0,0 +1,19 @@
+<template>
+  <div class="app">
+    <h1>Shopping Cart Example</h1>
+    <hr>
+    <h2>Products</h2>
+    <product-list></product-list>
+    <hr>
+    <cart></cart>
+  </div>
+</template>
+
+<script>
+import ProductList from './ProductList.vue'
+import Cart from './Cart.vue'
+
+export default {
+  components: { ProductList, Cart }
+}
+</script>

+ 47 - 0
examples/shopping-cart/components/Cart.vue

@@ -0,0 +1,47 @@
+<template>
+  <div class="cart">
+    <h2>Your Cart</h2>
+    <p v-show="!cart.added.length"><i>Please add some products to cart.</i></p>
+    <ul>
+      <li v-for="p in products">
+        {{ p.title }} - {{ p.price | currency }} x {{ p.quantity }}
+      </li>
+    </ul>
+    <p>Total: {{ total | currency }}</p>
+    <p><button :disabled="!cart.added.length" @click="checkout">Checkout</button></p>
+    <p v-show="cart.lastCheckout">Checkout {{ cart.lastCheckout }}.</p>
+  </div>
+</template>
+
+<script>
+import vuex from '../vuex'
+const { checkout } = vuex.actions
+
+export default {
+  data () {
+    return {
+      cart: vuex.get('cart')
+    }
+  },
+  computed: {
+    products () {
+      return this.cart.added.map(({ id, quantity }) => {
+        const product = vuex.state.products.find(p => p.id === id)
+        return {
+          title: product.title,
+          price: product.price,
+          quantity
+        }
+      })
+    },
+    total () {
+      return this.products.reduce((total, p) => {
+        return total + p.price * p.quantity
+      }, 0)
+    }
+  },
+  methods: {
+    checkout
+  }
+}
+</script>

+ 36 - 0
examples/shopping-cart/components/ProductList.vue

@@ -0,0 +1,36 @@
+<template>
+  <ul>
+    <li v-for="p in products">
+      {{ p.title }} - {{ p.price | currency }}
+      <br>
+      <button
+        :disabled="!p.inventory"
+        @click="addToCart(p)">
+        Add to cart
+      </button>
+    </li>
+  </ul>
+</template>
+
+<script>
+import vuex from '../vuex'
+const { getAllProducts, addToCart } = vuex.actions
+
+export default {
+  data () {
+    return {
+      products: vuex.get('products')
+    }
+  },
+  created () {
+    getAllProducts()
+  },
+  methods: {
+    addToCart (product) {
+      if (product.inventory > 0) {
+        addToCart(product.id)
+      }
+    }
+  }
+}
+</script>

+ 11 - 0
examples/shopping-cart/index.html

@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <title>vuex shpping cart example</title>
+  </head>
+  <body>
+    <app></app>
+    <script src="example.build.js"></script>
+  </body>
+</html>

+ 8 - 0
examples/shopping-cart/main.js

@@ -0,0 +1,8 @@
+import 'babel-polyfill'
+import Vue from 'vue'
+import App from './components/App.vue'
+
+new Vue({
+  el: 'body',
+  components: { App }
+})

+ 5 - 0
examples/shopping-cart/vuex/action-types.js

@@ -0,0 +1,5 @@
+export const ADD_TO_CART = 'ADD_TO_CART'
+export const CHECKOUT_REQUEST = 'CHECKOUT_REQUEST'
+export const CHECKOUT_SUCCESS = 'CHECKOUT_SUCCESS'
+export const CHECKOUT_FAILURE = 'CHECKOUT_FAILURE'
+export const RECEIVE_PRODUCTS = 'RECEIVE_PRODUCTS'

+ 24 - 0
examples/shopping-cart/vuex/actions.js

@@ -0,0 +1,24 @@
+import shop from '../api/shop'
+import * as types from './action-types'
+
+export const addToCart = types.ADD_TO_CART
+
+export function checkout (products) {
+  return (dispatch, state) => {
+    const savedCartItems = [...state.cart.added]
+    dispatch(types.CHECKOUT_REQUEST)
+    shop.buyProducts(
+      products,
+      () => dispatch(types.CHECKOUT_SUCCESS),
+      () => dispatch(types.CHECKOUT_FAILURE, savedCartItems)
+    )
+  }
+}
+
+export function getAllProducts () {
+  return dispatch => {
+    shop.getProducts(products => {
+      dispatch(types.RECEIVE_PRODUCTS, products)
+    })
+  }
+}

+ 17 - 0
examples/shopping-cart/vuex/index.js

@@ -0,0 +1,17 @@
+import Vue from 'vue'
+import Vuex from '../../../src'
+import * as actions from './actions'
+import { cartInitialState, cartMutations } from './stores/cart'
+import { productsInitialState, productsMutations } from './stores/products'
+
+Vue.use(Vuex)
+Vue.config.debug = true
+
+export default new Vuex({
+  state: {
+    cart: cartInitialState,
+    products: productsInitialState
+  },
+  actions,
+  mutations: [cartMutations, productsMutations]
+})

+ 45 - 0
examples/shopping-cart/vuex/stores/cart.js

@@ -0,0 +1,45 @@
+import {
+  ADD_TO_CART,
+  CHECKOUT_REQUEST,
+  CHECKOUT_SUCCESS,
+  CHECKOUT_FAILURE
+} from '../action-types'
+
+// initial state
+// shape: [{ id, quantity }]
+export const cartInitialState = {
+  added: [],
+  lastCheckout: null
+}
+
+// mutations
+export const cartMutations = {
+  [ADD_TO_CART] ({ cart }, productId) {
+    cart.lastCheckout = null
+    const record = cart.added.find(p => p.id === productId)
+    if (!record) {
+      cart.added.push({
+        id: productId,
+        quantity: 1
+      })
+    } else {
+      record.quantity++
+    }
+  },
+
+  [CHECKOUT_REQUEST] ({ cart }) {
+    // clear cart
+    cart.added = []
+    cart.lastCheckout = null
+  },
+
+  [CHECKOUT_SUCCESS] ({ cart }) {
+    cart.lastCheckout = 'successful'
+  },
+
+  [CHECKOUT_FAILURE] ({ cart }, savedCartItems) {
+    // rollback to the cart saved before sending the request
+    cart.added = savedCartItems
+    cart.lastCheckout = 'failed'
+  }
+}

+ 18 - 0
examples/shopping-cart/vuex/stores/products.js

@@ -0,0 +1,18 @@
+import { RECEIVE_PRODUCTS, ADD_TO_CART } from '../action-types'
+
+// initial state
+export const productsInitialState = []
+
+// mutations
+export const productsMutations = {
+  [RECEIVE_PRODUCTS] (state, products) {
+    state.products = products
+  },
+
+  [ADD_TO_CART] ({ products }, productId) {
+    const product = products.find(p => p.id === productId)
+    if (product.inventory > 0) {
+      product.inventory--
+    }
+  }
+}

+ 1 - 1
examples/todomvc/webpack.config.js → examples/webpack.shared.config.js

@@ -18,7 +18,7 @@ module.exports = {
     ]
   },
   babel: {
-    presets: ['es2015']
+    presets: ['es2015', 'stage-2']
   },
   devtool: 'source-map'
 }

+ 4 - 1
package.json

@@ -4,7 +4,8 @@
   "description": "state management for Vue.js",
   "main": "src/index.js",
   "scripts": {
-    "dev": "cd examples/todomvc && webpack-dev-server --inline --hot"
+    "dev-todomvc": "cd examples/todomvc && webpack-dev-server --inline --hot --config ../webpack.shared.config.js",
+    "dev-cart": "cd examples/shopping-cart && webpack-dev-server --inline --hot --config ../webpack.shared.config.js"
   },
   "repository": {
     "type": "git",
@@ -26,7 +27,9 @@
     "babel-core": "^6.2.1",
     "babel-loader": "^6.2.0",
     "babel-plugin-transform-runtime": "^6.1.18",
+    "babel-polyfill": "^6.2.0",
     "babel-preset-es2015": "^6.1.18",
+    "babel-preset-stage-2": "^6.1.18",
     "babel-runtime": "^6.2.0",
     "css-loader": "^0.21.0",
     "style-loader": "^0.13.0",

+ 48 - 3
src/index.js

@@ -17,9 +17,12 @@ export default class Vuex {
     state = {},
     actions = {},
     mutations = {},
-    middlewares = []
+    middlewares = [],
+    debug = false
   } = {}) {
 
+    this._debug = debug
+
     // use a Vue instance to store the state tree
     this._vm = new Vue({
       data: state
@@ -27,12 +30,18 @@ export default class Vuex {
 
     // create actions
     this.actions = Object.create(null)
+    actions = Array.isArray(actions)
+      ? mergeObjects(actions)
+      : actions
     Object.keys(actions).forEach(name => {
       this.actions[name] = createAction(actions[name], this)
     })
 
     // mutations
-    this._mutations = mutations
+    this._mutations = Array.isArray(mutations)
+      ? mergeObjects(mutations, true)
+      : mutations
+
     // middlewares
     this._middlewares = middlewares
   }
@@ -59,7 +68,11 @@ export default class Vuex {
   dispatch (type, ...payload) {
     const mutation = this._mutations[type]
     if (mutation) {
-      mutation(this.state, ...payload)
+      if (Array.isArray(mutation)) {
+        mutation.forEach(m => m(this.state, ...payload))
+      } else {
+        mutation(this.state, ...payload)
+      }
       this._middlewares.forEach(middleware => {
         middleware({ type, payload }, this.state)
       })
@@ -112,3 +125,35 @@ function createAction (action, vuex) {
     }
   }
 }
+
+/**
+ * Merge an array of objects into one.
+ *
+ * @param {Array<Object>} arr
+ * @param {Boolean} allowDuplicate
+ * @return {Object}
+ */
+
+function mergeObjects (arr, allowDuplicate) {
+  return arr.reduce((prev, obj) => {
+    Object.keys(obj).forEach(key => {
+      const existing = prev[key]
+      if (existing) {
+        // allow multiple mutation objects to contain duplicate
+        // handlers for the same mutation type
+        if (allowDuplicate) {
+          if (Array.isArray(existing)) {
+            existing.push(obj[key])
+          } else {
+            prev[key] = [prev[key], obj[key]]
+          }
+        } else {
+          console.warn(`[vuex] Duplicate action: ${ key }`)
+        }
+      } else {
+        prev[key] = obj[key]
+      }
+    })
+    return prev
+  }, {})
+}