1.0.5 • Published 5 years ago

vuex-typesafe-class v1.0.5

Weekly downloads
4
License
MIT
Repository
github
Last release
5 years ago

ES2015 Helpers for using classes as Vuex modules

Goals

  • Ensure your codebase is type safe when writing and using Vuex modules
  • Generate Vuex compatible store options, which can be used in Vue and Nuxt projects
  • Omit usage of decorators

Dependencies

This module has no external dependencies.

Installation

$ npm i vuex-typesafe-class

Getting started

Consider this example:

new Vuex.Store({
  namespace: true,
  state: {
    lineEnding: "\n",
    prename: "Jane",
    lastname: "Doe"
  },
  getters: {
    fullname(state, value) {
      return state.prename + " " + state.lastname;
    }
  },
  mutations: {
    name(state, value) {
      state.prename = value.prename;
      state.lastname = value.lastname;
    }
  },
  actions: {
    async action({ state, commit, getters }, { hello }) {
      commit("name", { prename: "John", lastname: "Smith" });
      return hello + " " + getters.fullname + "!" + state.lineEnding;
    }
  }
});

You can now easily write modules using the createModule helper:

import { createModule } from "vuex-typesafe-class";

class RootModule {
  lineEnding = "\n";
  prename: string = "Jane";
  lastname: string = "Doe";

  get fullname() {
    return this.prename + " " + this.lastname;
  }

  set name(value: { prename: string; lastname: string }) {
    this.prename = value.prename;
    this.lastname = value.lastname;
  }

  async action({ hello }: { hello: string }) {
    this.name = { prename: "John", lastname: "Smith" };
    return hello + " " + this.fullname + "!" + this.lineEnding;
  }
}

new Vuex.Store(createModule(RootModule));

Core concepts

State

To define the module state simply declare properties on your module class:

class RootModule {
  stateA: boolean = true;
  stateB: number = 123;
  stateC: string = "abc";
  stateD: string | undefined = undefined;
}

export default createModule(RootModule);

... will result in a state factory:

export default {
  namespaced: true,
  state() {
    return {
      stateA: true,
      stateB: 123,
      stateC: "abc",
      stateD: undefined
    };
  }
};

All properties that are declared public will be available for autocompletion when using mapState().

Note:

  • vuex-typesafe-class always produces state factories to allow Module Reuse.
  • While you can use undefined as value, you have to explicitly initialize state variables with undefined (see stateD). Otherwise reactivity will not work as expected and getters might not be called.

Mutations

Mutations are defined as ES6 setter methods:

class RootModule {
  stateB: number = 123;

  set setterB(value: number) {
    this.stateB = value;
  }
}

export default createModule(RootModule);

... will result in :

export default {
  namespaced: true,
  state() {
    return {
      stateB: 123
    };
  },
  mutations: {
    setterB(state, value) {
      state.stateB = value;
    }
  }
};

Actions

Actions are defined as class methods:

class RootModule {
  stateB: number = 123;

  get getterB() {
    return "Getter: " + this.stateB;
  }

  set setterB(value: number) {
    this.stateB = value;
  }

  async actionA({ paramA }: { paramA: number }) {
    return "actionA: " + paramA;
  }

  async actionB({ paramB }: { paramB: number }) {
    this.setterB = paramB;
    const result = await this.actionA({ paramA: 123 });
    return result + this.getterB + " State: " + this.stateB;
  }
}

export default createModule(RootModule);

... will result in :

export default {
  namespaced: true,
  state() {
    return {
      stateB: 123
    };
  },
  getters: {
    getterB(state) {
      return "Getter: " + state.stateB;
    }
  },
  mutations: {
    setterB(state, value) {
      state.stateB = value;
    }
  },
  actions: {
    async actionA({ paramA }) {
      return "actionA: " + paramA;
    },
    async actionB({ state, getters, commit, dispatch }, { paramB }) {
      commit("setterB", paramB);
      const result = await dispatch("actionA", { paramA: 123 });
      return getters.getterB + " State: " + state.stateB;
    }
  }
};

You can access state variables, getters, mutations and other actions from your class modules by simply referencing them with this. Because actions are asynchronous you should always define your actions as async function.

Note:

  • Do not use GeneratorFunctions as they would be detected as mutation functions (to allow mutators to be called as functions). This might be removed in the future.

Getters

Module getters are defined as ES6 getter methods:

class RootModule {
  stateB: number = 123;

  get getterB() {
    return "Getter: " + this.stateB;
  }
}

export default createModule(RootModule);

... will result in :

export default {
  namespaced: true,
  state() {
    return {
      stateB: 123
    };
  },
  getters: {
    getterB(state) {
      return "Getter: " + state.stateB;
    }
  }
};

Can I use...

...it in Vue components?

Consider a simple store like this:

//store/simple.ts
import { createModule, Mutation } from "vuex-typesafe-class";

class RootModule {
  lineEnding = "\n";
  prename: string = "Jane";
  lastname: string = "Doe";

  get text() {
    return "Hello " + this.fullname;
  }

  get fullname() {
    return this.prename + " " + this.lastname;
  }

  @Mutation
  setName(value: { prename: string; lastname: string }) {
    this.prename = value.prename;
    this.lastname = value.lastname;
  }

  async action({ hello }: { hello: string }) {
    const salute = await this.$salutation.salute({ gender: "male" });
    return hello + " " + salute + " " + this.fullname + "!" + this.lineEnding;
  }
}

export default createModule(RootModule);

You can now use these state variables, getters, mutations and actions in your components:

import Vue from "vue";
import Simple from "./store/simple";
import {
  mapState,
  mapGetters,
  mapActions,
  mapMutations
} from "vuex-typesafe-class";

export default Vue.extend({
  data() {
    return {
      prename: "Erika",
      lastname: "Musterfrau"
    };
  },
  computed: {
    ...mapState(Simple, {
      lineEnding: "lineEnding"
    }),
    ...mapGetters(Simple, {
      simpleText: "text"
    })
  },
  methods: {
    ...mapMutations(Simple, {
      setName: "setName"
    }),
    ...mapActions(Simple, {
      greet: "action"
    }),
    async submit() {
      this.setName({ prename: this.prename, lastname: this.lastname });
      await this.greet({ hello: "Hi" });
    }
  }
});

As in Vuex you can use the helper methods mapState, mapGetters, mapMutations, mapActions. All of these are called like their Vuex counterparts: The first parameter is the namespace (which accepts modules created by createModule), the second is the map you already know from Vuex. The map is type checked and the result also contains all type informations you provided in the store.

Note:

  • In the example we used the decorator function @Mutation. This is only necessary if you plan to use mutations inside your modules (with the help of mapMutations), which I understand as an anti-pattern. You can always use mutations you defined as setters inside actions and expose these to your components.

... it with Nuxt.js?

Yes! This library was designed with Nuxt.js in mind.

Simply install the module and add it to the list of modules that have to be transpiled in your nuxt.config.(js/ts):

//nuxt.config.js
//...
build: {
  transpile: [/^vuex-typesafe-class/];
}
//...

As Nuxt.js uses modules mode by default, you can simply put your modules in the stores directory.

/// ~/store/index.ts
import { createModule } from "vuex-typesafe-class";

class RootStore {
  async nuxtServerInit() {
    await this.init({});
  }

  async init({}) {}
}

export default createModule(RootStore);

Because Nuxt.js will automatically attach nested modules to your root module you do not have to define child modules in your RootStore. Simply put your module inside the store directory:

/// ~/store/nested.ts
import { createModule } from "vuex-typesafe-class";

class NestedStore {
  async test({}) {}
}

export default createModule(NestedStore, "nested");

Note: You can omit the module name (second parameter of createModule) for your root module. For all other modules use the module namespace (delimited by /). It is also possible to simply use __filename or module.id in development as vuex-typesafe-class can handle pathnames. Module filenames will be lost after building with nuxt build/generate.

...it with nested modules / submodules?

First of all createModule accepts two more parameters: createModule(ModuleClass: Constructor, namespace?: string, options?: {modules?: Array}).

Consider this example:

import { createModule, useStore } from "vuex-typesafe-class";

const salutationModule = createModule(
  class {
    salutation: string = "Mrs.";
  },
  "salutations"
);

class RootModule {
  private get $nested() {
    return useStore(salutationModule, this);
  }

  prename: string = "Jane";
  lastname: string = "Doe";

  get salutation() {
    return (
      "Hello " +
      this.$nested.salutation +
      " " +
      this.prename +
      " " +
      this.lastname
    );
  }
}

export default createModule(RootModule, "", { modules: [salutationModule] });

As you see you can use the options.modules parameter to attach further stores to your root store. To access external modules in your modules define a getter that executes useStore(module, this). module is the result of createModule. Note:

  • Please define this getter as private and prefix it with _ or $ to prevent creating Vuex getter.**

...it programmatically e.g. for usage in Nuxt.js fetch method?

You can use useStore outside of Vuex module classes.

const $nested = useStore(salutationModule, this);
assert($nested.salutation, "Mrs.");

... Inheritance

Modules created with createModule can make use of inheritance:

class BaseStore {
  async submit({ username, password }) {
    return await this.$axios.post("/login", { username, password });
  }
}

const rootModule = createModule(
  class extends BaseStore {
    async submit({ username, password }) {
      const response = await super.submit({ username, password });
      if (response.status == 401) {
        throw new Error("Authorization failed");
      }
      return response.data;
    }
  }
);

... injected variables (like \$axios) that are available in store context?

When using Nuxt.js in conjunction with Axios Module \$axios is injected to store context. You can access the context via this as you would do in standard Vuex modules.

const rootModule = createModule(
  class {
    protected $axios!: Axios;

    async submit({ username, password }) {
      return await this.$axios.post("/login", { username, password });
    }
  }
);

To use injected variables easily you can make use of inheritance:

class BaseStore {
  protected $axios!: Axios;
}

const rootModule = createModule(
  class extends BaseStore {
    async submit({ username, password }) {
      return await this.$axios.post("/login", { username, password });
    }
  }
);