Browse Source

Merge pull request #53 from blake-newman/feature/implement-getters-into-store

Feature/implement getters into store
Evan You 9 years ago
parent
commit
d975e9d08d

+ 1 - 0
docs/en/SUMMARY.md

@@ -5,6 +5,7 @@
   - [State](state.md)
   - [Mutations](mutations.md)
   - [Actions](actions.md)
+  - [Getters](getters.md)
 - [Data Flow](data-flow.md)
 - [Application Structure](structure.md)
 - [Middlewares](middlewares.md)

+ 14 - 1
docs/en/api.md

@@ -11,7 +11,7 @@ const store = new Vuex.Store({ ...options })
 ### Vuex.Store Constructor Options
 
 - **state**
-  
+
   - type: `Object`
 
     The root state object for the Vuex store.
@@ -43,6 +43,19 @@ const store = new Vuex.Store({ ...options })
 
     [Details](actions.md)
 
+
+- **getters**
+
+  - type: `Object | Array<Object>`
+
+    An object where each entry's key is the getter name and the value of a function which will receive the state as the first argument.
+
+    Vuex will process these entries and create the actual callable getter functions and expose them on the `getters` property of the store.
+
+    If passing in an Array of Objects, these objects will be automatically merged together into one final object.
+
+    [Details](getters.md)
+
 - **middlewares**
 
   - type: `Array<Object>`

+ 5 - 2
docs/en/concepts.md

@@ -8,6 +8,8 @@ You can use the `Vuex.Store` constructor to create Vuex stores. In most cases, y
 
 - **Actions**: Functions that dispatch mutations. An action can contain asynchronous operations and can dispatch multiple mutations.
 
+- **Getters**: Functions that receive state to return a computed value. Useful for extracting shared computed functions from Vue components.
+
 Why do we differentiate between *mutations* and *actions*, rather then just simple functions that manipulate the state however we want? The reason is because we want to **separate mutation and asynchronicity**. A lot of application complexity roots from the combination of the two. When separated, they both become easier to reason about and write tests for.
 
 > If you are familiar with Flux, note there's a term/concept difference here: Vuex mutations are the equivalent of Flux **actions**, while Vuex actions are equivalent to Flux **action creators**.
@@ -24,8 +26,9 @@ import Vuex from 'vuex'
 const store = new Vuex.Store({
   state: { ... },
   actions: { ... },
-  mutations: { ... }
+  mutations: { ... },
+  getters: { ... }
 })
 ```
 
-Once created, you can access the state via `store.state`, and the actions via `store.actions`. You cannot directly access the mutation functions - they can only be triggered by actions or calling `store.dispatch()`. We will discuss each concept in more details next.
+Once created, you can access the state via `store.state`, the actions via `store.actions` and the getters though `store.getters`. You cannot directly access the mutation functions - they can only be triggered by actions or calling `store.dispatch()`. We will discuss each concept in more details next.

+ 46 - 0
docs/en/getters.md

@@ -0,0 +1,46 @@
+# Getters
+
+It's possible that multiple components will need the same computed property based on Vuex state. Since computed getters are just functions, you can split them out into a separate file so that they can be shared in any component via the store:
+
+``` js
+import Vue from 'vue'
+import Vuex from '../../../src'
+import actions from './actions'
+import mutations from './mutations'
+import getters from './getters'
+
+Vue.use(Vuex)
+
+export default new Vuex.Store({
+  state: { /*...*/ },
+  actions,
+  mutations,
+  getters
+})
+```
+
+``` js
+// getters.js
+export function filteredTodos (state) {
+  return state.messages.filter(message => {
+    return message.threadID === state.currentThreadID
+  })
+}
+```
+
+``` js
+// in a component...
+import { getters } from './store'
+const { filteredTodos } = getters
+
+export default {
+  computed: {
+    filteredTodos
+  }
+}
+
+For an actual example, check out the [Shopping Cart Example](https://github.com/vuejs/vuex/tree/master/examples/shopping-cart).
+For an actual example with hot reload API, check out the [Counter Hot Example](https://github.com/vuejs/vuex/tree/master/examples/counter-hot).
+
+
+This is very similar to [Getters in NuclearJS](https://optimizely.github.io/nuclear-js/docs/04-getters.html).

+ 4 - 23
docs/en/structure.md

@@ -106,28 +106,9 @@ For an actual example, check out the [Shopping Cart Example](https://github.com/
 
 ### Extracting Shared Computed Getters
 
-In large projects, it's possible that multiple components will need the same computed property based on Vuex state. Since computed getters are just functions, you can split them out into a separate file so that they can be shared in any component:
+In large projects, it's possible that multiple components will need the same computed property based on Vuex state. Since computed getters are just functions, you can split them out into a separate file so that they can be shared in any component via the store:
 
-``` js
-// getters.js
-import store from './store'
-
-export function filteredTodos () {
-  return store.state.messages.filter(message => {
-    return message.threadID === store.state.currentThreadID
-  })
-}
-```
-
-``` js
-// in a component...
-import { filteredTodos } from './getters'
-
-export default {
-  computed: {
-    filteredTodos
-  }
-}
-```
+For an actual example, check out the [Shopping Cart Example](https://github.com/vuejs/vuex/tree/master/examples/shopping-cart).
+For an actual example with hot reload API, check out the [Counter Hot Example](https://github.com/vuejs/vuex/tree/master/examples/counter-hot).
 
-This is very similar to [Getters in NuclearJS](https://optimizely.github.io/nuclear-js/docs/04-getters.html).
+For more information, check out the [Getters documentation](getters.md)

+ 7 - 3
examples/counter-hot/Counter.vue

@@ -1,22 +1,26 @@
 <template>
   <div>
-    Clicked: {{ count }} times
+    Value: {{ count }}
     <button @click="increment">+</button>
     <button @click="decrement">-</button>
     <button @click="incrementIfOdd">Increment if odd</button>
     <button @click="incrementAsync">Increment async</button>
+    <div>
+      <div>Recent History: {{recentHistory}}</div>
+    </div>
   </div>
 </template>
 
 <script>
 import store from './store'
-
 export default {
   computed: {
     count () {
       return store.state.count
-    }
+    },
+    recentHistory: store.getters.recentHistory
   },
   methods: store.actions
+
 }
 </script>

+ 10 - 0
examples/counter-hot/store/getters.js

@@ -0,0 +1,10 @@
+export default {
+  recentHistory (state) {
+    const end = state.history.length
+    const begin = end - 5 < 0 ? 0 : end - 5
+    return state.history
+      .slice(begin, end)
+      .toString()
+      .replace(/,/g, ', ')
+  }
+}

+ 9 - 4
examples/counter-hot/store/index.js

@@ -2,26 +2,31 @@ import Vue from 'vue'
 import Vuex from '../../../src'
 import actions from './actions'
 import mutations from './mutations'
+import getters from './getters'
 
 Vue.use(Vuex)
 
 const state = {
-  count: 0
+  count: 0,
+  history: []
 }
 
 const store = new Vuex.Store({
   state,
   actions,
-  mutations
+  mutations,
+  getters
 })
 
 if (module.hot) {
-  module.hot.accept(['./actions', './mutations'], () => {
+  module.hot.accept(['./actions', './mutations', './getters'], () => {
     const newActions = require('./actions').default
     const newMutations = require('./mutations').default
+    const newGetters = require('./getters').default
     store.hotUpdate({
       actions: newActions,
-      mutations: newMutations
+      mutations: newMutations,
+      getters: newGetters
     })
   })
 }

+ 2 - 0
examples/counter-hot/store/mutations.js

@@ -3,8 +3,10 @@ import { INCREMENT, DECREMENT } from './mutation-types'
 export default {
   [INCREMENT] (state) {
     state.count++
+    state.history.push('increment')
   },
   [DECREMENT] (state) {
     state.count--
+    state.history.push('decrement')
   }
 }

+ 1 - 10
examples/shopping-cart/components/Cart.vue

@@ -19,16 +19,7 @@ const { checkout } = store.actions
 
 export default {
   computed: {
-    products () {
-      return store.state.cart.added.map(({ id, quantity }) => {
-        const product = store.state.products.find(p => p.id === id)
-        return {
-          title: product.title,
-          price: product.price,
-          quantity
-        }
-      })
-    },
+    products: store.getters.cartProducts,
     checkoutStatus () {
       return store.state.cart.lastCheckout
     },

+ 10 - 0
examples/shopping-cart/store/getters.js

@@ -0,0 +1,10 @@
+export function cartProducts (state) {
+  return state.cart.added.map(({ id, quantity }) => {
+    const product = state.products.find(p => p.id === id)
+    return {
+      title: product.title,
+      price: product.price,
+      quantity
+    }
+  })
+}

+ 2 - 0
examples/shopping-cart/store/index.js

@@ -1,6 +1,7 @@
 import Vue from 'vue'
 import Vuex from '../../../src'
 import * as actions from './actions'
+import * as getters from './getters'
 import { cartInitialState, cartMutations } from './modules/cart'
 import { productsInitialState, productsMutations } from './modules/products'
 
@@ -15,6 +16,7 @@ export default new Vuex.Store({
     products: productsInitialState
   },
   actions,
+  getters,
   mutations: [cartMutations, productsMutations],
   strict: debug,
   middlewares: debug ? [Vuex.createLogger()] : []

+ 36 - 9
src/index.js

@@ -1,4 +1,4 @@
-import { createAction, mergeObjects, deepClone } from './util'
+import { createAction, validateHotModules, mergeObjects, deepClone } from './util'
 import devtoolMiddleware from './middlewares/devtool'
 import createLogger from './middlewares/logger'
 
@@ -20,6 +20,7 @@ export class Store {
     actions = {},
     mutations = {},
     middlewares = [],
+    getters = {},
     strict = false
   } = {}) {
     // bind dispatch to self
@@ -33,9 +34,11 @@ export class Store {
     })
     this._dispatching = false
     this.actions = Object.create(null)
+    this.getters = Object.create(null)
     this._setupActions(actions)
     this._setupMutations(mutations)
     this._setupMiddlewares(middlewares, state)
+    this._setupGetters(getters)
     // add extra warnings in strict mode
     if (strict) {
       this._setupMutationCheck()
@@ -104,13 +107,16 @@ export class Store {
    *        - {Object} [mutations]
    */
 
-  hotUpdate ({ actions, mutations } = {}) {
+  hotUpdate ({ actions, mutations, getters } = {}) {
     if (actions) {
       this._setupActions(actions, true)
     }
     if (mutations) {
       this._setupMutations(mutations)
     }
+    if (getters) {
+      this._setupGetters(getters, true)
+    }
   }
 
   /**
@@ -163,13 +169,34 @@ export class Store {
     })
     // delete public actions that are no longer present
     // after a hot reload
-    if (hot) {
-      Object.keys(this.actions).forEach(name => {
-        if (!actions[name]) {
-          delete this.actions[name]
-        }
-      })
-    }
+    if (hot) validateHotModules(this.actions, actions)
+  }
+
+  /**
+   * Set up the callable getter functions exposed to components.
+   * This method can be called multiple times for hot updates.
+   * We keep the real getter functions in an internal object,
+   * and expose the public object which are just wrapper
+   * functions that point to the real ones. This is so that
+   * the reals ones can be hot reloaded.
+   *
+   * @param {Object} getters
+   * @param {Boolean} [hot]
+   */
+  _setupGetters (getters, hot) {
+    this._getters = Object.create(null)
+    getters = Array.isArray(getters)
+      ? mergeObjects(getters)
+      : getters
+    Object.keys(getters).forEach(name => {
+      this._getters[name] = (...payload) => getters[name](this.state, ...payload)
+      if (!this.getters[name]) {
+        this.getters[name] = (...args) => this._getters[name](...args)
+      }
+    })
+    // delete public getters that are no longer present
+    // after a hot reload
+    if (hot) validateHotModules(this.getters, getters)
   }
 
   /**

+ 16 - 0
src/util.js

@@ -16,6 +16,22 @@ export function createAction (action, store) {
   }
 }
 
+/**
+ * Validates hot api - unassigns any methods
+ * that do not exist.
+ *
+ * @param {Object} currentMethods
+ * @param {Object} newMethods
+ */
+
+export function validateHotModules (currentMethods, newMethods) {
+  Object.keys(currentMethods).forEach(name => {
+    if (!newMethods[name]) {
+      delete currentMethods[name]
+    }
+  })
+}
+
 /**
  * Merge an array of objects into one.
  *

+ 39 - 1
test/test.js

@@ -31,6 +31,11 @@ describe('Vuex', () => {
       actions: {
         test: TEST
       },
+      getters: {
+        getA (state) {
+          return state.a
+        }
+      },
       mutations: {
         [TEST] (state, n) {
           state.a += n
@@ -39,6 +44,7 @@ describe('Vuex', () => {
     })
     store.actions.test(2)
     expect(store.state.a).to.equal(3)
+    expect(store.getters.getA()).to.equal(3)
   })
 
   it('async action', function (done) {
@@ -92,12 +98,31 @@ describe('Vuex', () => {
             state.c += n
           }
         }
+      ],
+      getters: [
+        {
+          getA (state) {
+            return state.a
+          }
+        },
+        {
+          getB (state) {
+            return state.b
+          },
+
+          getC (state) {
+            return state.c
+          }
+        }
       ]
     })
     store.actions.test(2)
     expect(store.state.a).to.equal(3)
     expect(store.state.b).to.equal(3)
     expect(store.state.c).to.equal(1)
+    expect(store.getters.getA()).to.equal(3)
+    expect(store.getters.getB()).to.equal(3)
+    expect(store.getters.getC()).to.equal(1)
     store.actions.test2(2)
     expect(store.state.c).to.equal(3)
   })
@@ -105,7 +130,8 @@ describe('Vuex', () => {
   it('hot reload', function () {
     const store = new Vuex.Store({
       state: {
-        a: 1
+        a: 1,
+        b: 2
       },
       actions: {
         test: TEST
@@ -114,11 +140,17 @@ describe('Vuex', () => {
         [TEST] (state, n) {
           state.a += n
         }
+      },
+      getters: {
+        getA (state) {
+          return state.b
+        }
       }
     })
     const test = store.actions.test
     test(2)
     expect(store.state.a).to.equal(3)
+    expect(store.getters.getA()).to.equal(2)
     store.hotUpdate({
       actions: {
         test: ({ dispatch }, n) => dispatch(TEST, n + 1)
@@ -127,10 +159,16 @@ describe('Vuex', () => {
         [TEST] (state, n) {
           state.a = n
         }
+      },
+      getters: {
+        getA (state) {
+          return state.a
+        }
       }
     })
     test(999)
     expect(store.state.a).to.equal(1000)
+    expect(store.getters.getA()).to.equal(1000)
   })
 
   it('middleware', function () {