Skip to content

Instantly share code, notes, and snippets.

@superMDguy
Created July 13, 2018 22:08
Show Gist options
  • Select an option

  • Save superMDguy/46e28c7c238b35416322ae2688aa2fad to your computer and use it in GitHub Desktop.

Select an option

Save superMDguy/46e28c7c238b35416322ae2688aa2fad to your computer and use it in GitHub Desktop.
/**
* 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