@phoobynet/vuex-helper v1.0.16
WORK IN PROGRESS - DANGER HERE!
Vuex Helper
Creates namespaced Vuex modules based on a given state object with less coding.
Installation
NPM
npm i --save @phoobynet/vuex-helper
Yarn
yarn add @phoobynet/vuex-helper
Motivation
I built this as a time saver. I was writing a Vue.js application for a large legacy CRM system and was getting a lot of change requests that had become a chore. I wanted a simple way of adding data to state within a module, and not have to worry about add new types, mutations and mappings to components.
Conventions (that I use)
- Vuex modules are always namespaced.
- I don't nest modules (nothing wrong with it, I just choose not to do it)
- Vuex module file names end with
Module.js
, but module names don't, e.g. 'customersModule.js' is registered with the store as 'customers' - Vuex modules are kept smallish.
- Each value of
state
has a matchingtype
- Each
type
has a matching mutation function that has the same name as thetype
and therefore thestate
property.
Example
The following is a very simple module with no actions or getters.
To run the example:
Open a terminal and start the server
npm run run-example-server
Open another terminal and start the client
npm run run-example-client
Navigate to localhost:3001
You should see something similar to this:
customersModule.js (simplified version)
import { buildModule } from '@phoobynet/vuex-helper'
const state = {
fetching: false,
customers: [],
error: null
}
// create the module based on the above state
const customersModule = buildModule('customers', state)
export default customersModule
store.js
import Vue from 'vue'
import Vuex from 'vuex'
import customers from '@/components/customers/customersModule'
Vue.use(Vuex)
const store = new Vuex.Store({
strict: true,
modules: {
customers
}
})
export default store
buildModule
takes the namespace, and the state
and returns a customersModule
with the following properties.
state
- the state passed intobuildModule
namespace
- thenamespace
argument passed intobuildModule
namespaced
- set totrue
. See Vuex NamespacingstateKeys
- array of keys obtained from thestate
objects. Useful for creating mixins.defaultState
- copy ofstate
that can be used to set a state property back to the original value.types
- object where each property and value matches a property ofstate
. Useful when usingcommit
. An error is thrown if an attempt is made to access a property that does not exist.qualifiedTypes
- same astypes
, but each value is prefixed with the namespace. This is useful for commits directly against astore
mutations
- object where each function matches a property oftypes
and thereforestate
. When mapping mutations onto a component use the value ofmutationSettersMap
mutationSettersMap
- object where each property is prefixed withset*
but the value matches a property oftypes
and thereforestate
, e.g.setFetching
maps to thefetching
mutation function. Very useful for avoiding collisions when mapping modules onto components.resetState
- function that takes thecontext.commit
action vuex function to reset the modulestate
back todefaultState
.mixin
- object that can be mixed into components using{ mixins: [] }
property.
{
state,
namespace,
namespaced,
stateKeys,
defaultState,
types,
mutations,
mutationSettersMap,
resetState,
mixin
}
Composing actions
, getters
and custom mutations
Our customersModule
in the previous example is pretty dumb. Let's add the following:
- An action to retrieve
customers
- Add a couple of getters to determine if we actually have any customers and if a error is set (yes, I know, this is contrived and could be achieved in state)
- A custom or override mutation; in this case one to clear an error and the other to a remove a customer. Remember, vuex-helper will create default mutation functions for each
state
property, but they can be overridden. NOTE: themutationSettersMap
is not updated.
customersModule.js (more advanced version)
import {
buildModule,
compose,
composeActions,
composeGetters,
composeMutations
} from '@phoobynet/vuex-helper'
import customersApi from './customersApi'
const state = {
customers: [],
fetching: false,
error: null
}
const { types, resetState, ...customersModule } = buildModule('customers', state)
const getters = {
hasCustomers (state) {
return state.customers.length
},
hasError (state) {
return !!state.error
}
}
const customMutations = {
clearLastError (state) {
state.error = null
},
removeCustomer (state, id) {
state.customers = state.customers.filter(customer => customer.id + '' !== id + '')
}
}
const actions = {
cleanUp ({ commit }) {
resetState(commit)
},
async getCustomers ({ commit }) {
try {
commit(types.fetching, true)
commit(types.customers, [])
commit(types.error, null)
const customers = await customersApi.getCustomers()
commit(types.customers, customers)
} catch (err) {
commit(types.error, err)
console.error(err)
} finally {
commit(types.fetching, false)
}
}
}
export default compose(
composeActions(actions),
composeGetters(getters),
composeMutations(customMutations)
)(customersModule)
To pull actions, getters and custom mutations into our customersModule
, we compose them.
composeActions
adds theactions
object to the module, along with aactionsKeys
array. The defaultmixin.methods
is updated usingmapActions
.composeGetters
adds thegetters
object to the module, along with agettersKeys
array. The defaultmixin.computed
is updated usingmapGetters
composeMutation
overwrites default mutations or adds additional mutations to the module. The defaultmixin.methods
is updated usingmapMutations
compose
acceptsn
compose functions, returning a function that is executed with the result ofbuildModule
(customersModule
in this example)
Preventing name collisions
Calling mutation functions the same name as a state property would cause problems when mapping values and methods onto a component. Instead, when not using the generated <module>.mixin
, do the following:
import customersModule from './customersModule'
import { createNamespacedHelpers } from 'vuex'
const { mapMutations, mapState } = createNamespacedHelpers(customersModule.namespace)
export default {
name: 'Customers',
computed: {
...mapState(customersModule.stateKeys)
},
methods: {
...mapMutations(customersModule.mutationSettersMap),
doSomething () {
// maps to the mutation 'fetching'
this.setFetching(true)
}
}
}
Using <module>.mixin
Dead simple, just import the module, and use the mixin
property.
<template>
<div class="mt-2">
<template v-if="fetching">
<div class="row">
<div class="col">
<div class="text-center mt-5">
<i class="fa fa-spinner fa-spin fa-3x"></i>
</div>
</div>
</div>
</template>
<template v-else-if="hasCustomers">
<div class="row">
<div class="col">
<header>
<h1>Customers</h1>
</header>
</div>
</div>
<div class="row">
<div class="col">
<customer-list
@removeCustomer="onRemoveCustomer"
:customers="customers"
/>
</div>
</div>
</template>
<template v-else-if="hasError">
<pre>{{ JSON.stringify(error, null, 2) }}</pre>
<button
class="btn btn-warning btn-sm"
@click="clearLastError"
>Clear last error
</button>
</template>
<template v-else-if="!hasCustomers">
<div class="text-center">
<p class="text-muted">
No customers here.
</p>
<button
class="btn btn-primary btn-sm text-justify"
@click="refreshCustomers"
>Refresh customers
</button>
</div>
</template>
</div>
</template>
<script>
import CustomerList from './components/CustomerList'
import customersModule from './customersModule'
export default {
name: 'Customers',
components: {
CustomerList
},
mixins: [ customersModule.mixin ],
mounted () {
this.getCustomers()
},
beforeDestroy () {
this.cleanUp()
},
methods: {
refreshCustomers () {
this.getCustomers()
},
onRemoveCustomer (id) {
// custom mutation
this.removeCustomer(id)
}
}
}
</script>