Way to make inheritance in Vuex modules
Asked Answered
G

5

16

Im building my app with VueJS and Vuex and I'm facing the issue when I have Multiple modules using the same data fields. Its about API configuration like dat.

getUsers ({ state, commit }) {
    axios.get(urls.API_USER_URL).then( response => {
        let data = response.data;
        parseApi(state, data, 'user');

    }).catch( err => {
        console.log('getUser error: ', err);
    })
},

And another function in other Modules is like

getPosts ({ state, commit }) {
    axios.get(urls.API_POST_URL).then( response => {
        let data = response.data;
        parseApi(state, data, 'posts');

    }).catch( err => {
        console.log('getUser error: ', err);
    })
},

I would like to know if I can just inheritence my Module and add additional datafields / functions in there?

My every module would have message and status field which I getting in response of my API.

export default {
    state : {
        message : "",
        status : 0
    },
    parseApi: function(state, data, property) {
        if (data.hasOwnProperty('message')) {
            state.message = data.message;
        }
        if (data.hasOwnProperty('status')) {
            state.status = data.status;
        }
        if (data.hasOwnProperty(property)) {
            state[property] = data[property];
        }
    }
}

It would be something like that.

Is there a way to write this code once and have it in every module Im using?

EDITED:

I even cant get this apiParse function in there, I need to make muttation for those fields. But repeting it all time is pointless... Any advices?

Gaultheria answered 18/1, 2018 at 17:38 Comment(2)
Did you try it? If so, what happened?Lecce
It doesn't work ofc. I dont know how could I actually hanlde that. any advices?Gaultheria
B
24

I put my reusable vuex code in small classes. E.g.

crud.js

export default class {
    constructor ( endpoint ) {
       this.state = {
          endpoint: endpoint,
          meta:     {},
          status:   null,
          known:    [],
          currentId: null,
       };
       this.getters = {
          id: state => id => state.known.find( o => o.id === id )
       };
       this.actions = {
          async store( context, payload ) {
               *(call to API)*
          },
          async update( context, payload ) {
               *(call to API)*
          },
          *...etc*
      };
      this.mutations = {
         STORED(state, item) {
            state.known.push(item);
         },
         *...etc*
      };
   }
}

Then I can use it in all of my modules:

user.module.js

import Crud from '/crud';
var crud = new Crud('/api/users');

const state = {
   ...crud.state,
};
const getters = {
   ...crud.getters,
};
const actions = {
   ...crud.actions,
};
const mutations = {
   ...crud.mutations,
};

export default {
   namespaced: true,
   state,
   getters,
   actions,
   mutations
};
Boffin answered 13/3, 2019 at 14:28 Comment(2)
Is there any reason to put the endpoint in the module's state? I'm wondering because it's already available in the closure for the "(call to API)" parts.Dramamine
I know, "thanks" comments are discouraged, but this is one of the most elegant answers for the issue I've seen so far. :)Gaza
K
7

Developing a little bit more Erin's response, you can define a base class with common features like this:

export default class BaseModule {
    protected state() {
        return {
            isLoading: false,
        };
    };
    protected getters() {
        return {
            isLoading(s) {
                return s.isLoading;
            },
        };
    };
    protected actions() {
        return {};
    };
    protected mutations() {
        return {
            [START_TRANSACTION]: (s) => {
                s.isLoading = true;
            },
            [END_TRANSACTION]: (s) => {
                s.isLoading = false;
            },
        };
    }
    protected modules() {
        return {};
    };

    public getModule = () => {
        return {
            namespaced: true,
            state: this.state(),
            getters: this.getters(),
            actions: this.actions(),
            mutations: this.mutations(),
            modules: this.modules(),
        };
    }
}

You can now extend/override only the parts you need in derived classes, with class inheritance; for example, if you need to extend the modules...:

import BaseModule from './BaseModule';
import rowDensity from '@/store/modules/reusable/rowDensity';

export default class ItemListModule extends BaseModule {  
  protected modules() {
    return {
      ...super.modules(),
      rowDensity,
    };
  };
}

Finally, to use them as modules in the store, you can instantiate them and call .getModule():

import Vue from 'vue';
import Vuex from 'vuex';
import ItemListModule from './modules/ItemListModule';

Vue.use(Vuex);

const debug = process.env.NODE_ENV !== 'production';

export const MODULE_NAMESPACES = {
  List: 'list',
};

export default new Vuex.Store({
  modules: {
    [MODULE_NAMESPACES.List]: new ItemListModule().getModule(),
  },
  strict: debug,
});
Kaine answered 27/6, 2019 at 12:23 Comment(1)
Amazing! I'd love to see a complete example project, for example for a stock keeping store.Alliber
G
4

I figured out some inheritance with the state fields according to:

https://vuex.vuejs.org/en/modules.html#namespacing

export default {
    namespaced: true,
    state,
    getters,
    actions,
    mutations,
    modules : {
        apiResponses
    }
}

I exported apiResponses module after the module user with namespaced and next i did the same thing with posts.

The namespaces inherited those message / status states and their mutations and which i just called in my user and post module. Now they are working corectly.

My message muttation form apiResponses:

[types.SET_MESSAGE] (state, message) {
    state.message = message;
},

Works inside actions of my user modules

if (data.hasOwnProperty('message')) {
    commit(types.SET_MESSAGE, data.message);
}

Then in my commponent I just call.

    computed: {
        ...mapGetters({
            user : 'user/user',
            userMessage : 'user/message',
            post: 'post/monitoring',
            postMessage : 'post/message',

        }),
    },

EDITED

The last part of my issue is like that.

I got action inside apiResponse Module

let actions = {
    getResponseParsed({commit}, payload) {
        console.log(payload)
        if (payload.data.hasOwnProperty('message')) {
            commit(types.SET_MESSAGE, payload.data.message);
        }
        if (payload.data.hasOwnProperty('status')) {
            commit(types.SET_STATUS, payload.data.status);
        }
        if (payload.data.hasOwnProperty(payload.property)) {
            commit(payload.mutation, payload.data[payload.property]);
        }
    }
}

And then inside my user and other module i called it like:

getUser ({ state, commit, dispatch }) {
    axios.get(urls.API_GET_USER_URL).then( response => {
        let data = response.data;

        dispatch('getResponseParsed', {
            data : data,
            mutation : types.SET_USER,
            property : 'user'
        });

    });
},

And the last thing, we need to make this new module reusable to according to docs we need to create it like a components.

export default {
    state() {
        return {
            message : '',
            status : 0,
        }
    },
    getters,
    mutations,
    actions
}

With the state as function :)

Hope somone else got same issue :D

Gaultheria answered 18/1, 2018 at 19:43 Comment(0)
S
2

here is what I've done:

first of all, I created a mainApi.js whose duty is to just make connection with apis

mainApi.js

import axios from "@/plugins/axios";

export default {
    get(url ,id){
        return axios.get(`/${url}/${id}`);
    },
    getAll(url, filter) {
        return axios.get(`/${url}`, {params: {...filter}});
    },
    create(url ,teBeCreated){
        return axios.post(`/${url}`, teBeCreated);
    },
    update(url ,toBeUpdated){
        return axios.put(`/${url}/${toBeUpdated.oid}`, toBeUpdated);
    },
    delete(url ,id){
        return axios.delete(`/${url}/${id}`);
    },
}

second: I wrote a base class to define needed functions to store data. then this class can be inherited by other store modules.

gate.js

import mainApi from '@/api/main'
import store from '@/store'

export default class {

  constructor() {

    this.state = {
        view: null,
        list: [],
    };

    this.getters = {
        view: (state) => state.view,
        list: (state) => state.list,
    }

    this.mutations = {
        SET_VIEW(state, payload) {
            state.view = payload;
        },
        SET_LIST(state, payload) {
            state.list = payload;
        },
        UN_SET_VIEW(state) {
            state.view = null;
        },
        UN_SET_LIST(state) {
            state.list = [];
        },
    }

    this.actions = {
        get({ commit }, { url, id }) {
            return new Promise((resolve, reject) => {
                mainApi.get(url, id)
                    .then(response => {
                        commit('SET_VIEW', response.data.data);
                        resolve(response)
                    })
                    .catch(error => {
                        console.log("error in get method in gate store: ", error);
                        commit('UN_SET_VIEW');
                        reject(error)
                    })
            });
        },
        getAll({ commit }, { url, filter }) {
            return new Promise((resolve, reject) => {
                mainApi.getAll(url, filter)
                    .then(response => {
                        commit('SET_LIST', response.data.data);
                        resolve(response)
                    })
                    .catch(error => {
                        console.log("error in getAll method in gate store: ", error);
                        commit('UN_SET_LIST');
                        reject(error)
                    })
            });
        },
        create({ commit }, { url, params }) {
            return new Promise((resolve, reject) => {
                mainApi.create(url, params)
                    .then(response => {
                        resolve(response)
                    })
                    .catch(error => {
                        console.log("error in create method in gate store: ", error);
                        reject(error)
                    });
            });
        },
        update({ commit }, { url, params }) {
            return new Promise((resolve, reject) => {
                mainApi.update(url, params)
                    .then(response => {
                        resolve(response)
                    })
                    .catch(error => {
                        console.log("error in update method in gate store: ", error);
                        reject(error)
                    })
            })
        },
        delete({ commit }, { url, id }) {
            return new Promise((resolve, reject) => {
                mainApi.delete(url, id)
                    .then(response => {
                        resolve(response);
                    })
                    .catch(error => {
                        console.log("error in delete method in gate store: ", error);
                        reject(error)
                    })
            });
        },
    }
}

third: now, we can define as many separate store modules as we need. as you can see below, in each module we just need to get the data retrieved from views and pass them to mainApi (gate.js base class's functions and methods are all part of our modules) and manipulate with received data.

someStore.js

import Gate from '@/store/modules/gate'

let gate = new Gate();
const url = 'customUrl'

const gateStates = { ...gate.state }
const gateGetters = { ...gate.getters }
const gateMutations = { ...gate.mutations }

const state = {
    ...gateStates,
};
const getters = {
    ...gateGetters,
};
const mutations = {
    ...gateMutations,
};

const actions = {
    get: ({ commit }, id) => gate.actions.get({ commit }, { url, id }),
    getAll: ({ commit }) => gate.actions.getAll({ commit }, {url, filter: {}}),
    create: ({ commit }, params) => gate.actions.create({ commit }, { url, params }),
    update: ({ commit }, params) => gate.actions.update({ commit }, { url, params }),
    delete: ({ commit }, id) => gate.actions.delete({ commit }, { url, id })
};

export default {
    namespaced: true,
    state,
    getters,
    actions,
    mutations
};

finally we should import our modules and define them as "vuex store modules" so:

store/index.js

import Vue from 'vue'
import Vuex from 'vuex'
import someModule from './modules/someModule'

Vue.use(Vuex)

export default new Vuex.Store({

    state: {},

    mutations: {},

    actions: {},

    modules: {
        someModule
    },

    plugins: {}

})

in this example I used anotherPromise because I needed the server responses directly in my views. if you want to just use responses in your store, there is no need for these Promises and they should be removed as below:

in gate.js change this

get({ commit }, { url, id }) {
    return new Promise((resolve, reject) => {
        mainApi.get(url, id)
            .then(response => {
                commit('SET_VIEW', response.data.data);
                resolve(response)
            })
            .catch(error => {
                commit('UN_SET_VIEW');
                console.log("error in getOne method in gate store: ", error);
                reject(error)
            })
    });
},

to this

get({ commit }, { url, id }) {
    mainApi.get(url, id)
        .then(response => {
            commit('SET_VIEW', response.data.data);
        })
        .catch(error => {
            commit('UN_SET_VIEW');
            console.log("error in getOne method in gate store: ", error);
        })
},

in this way, you have list and view parameters in each module and they can be easily called in your views:

someView.vue

created() {
    store.dispatch('someModule/get', this.$route.params.id)
}

computed: {
    view() {
        return store.getters('someModule/view')
    }
}
Shopping answered 13/11, 2021 at 7:27 Comment(0)
O
0

As a personal challenge I wanted to be able to create a pure ES6 class that could express this need (meaning no annotation allowed). I thus created an AbstractModule class defining the high level operations:

export default class AbstractModule {
    constructor(namespaced = true) {
        this.namespaced = namespaced;
    }

    _state () {
        return {}
    }

    _mutations () {
        return {}
    }

    _actions () {
        return {}
    }

    _getters () {
        return {}
    }

    static _exportMethodList (instance, methods) {
        let result = {};
        // Process methods when specified as array
        if (Array.isArray(methods)) {
            for (let method of methods) {
                if (typeof method === 'string') {
                    result[method] = instance[method].bind(instance);
                }

                if (typeof method === 'function') {
                    result[method.name] = method.bind(instance);
                }

                // else ignore
            }
        }

        // Process methods when specified as plain object
        if (typeof methods === "object") {
            for (const [name, method] of Object.entries(methods)) {
                if (typeof method === 'string') {
                    result[name] = instance[method].bind(instance);
                }

                if (typeof method === 'function') {
                    result[name] = method.bind(instance);
                }
            }
        }

        // Process methods when specified as single string
        if (typeof methods === 'string') {
            result[name] = instance[methods].bind(instance);
        }

        // Process methods when specified as single callback
        if (typeof methods === 'function') {
            result[name] = methods.bind(instance);
        }

        return result;
    }

    static module() {
        let instance = new this();
        console.log(instance);

        return {
            namespaced: instance.namespaced,
            state: instance._state(),
            mutations: AbstractModule._exportMethodList(instance, instance._mutations()),
            actions: AbstractModule._exportMethodList(instance, instance._actions()),
            getters: AbstractModule._exportMethodList(instance, instance._getters())
        }
    }
}

From this I created my own class module by redefining the parent methods I wanted to customize this way:

export default class QuestionModule extends AbstractModule{
    constructor(question) {
        super();
        this.question = question;
    }

    selectLine (state, line) {
        this.question.selectLine(line);
    }

    unselectLine (state, line) {
        this.question.unselectLine(line);
    }

    submit ({ state, commit, rootState }) {
        /** API call */
    }

    _state () {
        return this.question;
    }

    _mutations () {
        return [this.selectLine, this.unselectLine, this.validate];
    }

    _actions () {
        return this.submit;
    }
}

Final step is to declare my class module into the Vuex store (through a call to the module static method):

const store = new Vuex.Store({
  modules: {
      question: QuestionModule.module()
  },
  strict: process.env.NODE_ENV !== 'production'
});
Ovipositor answered 25/7, 2020 at 14:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.