#12 Using Decorators To Bind Vuex States And Actions
Saturday, August 31, 2019
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.
Parts
- Part 45: Adjusting Shares
- Part 44: Plan Percentages
- Part 43: Home Securities
- Part 42: Updating Plans
- Part 41: Plan Details View
- Part 40: Portfolio Getters
- Part 39: Portfolio Plan
- Part 38: Portfolio Category
- Part 37: Account Securities
- Part 36: Account Transfer
- Part 35: View Account Security
- Part 34: Updating Deposit
- Part 33: View Account Deposit
- Part 32: Display Account Details
- Part 31: Account Getters
- Part 30: Deposits And Securities
- Part 29: Add Accounts Details
- Part 28: Refactoring Accounts
- Part 27: Add Security Models
- Part 26: Edit Security Details
- Part 25: View Security Details
- Part 24: Navigating To Details
- Part 23: Getters Validation
- Part 22: Query Parameters
- Part 21: Tab Entries
- Part 20: Tab Header
- Part 19: List View
- Part 18: Vuex Getters
- Part 17: End Domain Model
- Part 16: Start Domain Model
- Part 15: Pop Routes
- Part 14: Push Routes
- Part 13: Removing Accounts
- Part 12: Vuex (Decorators)
- Part 11: Vuex (Accounts)
- Part 10: The App Bar (Settings)
- Part 9: Remove Consumer Rxjs
- Part 8: The App Bar (Back)
- Part 7: Structuring Our App
- Part 6: Animation Between Views
- Part 5: Navigation Fade
- Part 4: Navigation Requests
- Part 3: Fade Animations (cont.)
- Part 2: Fade Animations
- Part 1: Splash Screen
Creating a state decorator
- root
- src
- store
- decorators.ts
- store
- src
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];
});
}
Exporting the decorators
- root
- src
- store
- index.ts
- store
- src
Not much to be said here just exporting the decorations (b).
index.ts
...
export * from "@/store/decorators";
...
Decorating our account state field
- root
- src
- views
- Accounts.vue
- views
- src
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>
Enter the decorator factory method
- root
- src
- store
- decorators.ts
- store
- src
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];
});
};
}
Small fix out of left field
- root
- src
- components
- routing
- TheRouterOutlet.vue
- routing
- components
- src
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>
Adding a plus icon
- root
- src
- features
- font-awesome.ts
- features
- src
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);
...
Adding the input and button (template)
- root
- src
- views
- Accounts.vue
- views
- src
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>
Adding the input and button (script)
- root
- src
- views
- Accounts.vue
- views
- src
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>
Adding the input and button (style)
- root
- src
- views
- Accounts.vue
- views
- src
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>
Constant keys
- root
- src
- store
- store-constants.ts
- store
- src
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";
Surprise more exporting
- root
- src
- store
- index.ts
- store
- src
Adding in items leads us to need to export them (k).
index.ts
...
export * from "@/store/store-constants";
...
The name of the account to add
- root
- src
- store
- account-types.ts
- store
- src
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;
Defining the add action and mutation
- root
- src
- store
- account-module.ts
- store
- src
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;
},
};
Add the action and mutation to the store
- root
- src
- store
- store.ts
- store
- src
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,
});
Creating the action decorator
- root
- src
- store
- decorators
- store
- src
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) {
...
}
Typing our refs
- root
- src
- types.ts
- src
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;
Adding the input to the refs
- root
- src
- views
- Accounts.vue
- views
- src
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>
Finally adding a new account
- root
- src
- views
- Accounts.vue
- views
- src
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>