VueX Modulator - Part 1: generating VueX modules
This article will be a step by step explanation of the creation of a VueX module generator. Other articles will follow concerning a _data dispatcher_ and the usage of all this with Nativescript-VueWhen I started using Vue last year I worked with an API, so the data I got from it was nice and structured. I used to create one VueX module per data type (i.e.: users, posts, …) and I quickly hit these questions: How can I manage to automatize the module creation? How can I dispatch incoming data in the right module?
I answered to those two questions with this: I need a module generator, and some kind of data dispatcher. So I wrote them.
This article will be a step by step explanation of the creation of a module generator for VueX. A few other articles will follow concerning creation of a VueX “data dispatcher” and the usage of all this with Nativescript-Vue
Before we continue here, I assume you already use VueX.
We’ll use an imaginary library called api
to make api calls. Every calls returns a promise.
A repo exists if you want to check the file structure the commits, etc:
[github-repo-card:el-cms/example-vuex-module-generator]
VueX modules are repetitive
A VueX module have this typical structure:
const exampleModule = {
// Current state
state: {
message: '',
},
// Sync methods to change the state
mutations: {
setMessage: (state, message) => { state.message = message },
},
// Async methods that prepare state change
actions: {
SET_MESSAGE: ({commit}, message) => { commit('setMessage', message)},
},
// Methods to get data from the store.
getters: {
GET_MESSAGE: state => state.message,
},
}
// I used to write actions in SCREAMING_CASE, but it's changing ;)
For a post
data type with CRUD actions, the module would look like
// We need Vue to ensure proper updates in mutations
import Vue from 'vue'
const articlesModule = {
// Current state
state: {},
// sync methods to change the state
mutations: {
setArticle: (state, article) => { Vue.set(state, article.id, article) },
removeArticle: (state, id) => {Vue.delete(state, id)},
},
// async methods that prepare state change (can be considered as setters)
actions: {
LOAD_ARTICLES: ({commit}) => {
api.get('articles')
.then((data) => {
for (let i = 0; i < data.length; i++) {
commit('setArticle', data[i])
}
})
.catch((error) => {console.log('ERROR in LOAD_ARTICLES:', error)})
},
LOAD_ARTICLE: ({commit}, id) => {
api.get(`articles/${id}`)
.then((data) => {commit('setArticle', data)})
.catch((error) => {console.log('ERROR in LOAD_ARTICLE:', error)})
},
CREATE_ARTICLE: ({commit}, payload) => {
api.post('articles', payload)
.then((data) => {
// Let's assume "data" is the new article, sent back by the API
// We don't want to store the user input in our store :)
commit('setArticle', data)
})
.catch((error) => {console.log('ERROR in CREATE_ARTICLE:', error)})
},
UPDATE_ARTICLE: ({commit}, payload) => {
api.patch(`articles/${payload.id}`, payload)
.then((data) => {
// Let's assume "data" is the updated article
commit('setArticle', data)
})
.catch((error) => {console.log('ERROR in UPDATE_ARTICLE:', error)})
},
DESTROY_ARTICLE: ({commit}, id) => {
api.delete(`articles/${id}`)
.then(() => { commit('removeArticle', id) })
.catch((error) => {console.log('ERROR in DESTROY_ARTICLE:', error)})
},
},
// methods to get data from the store.
getters: {
articles: state => state,
articles
},
}
That’s a lot of characters, and if we want to add, let’s say a getter, on a module, we have to update every modules. It’s hard to maintain, and not fun to write.
Creating a module generator
(I called mine “Modulator”, for MODULe generATOR, so it’s the name used here.)
Now if I wanted a data type for comments, the only changing things are the endpoints (“comments” instead of “and methods names.
But what if:
- Q: a data type should not be created?, deleted? …
- A: Event if the API should do the control, but it’s better to turn off the possibility on the front too, so our method must create an object conditionally
- Q: the data type is something like “people”?
- A: well, you don’t want to write
getPersons
in your code, so we’ll have to handle plurals.
- A: well, you don’t want to write
- Q: the data type is something like “a_composed_data_type”?
- A You don’t want to write
setA_composed_data_type
, so I propose the following:- generate snake_cased_methods mutators
- generate camelCased actions and getters (by doing this we’ll need a function for the conversion)
- A You don’t want to write
Here is what the generator might be:
// Let's save this file: src/modulator/index.js
// We need Vue to ensure proper updates in mutations
import Vue from 'vue'
const camelize = function (snakeText, capitalizeFirstLetter = true) {
let regexp = /(_\w)/g
if (capitalizeFirstLetter) {
regexp = /(^\w|_\w)/g
}
const out = snakeText.replace(regexp, (match) => {
if (match.length > 1) {
return match[1].toUpperCase()
} else {
return match.toUpperCase()
}
})
console.log(snakeText, capitalizeFirstLetter, out)
return out
}
export default function (endpoint, singular, plural, editable = true, destroyable = true) {
// Prepare some SCREAMING_NAMES
const lowCamelSingular = camelize(singular, false)
const lowCamelPlural = camelize(plural, false)
const camelSingular = camelize(singular)
const camelPlural = camelize(plural)
// Empty module
const module = {state: {}, mutations: {}, actions: {}, getters: {}}
// Mutators:
module.mutations[`set_${singular}`] = (state, entity) => { Vue.set(state, entity.id, entity) }
module.mutations[`remove_${singular}`] = (state, id) => {Vue.delete(state, id)}
// Actions:
module.actions[`load${camelPlural}`] = ({commit}) => {
api.get(endpoint)
.then((data) => {
for (let i = 0; i < data.length; i++) {
commit(`set_${singular}`, data[i])
}
})
.catch((error) => {console.log(`ERROR in load${camelPlural}:`, error)})
}
module.actions[`load${camelSingular}`] = ({commit}, id) => {
api.get(`${endpoint}/${id}`)
.then((data) => {commit(`set_${singular}`, data)})
.catch((error) => {console.log(`ERROR in load${camelSingular}:`, error)})
}
if (editable) {
module.actions[`create${camelSingular}`] = ({commit}, payload) => {
api.post(endpoint, payload)
.then((data) => {
// Let's assume "data" is the new article, sent back by the API
// We don't want to store the user input in our store :)
commit(`set_${singular}`, data)
})
.catch((error) => {console.log(`ERROR in create${camelSingular}:`, error)})
}
module.actions[`update${camelSingular}`] = ({commit}, payload) => {
api.patch(`${endpoint}/${payload.id}`, payload)
.then((data) => {
// Let's assume "data" is the updated article
commit(`set_${singular}`, data)
})
.catch((error) => {console.log(`ERROR in update${camelSingular}:`, error)})
}
}
if (destroyable) {
module.actions[`destroy${camelSingular}`] = ({commit}, id) => {
api.delete(`${endpoint}/${id}`)
.then(() => { commit(`remove_${singular}`, id) })
.catch((error) => {console.log('ERROR in DESTROY_ARTICLE:', error)})
}
}
// Getters
module.getters[lowCamelPlural] = state => state
module.getters[lowCamelSingular] = state => id => state[id] || undefined
return module
}
Note: If the API have different endpoints, as article/1
and articles
,
you can create a parameter like this: endpoint = {one: '', many: ''}
and use it like api.get(`${endpoint.one}/${id}`)
You can now use it in your store.
Basic usage of the Modulator
For the integration and usage, I assume you have this setup:
// src/main.js
import Vue from 'vue'
// ... other imports
import store from './store'
//...
new Vue({
// ...
store,
// ...
})
// src/store.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
//... your store
})
To use the new modulator, edit src/store/index.js
:
// src/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import moduleGenerator from './modulator'
Vue.use(Vuex)
export default new Vuex.Store({
// ...
modules: {
users: moduleGenerator('users', 'user', 'users', true, false),
posts: moduleGenerator('posts', 'post', 'posts'),
postTypes: moduleGenerator('post_types', 'post_type', 'post_types'),
}
})
This is the end of the first part; corresponding with commit #866fd148.
Custom modules
With this module generator, we can create more getters, setters, etc… for all our content types, that’s very generic. But what if we need to customize one of the generated modules?
The proposed solution here is to export an object with all the different methods used in module generation instead of the current function.
Decomposition
We should decompose the modulator in more methods: one for each section of a module:
const mutations = function (singular) {
const mutations = {}
mutations[`set_${singular}`] = (state, entity) => { Vue.set(state, entity.id, entity) }
mutations[`remove_${singular}`] = (state, id) => {Vue.delete(state, id)}
return mutations
}
const actions = function(endpoint, singular, plural, editable = true, destroyable = true){
const camelSingular = camelize(singular)
const camelPlural = camelize(plural)
const actions = {}
actions[`load${camelPlural}`] = ({commit}) => {
api.get(endpoint)
.then((data) => {
for (let i = 0; i < data.length; i++) {
commit(`set_${singular}`, data[i])
}
})
.catch((error) => {console.log(`ERROR in load${camelPlural}:`, error)})
}
actions[`load${camelSingular}`] = ({commit}, id) => {
api.get(`${endpoint}/${id}`)
.then((data) => {commit(`set_${singular}`, data)})
.catch((error) => {console.log(`ERROR in load${camelSingular}:`, error)})
}
if (editable) {
actions[`create${camelSingular}`] = ({commit}, payload) => {
api.post(endpoint, payload)
.then((data) => {
// Let's assume "data" is the new article, sent back by the API
// We don't want to store the user input in our store :)
commit(`set_${singular}`, data)
})
.catch((error) => {console.log(`ERROR in create${camelSingular}:`, error)})
}
actions[`update${camelSingular}`] = ({commit}, payload) => {
api.patch(`${endpoint}/${payload.id}`, payload)
.then((data) => {
// Let's assume "data" is the updated article
commit(`set_${singular}`, data)
})
.catch((error) => {console.log(`ERROR in update${camelSingular}:`, error)})
}
}
if (destroyable) {
actions[`destroy${camelSingular}`] = ({commit}, id) => {
api.delete(`${endpoint}/${id}`)
.then(() => { commit(`remove_${singular}`, id) })
.catch((error) => {console.log('ERROR in DESTROY_ARTICLE:', error)})
}
}
}
const getters = function (singular, plural) {
const lowCamelSingular = camelize(singular, false)
const lowCamelPlural = camelize(plural, false)
const getters = {}
getters[lowCamelPlural] = state => state
getters[lowCamelSingular] = state => id => state[id] || undefined
return getters
}
Make a function out of the previously exported function:
// export default function (endpoint, singular, plural, editable = true, destroyable = true) {
// ... old code
// }
// becomes:
export default function (endpoint, singular, plural, editable = true, destroyable = true) {
const module = {
state: {}, mutations: {
...mutations(singular),
}, actions: {
...actions(endpoint, singular, plural, editable, destroyable),
}, getters: {
...getters(singular, plural),
},
}
return module
}
See commit #c1b04031
Recomposition
Now we have exactly the same behavior as before, but with smaller methods. So it does not answer the question “How to customize a module created with the generator?”.
The idea is now to export all these new methods, so we can compose our modules on demand: move them in an object and export it.
export default {
mutations (singular) {
// code
},
actions (endpoint, singular, plural, editable = true, destroyable = true) {
// code
},
getters (singular, plural) {
// code
},
generateModule (endpoint, singular, plural, editable = true, destroyable = true) {
return {
state: {},
mutations: {
...this.mutations(singular),
}, actions: {
...this.actions(endpoint, singular, plural, editable, destroyable),
}, getters: {
...this.getters(singular, plural),
},
}
},
}
As the export changed, we should update src/store.js
: commit #3f067022
// src/store.js
import Vue from 'vue'
import Vuex from 'vuex'
import Modulator from './modulator'
Vue.use(Vuex)
export default new Vuex.Store({
state: {},
mutations: {},
actions: {},
modules: {
users: Modulator.generateModule('users', 'user', 'users', true, false),
posts: Modulator.generateModule('posts', 'post', 'posts'),
postTypes: Modulator.generateModule('post_types', 'post_type', 'post_types'),
}
})
Usage
Now we can build custom modules:
export default new Vuex.Store({
state: {},
mutations: {},
actions: {},
modules: {
users: Modulator.generateModule('users', 'user', 'users', true, false),
posts: Modulator.generateModule('posts', 'post', 'posts'),
postTypes: Modulator.generateModule('post_types', 'post_type', 'post_types'),
comments: {
state: {},
mutations: {
...Modulator.mutations('comment'),
my_custom_mutation: (state, data) => {/* do something */},
},
actions: {
...Modulator.actions('comments', 'comment', 'comments'),
createComment: ({commit}, payload) => {/* override an action from the Modulator */},
},
getters: {
...Modulator.getters('comment', 'comments'),
},
},
},
})
Visible in commit #315bc5a8
Conclusion
Creating Vuex module generators has been really useful; I used this system on every project where an API was involved. Sadly, every API is different and there was always small code differences in the way the responses were handled.
I’d really like to make a generic npm dependency out of it, but I don’t know where to start.
If you have a comment about this, leave it here, i’ll be happy to update my post. And if you want to talk about it, join me on Gitter !