Advertisement

#12 Using Decorators To Bind Vuex States And Actions

In this article we will focus on two main topics. The first is we will create decorators that will make it eaiser for us to bind fields in our view components to the vairous properties of our vuex store such as its state and action methods. Once those decorators are in place we will then use them to display our account models and use an add mutation to add new accounts.

Creating a state decorator

  • root
    • src
      • store
        • decorators.ts

In the previous article we saw how we can access our vuex state through the this.$store field in our accounts component. This is fine and of course it works but we can do things a little bit better. What we will do is create a decorator that we can add to a field on our component that will automatically bind the appropriate component of the vuex state to it (a). We will be adapting the code that is used in the vuex-class project on Github.

decorators.ts

import { createDecorator } from "vue-class-component";
import {
    mapState,
} from "vuex";

export function State(stateKey: string) {
    return createDecorator((componentOptions, key) => {
        const mapObject = { [key]: stateKey };

        if (typeof(componentOptions["computed"]) === "undefined")  {
            componentOptions["computed"] = {};
        }

        if (typeof(componentOptions["computed"][key]) !== "undefined") {
            return;
        }
        componentOptions["computed"][key] = mapState(mapObject)[key];
    });
}
(a) Decorator that we can apply to a field that will bind the appropriate vuex state component.

Exporting the decorators

  • root
    • src
      • store
        • index.ts

Not much to be said here just exporting the decorations (b).

index.ts

...
export * from "@/store/decorators";
...
(b) Exporting the decorators from our index file for easier imports.

Decorating our account state field

  • root
    • src
      • views
        • Accounts.vue

No we can convert our accountState computed property to a normal field (c). Although both get the job done I prefer this approach just because in my opinion it is a bit cleaner.

Accounts.vue

<script lang="ts">
...
import { 
    ...
    State
} from "@/store";
...
export default class Account extends Vue {
    @State("accounts") private accountState!: IAccountState;
}
</script>
(c) Updating our computed property to a field.

Enter the decorator factory method

  • root
    • src
      • store
        • decorators.ts

Our store contains more than just state it also holds our actions and mutations among other things. We are going to have to have access to the actions in order to make any changes to our state. What this is leading to is that we need the ability to create some more decorators. To facilitate this we need to refactor our code so that we can provide the options key and the mapping function (d).

decorators.ts

...
import {
    Computed,
    Dictionary,
    ...
} from "vuex";

type mapFnType = (map: Dictionary<string>) => Dictionary<Computed>;

export const State = createDecoratorFactory("computed", mapState);

function createDecoratorFactory(optionsKey: "computed", mapFn: mapFnType) {
    return (storeKey: string) => {
        return createDecorator((componentOptions, key) => {
            const mapObject = { [key]: storeKey };

            if (typeof(componentOptions[optionsKey]) === "undefined")  {
                componentOptions[optionsKey] = {};
            }

            if (typeof(componentOptions[optionsKey]![key]) !== "undefined") {
                return;
            }
            componentOptions[optionsKey]![key] = mapFn(mapObject)[key];
        });
    };
}
(d) Refactoring the creation of our state decorator to use a factory method.

Small fix out of left field

  • root
    • src
      • components
        • routing
          • TheRouterOutlet.vue

While working on this article I noticed that we need to make some small styling changes to our router outlet (e).

TheRouterOutlet.vue

<style lang="sass" scoped>
.router-view
    display: flex
    flex-direction: column
    ...

.router-view-animatable
    flex: 1
    height: 100% // <-- remove
</style>
(e) Small changes to our router outlet.

Adding a plus icon

  • root
    • src
      • features
        • font-awesome.ts

We are getting close to being ready to allow a user to add a new account by entering a name and clicking on an add button. I would like to have a plus icon in the button so we need to import one (f).

font-awesome.ts

import Vue from "vue";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { library } from "@fortawesome/fontawesome-svg-core";

import {
    ...
    faPlus,
    ...
} from "@fortawesome/free-solid-svg-icons";

...
library.add(faPlus);
...
(f) Importing the plus icon.

Adding the input and button (template)

  • root
    • src
      • views
        • Accounts.vue

It is time for us to update our user interface to include a text input and plus button (g).

Accounts.vue

<template lang="pug">
div.accounts-container
    h1 Accounts
    form
        div.add
            input(
                type="text"
                required=""
                placeholder="Name"
                v-model="name")
            button(
                type="submit"
                v-on:click.prevent="add")
                FontAwesomeIcon(icon="plus")
    ...
</template>
(g) Adding the text input and button so that a user can add an account.

Adding the input and button (script)

  • root
    • src
      • views
        • Accounts.vue

Now that the template is updaed we need to update our script to include the name field and the method to invoke when the user presses enter or the add button (h).

Accounts.vue

<script lang="ts">
...
export default class Account extends Vue {
    ...
    private name = "";

    private add() {
        if (this.name === "") {
            return;
        }
        console.log(this.name);
    }
}
</script>
(h) Adding the name field and the add method.
Advertisement

Adding the input and button (style)

  • root
    • src
      • views
        • Accounts.vue

Of course we need to make things look a little bit better so it is time to add a little styling (i).

Accounts.vue

<style lang="sass" scoped>
...
.add
    display: flex
    margin-bottom: 0.75rem

    input
        margin-bottom: 0
    
    button
        background-color: $green

        &:hover, &:focus
            background-color: darken($green, 10%)
            color: white
</style>
(i) Adding the styling for the add input and button.

Constant keys

  • root
    • src
      • store
        • store-constants.ts

In order for us to specify what actions and mutations that we want to run when we will be using some constant string which I like to place in a separate file (j). One of the reasons for this is that we, are at least for now, not going to be namespacing everything so we need to be sure that all of our strings are unique and we can accomplish that a bit easier if their are all in one location.

store-constants.ts

export const ACTION_ADD_ACCOUNT = "ACTION_ADD_ACCOUNT";
export const MUTATION_ADD_ACCOUNT = "MUTATION_ADD_ACCOUNT";
(j) Add a new file to contain the constants that we need for our store.

Surprise more exporting

  • root
    • src
      • store
        • index.ts

Adding in items leads us to need to export them (k).

index.ts

...
export * from "@/store/store-constants";
...
(k) Exporting the constants.

The name of the account to add

  • root
    • src
      • store
        • account-types.ts

Next up is to specify the type of object that will passed to our action. In this case we just need to pass a string but I prefer to create a type alias for it this payload (l).

account-types.ts

...
export type AddAccountPayload = string;
(l) Specifying the type of the payload that will be passed to our add account action.

Defining the add action and mutation

  • root
    • src
      • store
        • account-module.ts

Finally with everything that we have done in this article we are now in a position to define the add action that we will invoke within our view so that the add mutation will be fired which will result in the updating of the accounts within our store (m).

account-module.ts

import {
    ActionContext,
    ActionTree,
    MutationTree,
    Store,
} from "vuex";

import { AccountModel } from "@/store/account-model";

import {
    ACTION_ADD_ACCOUNT,
    MUTATION_ADD_ACCOUNT,
} from "@/store/store-constants";

import { IStoreState } from "@/store/store-types";

import {
    AddAccountPayload,
} from "@/store/account-types";

type AccountContext = ActionContext<IStoreState, IStoreState>;

export const actions: ActionTree<IStoreState, IStoreState> = {
    [ACTION_ADD_ACCOUNT](this: Store<IStoreState>, { commit }: AccountContext, name: AddAccountPayload) {
        commit(MUTATION_ADD_ACCOUNT, name);
    },
};

export const mutations: MutationTree<IStoreState> = {
    [MUTATION_ADD_ACCOUNT](state: IStoreState, payload: AddAccountPayload) {
        const account = new AccountModel({ id: state.accounts.index, name: payload});
        state.accounts.items = [...state.accounts.items, account].sort((a, b) => {
            const aName = a.name.toUpperCase();
            const bName = b.name.toUpperCase();
            if (aName < bName) { return -1; }
            if (aName > bName) { return 1; }
            return 0;
        });
        state.accounts.index += 1;
    },
};
(m) Defining the action and mutation what will be used to add a new account to our store.

Add the action and mutation to the store

  • root
    • src
      • store
        • store.ts

Just like with the state we need to add our action and mutation to our store so that we can use them (n).

store.ts

...
import {
    actions as accountActions,
    mutations as accountMutations,
} from "@/store/account-module";
...
const actions = {
    ...accountActions,
};

const mutations = {
    ...accountMutations,
};
...
export const store = new Vuex.Store({
    actions,
    mutations,
    state,
});
(n) Adding the action and mutation to the store.

Creating the action decorator

  • root
    • src
      • store
        • decorators

Now that we need access to an action within our components we need to return to our decorators and create an action decorator (o).

decorators

import { createDecorator } from "vue-class-component";
import {
    ActionMethod,
    ...
    mapActions,
    ...
} from "vuex";

type mapActionFnType = (map: Dictionary<string>) => Dictionary<ActionMethod>;
type mapComputedFnType = (map: Dictionary<string>) => Dictionary<Computed>;

type mapFnType = mapActionFnType | mapComputedFnType;

export const Action = createDecoratorFactory("methods", mapActions);
...
function createDecoratorFactory(optionsKey: "computed" | "methods", mapFn: mapFnType) {
    ...
}
(o) Creating an action decorator.

Typing our refs

  • root
    • src
      • types.ts

One other behavior I would like to support is if a user adds a new account I would like to focus the add text input so that the user can then just start typing away in order to add another one. This is something that is very easy to do with vue using the $refs object. Unfortunately by necessity the items that it contains are untyped. To facilitate adding types to the the refs object we can use a type definition (p).

types.ts

...
export type Refs<T extends object> = Vue["$refs"] & T;
(p) Creating a type definition that we can use to type a components refs.

Adding the input to the refs

  • root
    • src
      • views
        • Accounts.vue

Adding our input to the $refs object just requires us to add the ref attribute with a value to our template (q).

Accounts.vue

<template lang="pug">
div
    form
        div.add
            input(
                ref="add"
                ...)
    ...
</template>
(q) Adding the refs attribute with a value to our template adds the input to the refs object.

Finally adding a new account

  • root
    • src
      • views
        • Accounts.vue

Last but not least we are in a position to be able to add a new account to our store (r). Once an account has been added we set the name to the empty string and refocus the input so that another account can be added immediately afterwards.

Accounts.vue

<script lang="ts">
...
import { 
    ACTION_ADD_ACCOUNT,
    Action,
    AddAccountPayload,
    ...
} from "@/store";

import { Refs } from "@/types";
...
export default class Account extends Vue {
    public $refs!: Refs<{
        add: HTMLInputElement,
    }>
    @Action(ACTION_ADD_ACCOUNT) private addAccount!: (payload: AddAccountPayload) => void;
    ...
    private add() {
        if (this.name === "") {
            return;
        }
        this.addAccount(this.name);
        this.name = "";
        this.$refs.add.focus();
    }
}
</script>
(r) Updating the script so that our add action is invoked in order to add a new account to our store.
Exciton Interactive LLC
Advertisement