championswimmer / vuex-module-decorators

TypeScript/ES7 Decorators to create Vuex modules declaratively
https://championswimmer.in/vuex-module-decorators/
MIT License
1.8k stars 170 forks source link

Examples of how to use the modules in a Vue component #80

Closed kpturner closed 5 years ago

kpturner commented 5 years ago

Having been frustrated by vuex-typescript I thought I would give this ago but I find that I am struggling without, apparently, any examples of how to import and use the modules (and the getters, actions etc) in a standard Typescript Vue component.

It looks like it could be a useful library but the README is too lightweight at the moment. Any chance of some better, more complete examples?

championswimmer commented 5 years ago

championswimmer.in/vuex-module-decorators

On Wed 2 Jan, 2019, 11:30 PM Kevin Turner <notifications@github.com wrote:

Having been frustrated by vuex-typescript I thought I would give this ago but I find that I am struggling without, apparently, any examples of how to import and use the modules (and the getters, actions etc) in a standard Typescript Vue component.

It looks like it could be a useful library but the README is too lightweight at the moment. Any chance of some better, more complete examples?

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/championswimmer/vuex-module-decorators/issues/80, or mute the thread https://github.com/notifications/unsubscribe-auth/ABQ_ynq5IVdSW9nG_Jwdq7Z7yVRLdkrgks5u_POkgaJpZM4ZnATN .

kpturner commented 5 years ago

Thanks for the quick response. Maybe the README on the npm package should point to that documentation? Or does it?

Anyway I am still getting troubles. I have a basic modules like this:

import { getModule, Module, VuexModule, Mutation, Action } from 'vuex-module-decorators';

@Module({ stateFactory: true })
class CounterModule extends VuexModule {
    private count: number = 0;

    @Mutation
    public increment(delta: number): void {
        this.count += delta;
    }
    @Mutation
    public decrement(delta: number): void {
        this.count -= delta;
    }

    // action 'incr' commits mutation 'increment' when done with return value as payload
    @Action({ commit: 'increment' })
    public incr(): number {
        return 5;
    }
    // action 'decr' commits mutation 'decrement' when done with return value as payload
    @Action({ commit: 'decrement' })
    public decr(): number {
        return 5;
    }

    get getCount(): number {
        return this.count;
    }

}

export default getModule(CounterModule);

as per your example. I am registering it to the store as directed and that seems OK.

I import it in a Vue component like this:

import counterModule from '../store/modules/test_module'; (possibly this is wrong?)

I set a piece of data like this:

data() {
        return {
            counter: counterModule.getCount
        };
    }

My app crashes with

{ Error: ERR_GET_MODULE_NO_STATICS : Could not get module accessor.
      Make sure your module has name, we can't make accessors for unnamed modules
      i.e. @Module({ 'something' })
    at getModule (/Users/turnerk/Documents/GitHub/private/squareadmin/node_modules/vuex-module-decorators/dist/cjs/index.js:22:15)
    at Object.<anonymous> (src/app/store/modules/test_module.ts:39:0)
    at __webpack_require__ (webpack:/webpack/bootstrap ed6ad46a43b35ba4ffec:25:0)
    at Object.<anonymous> (src/app/store/index.ts:1:0)
    at __webpack_require__ (webpack:/webpack/bootstrap ed6ad46a43b35ba4ffec:25:0)
    at webpackContext (server-bundle.js:4362:9)
    at getModule (.nuxt/store.js:57:0)
    at Object.module.exports.map../index.ts (.nuxt/store.js:22:0)
    at __webpack_require__ (webpack:/webpack/bootstrap ed6ad46a43b35ba4ffec:25:0)
    at Object.<anonymous> (.nuxt/index.js:1:0)
    at __webpack_require__ (webpack:/webpack/bootstrap ed6ad46a43b35ba4ffec:25:0)
    at Object.<anonymous> (.nuxt/server.js:1:0)
    at __webpack_require__ (webpack:/webpack/bootstrap ed6ad46a43b35ba4ffec:25:0)
    at server-bundle.js:92:18
    at Object.<anonymous> (server-bundle.js:95:10)
    at evaluateModule (/Users/turnerk/Documents/GitHub/private/squareadmin/node_modules/vue-server-renderer/build.js:8368:21) statusCode: 500, name: 'NuxtServerError' }
kpturner commented 5 years ago

After much faffing around I defined the module like this:

import { Module, VuexModule, Mutation, Action } from 'vuex-module-decorators';

@Module({ stateFactory: true, name: 'counter' })
export default class CounterModule extends VuexModule {
    private count: number = 0;

    @Mutation
    public increment(delta: number): void {
        this.count += delta;
    }
    @Mutation
    public decrement(delta: number): void {
        this.count -= delta;
    }

    // action 'incr' commits mutation 'increment' when done with return value as payload
    @Action({ commit: 'increment' })
    public incr(): number {
        return 5;
    }
    // action 'decr' commits mutation 'decrement' when done with return value as payload
    @Action({ commit: 'decrement' })
    public decr(): number {
        return 5;
    }

    get getCount(): number {
        return this.count;
    }

}

Then accessed it in my Vue component like this:

import CounterModule from '../store/modules/test_module';
import { getModule } from 'vuex-module-decorators';
let counterModule: CounterModule;

Then

created() {
        counterModule = getModule(CounterModule, this.$store);
}

Then I was able to access methods etc elsewhere - for example

computed: {
        counter() {
            return counterModule.getCount
        }
    }

But it seems a bit contrived. Is this a good way or is there a better way, considering that I am not using vue-class-component ?

championswimmer commented 5 years ago

You might want to take a look at this maybe https://github.com/coding-blocks-archives/realworld-vue-typescript

Should give you a good place to start from.

On Thu 3 Jan, 2019, 1:43 AM Kevin Turner <notifications@github.com wrote:

After much faffing around I defined the module like this:

import { Module, VuexModule, Mutation, Action } from 'vuex-module-decorators';

@Module({ stateFactory: true, name: 'counter' }) export default class CounterModule extends VuexModule { private count: number = 0;

@Mutation
public increment(delta: number): void {
    this.count += delta;
}
@Mutation
public decrement(delta: number): void {
    this.count -= delta;
}

// action 'incr' commits mutation 'increment' when done with return value as payload
@Action({ commit: 'increment' })
public incr(): number {
    return 5;
}
// action 'decr' commits mutation 'decrement' when done with return value as payload
@Action({ commit: 'decrement' })
public decr(): number {
    return 5;
}

get getCount(): number {
    return this.count;
}

}

Then accessed it in my Vue component like this:

import CounterModule from '../store/modules/test_module'; import { getModule } from 'vuex-module-decorators'; let counterModule: CounterModule;

Then

created() { counterModule = getModule(CounterModule, this.$store); }

Then I was able to access methods etc elsewhere - for example

computed: { counter() { return counterModule.getCount } }

But it seems a bit contrived. Is this a good way or is there a better way, considering that I am not using vue-class-component ?

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/championswimmer/vuex-module-decorators/issues/80#issuecomment-450971874, or mute the thread https://github.com/notifications/unsubscribe-auth/ABQ_yvMNj_xWA4fu0yOoW89-SHHKvahKks5u_RMDgaJpZM4ZnATN .

kpturner commented 5 years ago

I think that is where I got my examples from to get this far. Am I barking up the wrong tree?

Ultimately I may be stuffed because this is a nuxt application and in my old modules I can use this.$axios to make API calls from actions, but I do not know how I can get access to nuxt objects through a module written with vuex-module-decorators

kpturner commented 5 years ago

I have it all working OK now (even with nuxtjs.$axios) but I am still having to use getModule within the components themselves so I am able to pass the store to it.

In all your examples in https://github.com/coding-blocks-archives/realworld-vue-typescript getModule is called/exported at the end of the vuex module definition without having to pass the store. If I try that it just crashes with an error telling me I haven't supplied the store. I don't know how I can do that properly. Any clues?

championswimmer commented 5 years ago

To prevent that error you have to pass the store into the decorator like this line

https://github.com/coding-blocks-archives/realworld-vue-typescript/blob/master/src/store/modules/users.ts#L9

kpturner commented 5 years ago

How would you do that with a nuxt application? You cannot import the store as shown in that example because the store definition looks like this:

import Vuex from 'vuex';
import { IState } from '../types';

import counter from '../store/modules/test_module';
import hgapi from '../store/modules/test_module.1';

export default () => new Vuex.Store<IState>({
    modules: {
        counter,
        hgapi
    }
});

It returns a function, not a ready made store - and I cannot call the function to create the store from within the decorated module. I need the store already created.

kpturner commented 5 years ago

OK since it is working by me calling getModule from components (where I have access to the store) I have just created a helper function to use there. Seems to work well so far.

0pt1m1z3r commented 5 years ago

@kpturner can you show your final code?

kpturner commented 5 years ago

Yes - and I would be immensely grateful if somebody could/would help me improve it:

Example UserStore module

import { Module, VuexModule, Mutation, Action } from 'vuex-module-decorators';
import axios from '../../common/axiosAccessor';
import { IGetByIdPayload, IUser, ISocialLoginPayload, ISocialLoginResult, IStrategy } from '../../types';
import auth from '../../common/authAccessor';
import { get } from 'lodash';

export interface IUserModuleState {
    userData: Partial<IUser>;
    errs: string[];
    infos: string[];
}

@Module({ stateFactory: true, namespaced: true, name: 'userStore' })
export default class UserStore extends VuexModule {

    public user: IUserModuleState = {
        userData: null,
        errs: null,
        infos: null
    };

    @Mutation
    public update(user: Partial<IUser>): void {
        this.user.userData = {
            ...user
        };
    }
    @Mutation
    public setErrs(errors: string[]): void {
        this.user.errs = errors;
    }
    @Mutation
    public setInfos(infos: string[]): void {
        this.user.infos = infos;
    }

    @Action({ commit: 'update' })
    public async fetchUser(payload: IGetByIdPayload): Promise<Partial<IUser>> {
        this.setErrs(null);
        this.setInfos(null);
        let data: IUser;
        try {
            data = await axios.$post('/api/user/get-user', payload);
        } catch (err) {
            this.setErrs([err]);
        }
        if (!data) {
            data = {} as IUser;
        }
        return data;
    }

    @Action({ commit: 'update' })
    public async socialLogin(payload?: ISocialLoginPayload): Promise<Partial<IUser>> {
        this.setErrs(null);
        this.setInfos(null);

        const context: any = this.context;
        const configPath: string = 'auth.strategies';
        let strategies: IStrategy[] = get(context.rootState.configStore.config, configPath);
        if (!strategies) {
            await context.dispatch('configStore/fetchConfig', { path: configPath }, { root: true });
            strategies = get(context.rootState.configStore.config, configPath);
        }

        const usernameField: string = strategies.find(strategy => strategy.key === context.rootState.auth.strategy).usernameField;

        const outboundPayload: ISocialLoginPayload = payload ? payload : {
            user: this.context.rootState.auth.user,
            username: this.context.rootState.auth.user[usernameField],
            provider: this.context.rootState.auth.strategy
        };

        let result: ISocialLoginResult;
        try {
            result = await axios.$post('/api/user/social-login', outboundPayload);
            if (result.errs) {
                this.setErrs(result.errs);
                auth.logout();
            }
        } catch (err) {
            this.setErrs([err]);
        }
        return result.user;
    }

    @Action({ commit: 'update' })
    public initUser(payload: Partial<IUser>): Partial<IUser> {
        this.setErrs(null);
        this.setInfos(null);
        const data: Partial<IUser> = payload ? payload : {} as Partial<IUser>;
        return data;
    }

    @Action
    public async saveUser(createMode: boolean): Promise<boolean> {
        this.setErrs(null);
        this.setInfos(null);
        try {
            const response: any = await axios.$post('/api/user/save-user', {
                user: this.userData,
                newUser: createMode
            });
            this.setInfos(response.infos);
            this.setErrs(response.errs);
        } catch (err) {
            this.setErrs([err]);
        }
        return (!this.errs || this.errs.length === 0);
    }

    get userData(): Partial<IUser> {
        return this.user.userData;
    }
    get errs(): string[] {
        return this.user.errs;
    }
    get infos(): string[] {
        return this.user.infos;
    }

}

Setting up the store:

import Vuex from 'vuex';
import { IState } from '../types';
import UserStore from './modules/user_store';

export default () => new Vuex.Store<IState>({
    modules: {
        userStore: UserStore
        .... plus others
    }
});

I then have this helper

import { getModule } from 'vuex-module-decorators';
import UserStore from './modules/user_store';
import { Store } from 'vuex';

let initialised: boolean = false;
let userStore: UserStore;

function initialiseStores(store: Store<any>): void {
    if (!initialised) {
        userStore = getModule(UserStore, store);
        initialised = true;
    }
}

export { initialiseStores, userStore };

Then in components and pages:

import { initialiseStores, userStore, blah, blah } from '../store/storeModules';
...
initialiseStores(this.$store);
await userStore.fetchUser({ id: this.$auth.user.id });

But I would rather export getModule from the module definition as shown in the docs - but I don't seem to have access to the main Vuex store at that stage. It just throws a load of errors. It is as if its a dynamic module when I haven't defined it as such.

mfulop commented 5 years ago

How about a factory in your user store module:

import { getModule } from 'vuex-module-decorators';
...
export const getStore = (store?: Store<any>): UserStore => getModule(UserStore, store)

@Module({ stateFactory: true, namespaced: true, name: 'userStore' })
export default class UserStore extends VuexModule {
...

and then to use:

import UserStore, { getStore as getUserStore } from '~/modules/user_store'
...
created () {
  const userStore: UserStore = getUserStore(this.$store)
}
ghost commented 5 years ago

@kpturner Did you find any sophisticated solution for exporting getModule from a module?

danielroe commented 5 years ago

@Yackbz @kpturner I've added a potential solution as a pull request: #124. I'd be very grateful for your thoughts and improvements :smile:

My takeaway is that initialising modules with a vuex store plugin really improves usability of vuex-module-decorators for me in NuxtJS. I'm also happy to share the way I access axios in modules, if you think that would be useful.

ghost commented 5 years ago

I'll look into it. Thanks. Did you also try to invoke nuxtServerIniti()?

async nuxtServerInit() {
        console.log("RUNNING");
}

This doesn't seem to run.

ghost commented 5 years ago

@Action({rawError: true}). This is added as a decorator.

Yes, I'd like to see the how you use axios.

danielroe commented 5 years ago

@yackbz you would add nuxtServerInit as you normally do in Nuxt. ~/store/index.ts is completely vanilla, not using vuex-module-decorators at all. So just see NuxtJS documentation for that. Example:

export const actions: ActionTree<RootState, RootState> = { async nuxtServerInit(context, server ) { }
}

I've not included that in the file to avoid overcomplicating it.

ghost commented 5 years ago

@danielroe I used it inside a module and it won't run.

@Module({ namespaced: true, name: "userStore", stateFactory: true })
export class UserStore extends VuexModule {
    @Action
    async nuxtServerInit() {...}
}

Can it only be used inside index.ts?

danielroe commented 5 years ago

See NuxtJS documentation. ServerInit is only called in index.ts. you have to call module actions from there if you want to initialise modules. Again, there is lots of NuxtJS documentation on this.

ghost commented 5 years ago

@danielroe Thanks for the advice. Do you might have typescript nuxt vuex-module-decorator repository you can share?

ghost commented 5 years ago

@danielroe Your pr looks promising. Intellisense works correctly but the store doesn't seem to be setup correctly.

I have these files setup.

// ~/utils/store.accessor.ts
import { Store } from 'vuex'
import { getModule } from 'vuex-module-decorators'
import {PostStore} from '~/store/post-store'

let postStore: PostStore

function initialiseStores(store: Store<any>): void {
    postStore = getModule(PostStore, store)
}

export {
    initialiseStores,
    postStore,
}
// ~/store/index.ts

import { Store } from 'vuex'
import { initialiseStores } from '~/utils/store-accessor'
const initializer = (store: Store<any>) => initialiseStores(store)
export const plugins = [initializer]
export * from '~/utils/store-accessor'
// ~/store/post-store.ts
@Module({ name: "postStore", stateFactory: true })
export class PostStore extends VuexModule {
    /* -------- STATE --------
     *
     */
    loadedPosts: object[] = [];

    /* -------- GETTERS --------
     *
     */
    get getLoadedPosts() {
        return {posts:this.loadedPosts, test: "YES"};
    }
...
}

I'm accessing the poststore like this.

// ~/pages/index.vue
import { postStore } from "~/store";

@Component({})
    export default class Index extends Vue {
        get loadedPosts() {
            console.log("TEST", postStore.getLoadedPosts);
            return postStore.getLoadedPosts
        }
    }

loadedPosts is unedfined. Can you tell me what's wrong? --> I solved it. I had to use a default exports

Where do you put your nuxtServerInit() if index.ts doesn't have any functions or objects?

danielroe commented 5 years ago

@Yackbz In Nuxt, index.ts can have export functions and objects, and that is where your nuxtServerInit() belongs. That function can then call server initialisation functions in modules.

So, for example:

import { ActionTree, Store } from 'vuex'
import Vue from 'vue'

import { initialiseStores, sampleStore } from '~/utils/store-accessor'
import { RootState } from '~/types/state'

const initializer = (store: Store<any>) => initialiseStores(store)
export const plugins = [initializer]

export const actions: ActionTree<RootState, RootState> = {
  async nuxtServerInit(_context, server: { req: IncomingMessage; app: Vue }) {
    await sampleStore.serverInit(server)
  },

  async nuxtClientInit(context) {
    await sampleStore.clientInit()
  },
}

export * from '~/utils/store-accessor'

Note that the above code doesn't use vuex-module-decorators for the index.ts, but it does for everything else. Also see #124 for an example of what the store-accessor would look like.