/** | |
* Create a Vuex store module to represent states for an asynchronous API getter. | |
* | |
* Includes defaultState, actions, and mutations for a standard value gotten via asynchronous call. | |
* See defaultState() function for list of states included. | |
* | |
* Usage: | |
* Assuming we have an async call to get documents (getDocuments) which takes a payload object as an arg, here's what we can do: | |
* | |
* ----- store.js ----- | |
import Vue from 'vue' | |
import Vuex from 'vuex' | |
import buildAsyncModule from './vuex-async-module-builder' | |
import actions from './actions' | |
import getters from './getters' | |
import mutations from './mutations' | |
import { getDocuments } from '@/api' | |
const modules = { | |
documents: buildAsyncModule({ fnApiCall: getDocuments }) | |
} | |
export default new Vuex.Store({ | |
actions, | |
getters, | |
modules, | |
mutations, | |
state: rootStateData() | |
}) | |
* ----- Documents.vue ----- | |
<template> | |
<div> | |
<button @click="$store.dispatch('documents/GET')">Get Documents</button> | |
<div v-if="documents.pending">Fetching documents...</div> | |
<div v-else-if="documents.spinning">Displaying delayed spinner while still fetching documents...</div> | |
<div v-else-if="documents.empty">No documents found</div> | |
<div v-else-if="documents.error">Error getting documents</div> | |
<div v-else-if="documents.hasValue"> | |
<pre>{{ documents.value }}</pre> | |
</div> | |
</div> | |
</template> | |
<script> | |
import { mapState } from 'vuex' | |
export default { | |
computed: mapState([ | |
'documents' | |
]) | |
} | |
</script> | |
* | |
* Options object: | |
* @param {*} fnApiCall - The API call to get the item(s) | |
* @param {*} fnIsEmpty - (OPTIONAL) A callback function that when passed the result of the API call, returns true if the result is empty. If your expected result is an array, the default should work fine. | |
* @param {*} initialValue - (OPTIONAL) The store state value assigned initially | |
* @param {*} spinnerDelay - (OPTIONAL) The number of milliseconds to delay before committing the "spinning" mutation | |
* | |
*/ | |
var vuexAsyncModule = (function () { | |
return { build: buildAsyncModule } | |
function buildAsyncModule ({ | |
fnApiCall, | |
fnIsEmpty = (result) => (!result || !result.length), // a default for array values | |
initialValue = [], // a default for array values | |
spinnerDelay = 1000 // number of milliseconds to wait after the api call starts before committing the "spinning" mutation | |
}) { | |
if (typeof fnApiCall !== 'function') throw new TypeError('Must pass functon: fnApiCall') | |
if (typeof fnIsEmpty !== 'function') throw new TypeError('Must be a function: fnIsEmpty') | |
if (!Number.isInteger(spinnerDelay)) throw new TypeError('Must pass number: spinnerDelay') | |
return { | |
namespaced: true, | |
state: defaultState(), | |
actions: actions(), | |
mutations: mutations() | |
} | |
function defaultState () { | |
return { | |
empty: false, // whether we got a value, but it was essentially empty | |
error: false, // whether there was an error getting the value (and so the value is meaningless) | |
hasValue: false, // whether we've gotten a value (non-empty, non-error) from the async call | |
pending: false, // whether we're currently in an async call getting this | |
spinning: false, // whether the spinner should be visible in the UI | |
value: initialValue // the value resulting from the async call | |
} | |
} | |
function actions () { | |
return { | |
GET ({ commit, state }, payload) { | |
commit('RESET') | |
commit('PENDING') | |
setTimeout(() => { | |
if (state.pending) { | |
commit('SPINNING') | |
} | |
}, spinnerDelay) | |
return fnApiCall(payload) | |
.then(result => { | |
fnIsEmpty(result) | |
? commit('SET_EMPTY') | |
: commit('SET', result) | |
return result | |
}) | |
.catch(error => { | |
console.error(error) | |
commit('SET_ERROR') | |
}) | |
} | |
} | |
} | |
function mutations () { | |
return { | |
RESET (state) { | |
state = replaceState(state, defaultState()) | |
}, | |
PENDING (state) { | |
state.pending = true | |
}, | |
SPINNING (state) { | |
state.spinning = true | |
}, | |
SET_EMPTY (state) { | |
endPending(state) | |
state.empty = true | |
state.error = false | |
state.hasValue = false | |
}, | |
SET_ERROR (state) { | |
endPending(state) | |
state.empty = false | |
state.error = true | |
state.hasValue = false | |
}, | |
SET (state, payload) { | |
endPending(state) | |
state.empty = false | |
state.error = false | |
state.hasValue = true | |
state.value = payload | |
} | |
} | |
} | |
} | |
function endPending (state) { | |
state.pending = false | |
state.spinning = false | |
} | |
function replaceState (state, newState) { | |
Object.keys(newState).forEach(newStateKey => { | |
state[newStateKey] = newState[newStateKey] | |
}) | |
} | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment