Created
July 13, 2018 22:08
-
-
Save superMDguy/46e28c7c238b35416322ae2688aa2fad to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * 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/START')">Get Documents</button> | |
| <div v-if="documents.pending">Fetching documents...</div> | |
| <div v-else-if="documents.spinning">Displaying a loading indicator because this is taking awhile...</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 | |
| * @param {*} sessionStorageKey - (OPTIONAL) The key to use for persisting to session storage | |
| * | |
| */ | |
| import Vue from 'vue' | |
| export default function buildAsyncModule({ | |
| fnApiCall, // function that optionally accepts a payload parameter and returns a promise | |
| // eslint-disable-next-line no-unused-vars | |
| fnIsEmpty = result => false, // optionally provide value to decide if result is "empty" | |
| key = null, // optionally provide either key to use to check if payloads are equal, or a function that checks if 2 payloads are equal | |
| initialValue = null, // default value | |
| spinnerDelay = 1000, // number of milliseconds to wait after the api call starts before committing the "spinning" mutation | |
| sessionStorageKey = null | |
| }) { | |
| 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') | |
| if (key && typeof key !== 'string' && typeof key !== 'function') | |
| throw new TypeError('Must pass either string or function: key') | |
| if (sessionStorageKey) { | |
| if (key) { | |
| throw new Error('Using both sessionStorageKey and key is not currently supported') | |
| } | |
| if (initialValue) { | |
| console.warn('Using provided initial value instead of session storage key') | |
| } else { | |
| initialValue = JSON.parse(sessionStorage.getItem(sessionStorageKey)) | |
| } | |
| } | |
| if (!key) { | |
| return { | |
| namespaced: true, | |
| state: defaultState(true), | |
| actions: actions(), | |
| mutations: mutations() | |
| } | |
| } else { | |
| const fnKey = payload => { | |
| if (payload != null) { | |
| let keyedPayload = typeof key === 'string' ? payload[key] : key(payload) | |
| if (keyedPayload != null) { | |
| return keyedPayload | |
| } else { | |
| console.error(payload, key) | |
| throw new Error('Payload key is undefined or null') | |
| } | |
| } | |
| } | |
| const genKeyedGetter = propName => { | |
| return state => payload => { | |
| let jobState = state.jobs[fnKey(payload)] | |
| return jobState && jobState[propName] | |
| } | |
| } | |
| let getters = {} | |
| Object.keys(defaultState()).forEach(stateKey => (getters[stateKey] = genKeyedGetter(stateKey))) | |
| return { | |
| namespaced: true, | |
| state: { | |
| jobs: {} | |
| }, | |
| actions: keyedActions(fnKey), | |
| mutations: keyedMutations(), | |
| getters | |
| } | |
| } | |
| function defaultState(initValue = false) { | |
| let defaultState = { | |
| 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 loading indicator should be visible in the UI | |
| } | |
| if (initValue) { | |
| defaultState.value = initialValue | |
| } | |
| return defaultState | |
| } | |
| function actions() { | |
| const TASK_ERROR = 'not newest task' | |
| return { | |
| START({ commit, state }, payload) { | |
| const taskId = (state.activeTaskId || 0) + 1 | |
| commit('ACTIVE_TASK', taskId) | |
| commit('RESET') | |
| commit('PENDING') | |
| setTimeout(() => { | |
| if (state.activeTaskId === taskId && state.pending) { | |
| commit('SPINNING') | |
| } | |
| }, spinnerDelay) | |
| return fnApiCall(payload) | |
| .then(result => { | |
| if (state.activeTaskId === taskId) { | |
| fnIsEmpty(result) ? commit('SET_EMPTY') : commit('SET', result) | |
| if (sessionStorageKey) sessionStorage.setItem(sessionStorageKey, JSON.stringify(result)) | |
| return result | |
| } else { | |
| return Promise.reject(TASK_ERROR) | |
| } | |
| }) | |
| .catch(err => { | |
| if (err === TASK_ERROR) { | |
| return Promise.reject(TASK_ERROR) // pass up the chain so no .then gets called | |
| } | |
| console.error(err) | |
| if (state.activeTaskId === taskId) { | |
| commit('SET_ERROR', err) | |
| } | |
| }) | |
| } | |
| } | |
| } | |
| function keyedActions(fnKey) { | |
| return { | |
| START({ commit, state }, payload) { | |
| let payloadKey = fnKey(payload) | |
| commit('RESET', { key: payloadKey }) | |
| commit('PENDING', { key: payloadKey }) | |
| setTimeout(() => { | |
| if (state.jobs[payloadKey].pending) { | |
| commit('SPINNING', { key: payloadKey }) | |
| } | |
| }, spinnerDelay) | |
| return fnApiCall(payload) | |
| .then(result => { | |
| fnIsEmpty(result) | |
| ? commit('SET_EMPTY', { key: payloadKey }) | |
| : commit('SET', { | |
| key: payloadKey, | |
| payload: result | |
| }) | |
| return result | |
| }) | |
| .catch(error => { | |
| console.error(error) | |
| commit('SET_ERROR', { key: payloadKey }) | |
| }) | |
| } | |
| } | |
| } | |
| function mutations() { | |
| return { | |
| RESET(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 | |
| }, | |
| ACTIVE_TASK(state, taskId) { | |
| state.activeTaskId = taskId | |
| } | |
| } | |
| } | |
| function keyedMutations() { | |
| return { | |
| RESET(state, { key }) { | |
| Vue.set(state.jobs, key, defaultState()) | |
| }, | |
| PENDING(state, { key }) { | |
| state.jobs[key].pending = true | |
| }, | |
| SPINNING(state, { key }) { | |
| state.jobs[key].spinning = true | |
| }, | |
| SET_EMPTY(state, { key }) { | |
| endPending(state.jobs[key]) | |
| state.jobs[key].empty = true | |
| state.jobs[key].error = false | |
| state.jobs[key].hasValue = false | |
| }, | |
| SET_ERROR(state, { key }) { | |
| endPending(state.jobs[key]) | |
| state.jobs[key].empty = false | |
| state.jobs[key].error = true | |
| state.jobs[key].hasValue = false | |
| }, | |
| SET(state, { key, payload }) { | |
| endPending(state.jobs[key]) | |
| state.jobs[key].empty = false | |
| state.jobs[key].error = false | |
| state.jobs[key].hasValue = true | |
| state.jobs[key].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