Introduction
In this tutorial, we are going to create a task management application to demonstrate how to build applications with the new Vue 3 composition API, typescript, and Vuex(4.0). Furthermore, we will explore Vuex(4.0) practically.
Pre-requisite
Node.js 10.x and above
Knowledge of JavaScript Es6
Basic knowledge of vue.js
Basic knowledge of Typescript
Introducing Vue composition API
The composition API is a new API for creating components in vue 3.
It presents a clean and flexible way to compose logic inside and between components. Also, it solves the problems associated with using mixins and higher-order component to share re-usable logic between components. It has a small bundle size and naturally supports typescript.
Introducing Vuex(4.0)
Vuex is a state management library created by the Vue team and built solely for use with Vue. It provides an intuitive development experience when integrated into an existing Vue app. Vuex becomes crucial when your Vue app gets more complex as it grows.
The latest version of Vuex, version v4.0.0 supports the new Composition API introduced in Vue 3 as well as a more robust inference for TypeScript.
In this article, we will explore the latest version of Vuex, version v4.0.0 practically as we build along.
Setting up vue 3 + vuex + TypeScript App
Let’s start by creating a Vue 3 app with typescript support using the Vue-CLI tool.
Take the following steps to create a Vue 3 + Vuex app with Typescript support, using Vue CLI tool
- Install the Vue-CLI tool globally by executing the command below:
npm install --global @vue/cli
2. If you have the CLI already you can check if you have the appropriate version (v4.x):
vue --version
3. Make sure you upgrade it to the latest version with the following command:
npm update -g @vue/cli # OR yarn global upgrade --latest @vue/cli
Create a new Vue 3 app with typescript and Vuex support with the command below:
vue create vuex-ts-project
Choose the manually select feature option
Hit the space key to select the following options:
- choose Vue version
- Babel
- TypeScript
- Linter / Formatter
Next, Choose Vue 3.x(Preview) as the version for the project.
- Enter yes to use the class-style component syntax
- Enter yes to use Babel alongside TypeScript
- Select any linter of your choice
Your final setup options should be like the above.
Once the Vue 3 app is generated successfully, we will have a Vue 3 project setup with vuex (V4.x) and full typescript support.
To start the development server, run the command below in your terminal and head over to http://localhost:8080 to preview your project in the browser.
npm run serve
Next, open the vuex-ts-project folder in VS code editor or any other code editor with typescript support.
For developing Vue applications with TypeScript, the Vue core team strongly recommends using Visual Studio Code or WebStorm which provides great out-of-the-box support for TypeScript and Vue. Hence in this tutorial, we will use Visual Studio Code.
Overview of what we will build in this tutorial
In this tutorial we will build a task management application with the following features:
- Create task
- Edit task
- View task
- Delete task
Dependency Installation
We will use the Bulma CSS framework for styling our application. Run the command below to install Bulma CSS:
npm install bulma
Head over to App.vue and import Bulma CSS as follows:
//App.vue <style lang="scss"> @import '~bulma/css/bulma.css'; </style>
Setting up our Vuex store
In Vuex, the state of the application is kept in the store. The application state can only be updated by dispatching the actions within a component which triggers the mutations in the store. The Vuex store is made up of the state, mutations, actions, and getters.
We’ll start building our application by first building the Vuex store with TypeScript for type safety. We already know that a fully-defined Vuex store is composed of 4 distinct pieces – state, mutations, actions, and getters. We’ll set up the store in steps:
- Create our state objects
- Set up the mutations that will occur in our application
- Create the actions that will commit to these subsequent mutations
- Create getters for components to directly compute state data
State
A state is a store object that holds the application-level data that needs to be shared between components.
Head over to the store folder and create a state.ts file inside it. Add the following code below to state.ts:
// /store/state.ts export type TaskItem = { id: number; title: string; description: string; createdBy: string; assignedTo: string; completed: boolean; editing: boolean; }; export type State = { loading: boolean; tasks: TaskItem[]; showCreateModal: boolean; showEditModal: boolean; showTaskModal: boolean; editModalTaskId: number | undefined; showTaskId: number | undefined; }; export const state: State = { loading: false, tasks: [], showCreateModal: false, showEditModal: false, showTaskModal: false, editModalTaskId: undefined, showTaskId: undefined, };
Here, we add some type of safety to the TaskItem and State by defining their types. We also export types because they will be used in the definitions of getters, mutations and actions. Finally, we cast the State type to the state.
Mutations
Mutations are methods that modify the store state. It accepts state as the first argument and payload as the second, and eventually modifies the store state with the payload.
To create mutations, we will use constants for mutation types as recommended by the Vuex docs.
Still in the store folder, create a mutations.ts file with the following code below:
// /store/mutations.ts import { MutationTree } from 'vuex' import { State, TaskItem } from './state' export enum MutationType { CreateTask = 'CREATE_TASK', SetTasks = 'SET_TASKS', CompleteTask = 'COMPLETE_TASK', RemoveTask = 'REMOVE_TASK', EditTask = 'EDIT_TASK', UpdateTask = `UPDATE_TASK`, SetLoading = 'SET_LOADING', SetCreateModal = 'SET_CREATE_MODAL', SetEditModal = 'SET_EDIT_MODAL', SetTaskModal = 'SET_TASK_MODAL' }
MutationTree is a generic type, that is shipped with the Vuex package. it helps to declare a type of mutation tree. We will use it as we proceed.
Here, all of our possible names of mutations are stored in the MutationTypes enum.
Next, we will declare a contract (types) for each MutationType
Add the code below to the mutation.ts file:
//mutations.ts export type Mutations = { [MutationType.CreateTask](state: State, task: TaskItem): void [MutationType.SetTasks](state: State, tasks: TaskItem[]): void [MutationType.CompleteTask]( state: State, task: Partial<TaskItem> & { id: number } ): void [MutationType.RemoveTask]( state: State, task: Partial<TaskItem> & { id: number } ): void [MutationType.EditTask]( state: State, task: Partial<TaskItem> & { id: number } ): void [MutationType.UpdateTask]( state: State, task: Partial<TaskItem> & { id: number } ): void [MutationType.SetLoading](state: State, value: boolean): void [MutationType.SetCreateModal](state: State, value: boolean): void [MutationType.SetEditModal](state: State, value: {showModal: boolean, taskId: number|undefined}): void [MutationType.SetTaskModal](state: State, value: {showModal: boolean, taskId: number|undefined}): void }
Next, we will implement the contract (types) declared for each MutationType :
Add the code below to the mutation.ts file:
// /store/mutation.js export const mutations: MutationTree<State> & Mutations = { [MutationType.CreateTask](state, task) { state.tasks.unshift(task) }, [MutationType.SetTasks](state, tasks) { state.tasks = tasks }, [MutationType.CompleteTask](state, newTask) { const task = state.tasks.findIndex(element => element.id === newTask.id) if (task === -1) return state.tasks[task] = { ...state.tasks[task], ...newTask } }, [MutationType.RemoveTask](state, Task) { const task = state.tasks.findIndex(element => element.id === Task.id) if (task === -1) return //If Task exist in the state, remove it state.tasks.splice(task, 1) }, [MutationType.EditTask](state, Task) { const task = state.tasks.findIndex(element => element.id === Task.id) if (task === -1) return //If Task exist in the state, toggle the editing property state.tasks[task] = { ...state.tasks[task], editing: !state.tasks[task].editing } console.log("taskino", state.tasks[task]) }, [MutationType.UpdateTask](state, Task) { state.tasks = state.tasks.map(task => { if(task.id === Task.id) { return {...task, ...Task} } return task; }) }, [MutationType.SetLoading](state, value) { state.loading = value console.log("I am loading...") }, [MutationType.SetCreateModal](state, value) { state.showCreateModal = value }, [MutationType.SetEditModal](state, value) { state.showEditModal = value.showModal state.editModalTaskId = value.taskId }, [MutationType.SetTaskModal](state, {showModal, taskId}) { state.showTaskModal = showModal state.showTaskId = taskId } }
Here, we create the mutations variable which stores all the implemented mutations. The MutationTree<State> & Mutations ensures that the contract is implemented correctly else Typescript will trigger an error.
Let’s assume that we failed to implement the contract below:
[MutationType.CreateTask](state: State, task: TaskItem): void
By adding comments the mutation as seen below:
export const mutations: MutationTree<State> & Mutations = { // [MutationType.CreateTask](state, task) { // state.tasks.unshift(task) // }, }
Here typescript IntelliSense will highlight const mutations in red and Hovering on the highlight we will get the following typescript error:
Property '[MutationType.CreateTask]' is missing in type '{ SET_TASKS(state: State, tasks: TaskItem[]): void; COMPLETE_TASK(state: State, newTask: Partial<TaskItem> & { id: number; }): void; ... 6 more ...; SET_TASK_MODAL(state: State, { showModal, taskId }: { ...; }): void; }' but required in type 'Mutations'.ts(2322)
Actions
Actions are methods that trigger the mutations. When handling asynchronous tasks, actions are used before calling the corresponding mutations. We will simulate asynchronous tasks as we create our actions.
Still in the store folder, create an actions.ts file with the following code below:
// /store/action.ts import { ActionContext, ActionTree } from 'vuex' import { Mutations, MutationType } from './mutations' import { State } from './state' export enum ActionTypes { GetTaskItems = 'GET_Task_ITEMS', SetCreateModal = 'SET_CREATE_MODAL', SetEditModal = 'SET_EDIT_MODAL' }
Similarly, all of our possible names of actions is stored in the ActionTypes enum.
Next, we will declare a contract (types) for each ActionType
Add the code below to the action.ts file:
// /store/action.ts type ActionAugments = Omit<ActionContext<State, State>, 'commit'> & { commit<K extends keyof Mutations>( key: K, payload: Parameters<Mutations[K]>[1] ): ReturnType<Mutations[K]> } export type Actions = { [ActionTypes.GetTaskItems](context: ActionAugments): void [ActionTypes.SetCreateModal](context: ActionAugments): void [ActionTypes.SetEditModal](context: ActionAugments): void }
Here we use the ActionContext type which is shipped with the Vuex package, in the
ActionAugments type to restrict commits only to the declared mutations and also to check the payload type.
Next, we will implement the contract (types) declared for each ActionType
Add the code below to the action.ts file:
//action.ts const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) export const actions: ActionTree<State, State> & Actions = { async [ActionTypes.GetTaskItems]({ commit }) { commit(MutationType.SetLoading, true) await sleep(1000) commit(MutationType.SetLoading, false) commit(MutationType.SetTasks, [ { id: 1, title: 'Create a new programming language', description: "The programing language should have full typescript support ", createdBy: "Emmanuel John", assignedTo: "Saviour Peter", completed: false, editing: false } ]) }, async [ActionTypes.SetCreateModal]({ commit }) { commit(MutationType.SetCreateModal, true) }, async [ActionTypes.SetEditModal]({ commit }) { commit(MutationType.SetEditModal, {showModal: true, taskId: 1}) } }
Here we create a sleep variable that returns Promise. Followed by the actions variable which stores all the implemented actions. Similarly, the ActionTree<State> & Actions
ensures that the contract (type Actions
) is implemented correctly else Typescript will trigger an error.
Next, we set up our asynchronous call in the GetTaskItems
action and commit an array containing a single task object. we also set up SetCreateModal
and SetEditModal
actions.
Getters
Getters are methods that receive the state as its first parameter and return computed information from the store state.
The only getters we’ll need in our store are:
completedTaskCount
– A function that gets the total number of completed task in our statetotalTaskCount
– A function that gets the total number of task in our stategetTaskById
– A function that gets a task by its id.
Still in the store folder, create a getters.ts file with the following code below:
// store/getters.ts import { GetterTree } from 'vuex' import { State, TaskItem } from './state' export type Getters = { completedTaskCount(state: State): number totalTaskCount(state: State): number getTaskById(state: State): (id :number) => TaskItem | undefined } export const getters: GetterTree<State, State> & Getters = { completedTaskCount(state) { return state.tasks.filter(element => element.completed).length }, totalTaskCount(state) { return state.tasks.length }, getTaskById: (state) => (id: number) => { return state.tasks.find(task => task.id === id) } }
Similarly, we have added some type of safety to Getters.
Store
Now that our state, mutations
, actions
, and getters
are all set-up, let’s wire them to the global Vuex store.
The main Vuex store now needs to import these pieces ( state
, mutations
, actions
, and getters
) and include them within the createStore
method.
We’ll update store/index.ts
to reflect this:
//store/index.ts import { createStore, Store as VuexStore, CommitOptions, DispatchOptions, createLogger } from 'vuex' import { State, state } from './state' import { Mutations, mutations } from './mutations' import { Actions, actions } from './actions' import { Getters, getters } from './getters' export const store = createStore<State>({ plugins: process.env.NODE_ENV === 'development' ? [createLogger()] : [], state, mutations, actions, getters }) export function useStore() { return store as Store } export type Store = Omit< VuexStore<State>, 'getters' | 'commit' | 'dispatch' > & { commit<K extends keyof Mutations, P extends Parameters<Mutations[K]>[1]>( key: K, payload: P, options?: CommitOptions ): ReturnType<Mutations[K]> } & { dispatch<K extends keyof Actions>( key: K, payload?: Parameters<Actions[K]>[1], options?: DispatchOptions ): ReturnType<Actions[K]> } & { getters: { [K in keyof Getters]: ReturnType<Getters[K]> } }
Here we create a store using the createStore()
method from Vuex. We trigger the createLogger
plugin which logs the state (previous state and next state) and mutations to the console. Export the useStore
function for use in the application components.
Now we have access to a fully typed store all through our application.
To have the store accessible in all our application’s components, we need to inject it into the entire application. Fortunately, the Vue-CLI tool had already imported the entire store and passed it within the application’s Vue instance.
// src/main.ts import { createApp } from 'vue' import App from './App.vue' import {store} from './store' createApp(App).use(store).mount('#app')
Connecting the component’s logic with the Vuex store
Now that the store is properly set up with full type support we will look at how to use the store in our app components using the composition API.
App
For us to retrieve information from the store, we first need to invoke the mutation that sets the hardcoded task in the store. Since we need the mutation to occur at the moment the component is mounted, we’ll dispatch the GetTaskItems
action within the component’s mounted()
hook.
In order to use the store in a component, we must access it via the useStore
hook, which will return our store.
We will create three computed properties:
-
completedCount
– that calls thecompletedTaskCount
getter method to retrieve the total number of completed tasks totalCount
– that calls thetotalTaskCount
getter method to retrieve the total number of tasksloading
– that gets the loading property of the state
Update App.vue with the following code below:
//App.vue <template> <div class="container mx-auto mt-4"> <h1 class="is-size-3 has-text-centered p-2 has-text-weight-bold"> Vue 3 Task Management App with Typescript and Vuex 4 </h1> <div v-if="loading"> <h3 class="has-text-centered mt-4">Loading...</h3> </div> <div v-else> <p class="has-text-centered mt-2"> {{ completedCount }} of {{ totalCount }} completed. </p> <TaskList/> </div> </div> </template> <script lang="ts"> import { computed, defineComponent, onMounted } from 'vue' import TaskList from './components/TaskList.vue' import { useStore } from './store' import { ActionTypes } from './store/actions' export default defineComponent({ components: { TaskList }, setup() { const store = useStore() const loading = computed(() => store.state.loading) onMounted(() => store.dispatch(ActionTypes.GetTaskItems)) const completedCount = computed(() => store.getters.completedTaskCount) const totalCount = computed(() => store.getters.totalTaskCount) return { loading, completedCount, totalCount } } }) </script> <style> @import '~bulma/css/bulma.css'; </style>
Notice how we return loading, completedCount and totalCount
. This is because we need to access them in the template. If we fail to return a method that is accessed in the template, Vue will trigger some errors.
TaskList
In order to launch the modal responsible for creating a task, we first need to create a method (setModal
) that invokes the mutation that sets showCreateModal
in the state to true.
We will create six computed properties:
-
showCreateModal
– that gets theshowCreateModal
property of the state. showEditModal
– that gets theshowEditModal
property of the state.showTaskModal
– that gets theshowTaskModal
property of the state.tasks
– that gets the list of tasks from the state.showTaskId
– that gets theshowTaskId
property of the state.editTaskId
– that gets theeditTaskId
property of the state.
Create TaskList.vue inside the components folder with the following code below:
//components/TaskList.vue <template> <table class="table"> <thead> <tr> <th><abbr title="Position">Task Id</abbr></th> <th>Completed</th> <th>Task</th> <th><abbr title="Won">Created By</abbr></th> <th><abbr title="Drawn">Assigned To</abbr></th> <th><abbr title="Lost">Actions</abbr></th> </tr> </thead> <tbody v-if="tasks"> <tr v-for="task in tasks" :key="task.id"> <TaskListItem v-bind="task" /> </tr> </tbody> <tfoot> <CreateModal v-show="showCreateModal"></CreateModal> <button class="button is-link" @click="setModal">Create Task</button> </tfoot> </table> <EditModal v-if="showEditModal" :id="editTaskId"></EditModal> <TaskItem v-if="showTaskModal" :id="showTaskId"></TaskItem> </template> <script> import CreateModal from './CreateModal' import EditModal from './EditModal' import TaskItem from './TaskItem' import TaskListItem from './TaskListItem' import { defineComponent, computed } from 'vue' import { useStore } from '@/store' import { MutationType } from '@/store/mutations' export default defineComponent({ name: 'TaskList', components: { CreateModal, TaskListItem, TaskItem, EditModal }, setup() { const store = useStore() const setModal = () => { store.commit(MutationType.SetCreateModal, true) } const showCreateModal = computed(() => store.state.showCreateModal) const showEditModal = computed(() => store.state.showEditModal) const editTaskId = computed(() => store.state.editModalTaskId) const showTaskModal = computed(() => store.state.showTaskModal) const showTaskId = computed(()=> store.state.showTaskId) const tasks = computed(() => store.state.tasks) return { showCreateModal, setModal, tasks, showEditModal,showTaskModal, editTaskId, showTaskId } } }) </script> <style scoped> table { width: 100%; } .fa { font-size: 1.2rem; margin-left: 15px; } .fa:hover { font-size: 1.4rem; } </style>
Here the TaskList
component houses the CreateModal
, TaskListItem
, TaskItem
, and the EditModal
components. It passes edidtaskId
and showtaskId
as props to EditModal
and TaskItem
respectively. We use the v-for directive to render the TaskListItem
component for every task in the computed tasks property. We’ve passed each task as a task prop for every iterated TaskListItem
component. We’ve used task.id
as the unique key identifier. Our application will throw an error until we create all the children components in TaskList
the component.
TaskListItem
This component receives the task props from every iteration in the TaskList component and renders the list of these task properties in the browser.
We will create four methods to mutate our store’s properties:
toggleCompletion
– that toggles the `completed` property of a task.removeTask
– that commits theRemoveTask
mutation (which removes a task from the state by its id ) with an id object as payload.viewTask
– that commits theSetTaskModal
mutation (which allows us to view a single task).editTask
– that commits theSetEditModal
mutation (which allows us to edit a single task).
Within the components folder, let’s create a new TaskListItem.vue
file:
<template> <th>{{ id }}</th> <td> <input type="checkbox" :checked="completed" @change="toggleCompletion()" /> </td> <td>{{ title }} <strong>(C)</strong></td> <td>{{ createdBy }}</td> <td>{{ assignedTo }}</td> <td> <span class="icon" @click="viewTask()"> <i class="fa fa-eye"></i> </span> <span class="icon" @click="editTask()"> <i class="fa fa-edit"></i> </span> <span class="icon" @click="removeTask()"> <i class="fa fa-trash"></i> </span> </td> </template> <script lang="ts"> import { defineComponent, computed } from 'vue' import { useStore } from '@/store' import { MutationType } from '@/store/mutations' export default defineComponent({ props: { id: { type: Number, required: true }, title: { type: String, required: true }, createdBy: { type: String, required: true }, assignedTo: { type: String, required: true }, completed: { type: Boolean, required: true } }, setup(props) { const store = useStore() const toggleCompletion = () => { store.commit(MutationType.CompleteTask, { id: props.id, completed: !props.completed }) } const removeTask = () => { store.commit(MutationType.RemoveTask, { id: props.id }) } const viewTask = () => { store.commit(MutationType.SetTaskModal, {showModal:true, taskId:props.id}) } const editTask = () => { store.commit(MutationType.SetEditModal, {showModal:true, taskId: props.id}) } return { toggleCompletion, removeTask, editTask, viewTask } } }) </script>
TaskItem
This component receives the task id props from the TaskList component. The id props are used to fetch a task with the corresponding id via the getTaskById getters method and render these task properties in the browser.
We will create a computed property:
task
– that calls thegetTaskById
getter method to retrieve a task by its Id
Also, we will create a method:
closeModal
– that commits theSetTaskModal
mutation which sets theshowModal
property in the state to false.
Within the components folder, let’s create a new TaskItem.vue
file:
//components/TaskItem.vue <template> <div class="modal is-active"> <div class="modal-background"></div> <div class="modal-content"> <h1>VIEW TASK</h1> <div class="card"> <div class="card-content"> <div class="media"> <div class="media-content"> <p class="title is-4">Title: {{ task.title }}</p> <p class="subtitle is-6"> <b>Assigned To:</b> {{ task.assignedTo }}</p> <p class="subtitle is-6"> <b>Created By:</b> {{ task.createdBy }}</p> </div> </div> <div class="content"> <p class="subtitle is-6">Description: {{ task.description }}</p> </div> </div> </div> </div> <button class="modal-close is-large" @click="closeModal" aria-label="close" ></button> </div> </template> <script lang="ts"> import { defineComponent, reactive, toRefs, computed, onMounted } from 'vue' import { useStore } from '@/store' import { TaskItem } from '@/store/state' import { MutationType } from '@/store/mutations' export default { name: 'EditModal', props: { id: { type: Number, required: true } }, setup(props: any) { const store = useStore() const task = computed(() => store.getters.getTaskById(Number(props.id))) const closeModal = () => { store.commit(MutationType.SetTaskModal, {showModal :false, taskId: undefined}) } return { closeModal, task } } } </script>
CreateModal
This component is responsible for creating tasks.
Hence we will create two methods to mutate our state:
createTask
– that commits theCreateTask
mutation (which adds a new task to the tasks property in the state).closeModal
– that commits theSetTaskModal
mutation (which sets showModal property in the state to false).
Within the components folder, let’s create a new CreateModal.vue file:
<template> <div class="modal is-active"> <div class="modal-background"></div> <div class="modal-content"> <form @submit.prevent="createTask"> <div class="field"> <label class="label">Task Title</label> <div class="control"> <input v-model="title" class="input" type="text" placeholder="Enter task" /> </div> </div> <div class="field"> <label class="label">Description</label> <div class="control"> <textarea v-model="description" class="textarea" placeholder="Textarea" ></textarea> </div> </div> <div class="field"> <label class="label">Assigned By</label> <div class="control"> <input v-model="createdBy" class="input" type="text" placeholder="Enter Assigner's name" /> </div> </div> <div class="field"> <label class="label">Assigned To</label> <div class="control"> <input v-model="assignedTo" class="input" type="text" placeholder="Enter task creator's name" /> </div> </div> <div class="field is-grouped"> <div class="control"> <button type="submit" class="button is-link">Submit</button> </div> <div class="control" @click="closeModal"> <button class="button is-link is-light">Cancel</button> </div> </div> </form> </div> <button class="modal-close is-large" @click="closeModal" aria-label="close" ></button> </div> </template> <script lang="ts"> import { defineComponent, reactive, toRefs } from 'vue' import { useStore } from '@/store' import { TaskItem } from '@/store/state' import { MutationType } from '@/store/mutations' export default { name: 'CreateModal', setup() { const state = reactive({ title: '', description: '', createdBy: '', assignedTo: '' }) const store = useStore() const createTask = () => { if ( state.title === '' || state.description === '' || state.createdBy === '' || state.assignedTo === '' ) return const task: TaskItem = { id: Date.now(), title: state.title, description: state.description, createdBy: state.createdBy, assignedTo: state.assignedTo, completed: false, editing: false } store.commit(MutationType.CreateTask, task) state.title = '' state.createdBy = '' state.assignedTo = '' state.description = '' } const closeModal = () => { store.commit(MutationType.SetCreateModal, false) } return { closeModal, ...toRefs(state), createTask } } } </script>
Notice how we used vue3’s reactive
method to store values that is used in the template.
We could create a variable as we normally do inside a setup function and add it to the returned object, then render it in the template. This will work but there will be no reactivity. Also we could use refs
but then, it is used on primitives (strings, numbers, booleans).
When using reactive
, we need to use toRefs
to convert the reactive object to a plain object, where each property on the resulting object is a ref pointing to the corresponding property in the original object.
EditModal
This component is responsible for updating tasks. Its logic is similar to the CreateTask
component we just discussed above.
Hence we will create two methods to mutate our state:
updateTask
– that commits theUpdateTask
mutation (which modifies a task with a corresponding id in the task’s property of the state).closeModal
– that commits theSetTaskModal
mutation (which sets showModal property in the state to false).
Within the components folder, let’s create a new EditModal.vue
file:
<template> <div class="modal is-active"> <div class="modal-background"></div> <div class="modal-content"> <form @submit.prevent="updateTask"> <h1>Edit Modal</h1> <div class="field"> <label class="label">Task Title</label> <div class="control"> <input v-model="title" class="input" type="text" placeholder="Enter task" /> </div> </div> <div class="field"> <label class="label">Description</label> <div class="control"> <textarea v-model="description" class="textarea" placeholder="Textarea" ></textarea> </div> </div> <div class="field"> <label class="label">Assigned By</label> <div class="control"> <input v-model="createdBy" class="input" type="text" placeholder="Enter Assigner's name" /> </div> </div> <div class="field"> <label class="label">Assigned To</label> <div class="control"> <input v-model="assignedTo" class="input" type="text" placeholder="Enter task creator's name" /> </div> </div> <div class="field is-grouped"> <div class="control"> <button type="submit" class="button is-link">Submit</button> </div> <div class="control"> <button class="button is-link is-light">Cancel</button> </div> </div> </form> </div> <button class="modal-close is-large" @click="closeModal" aria-label="close" ></button> </div> </template> <script lang="ts"> import { defineComponent, reactive, toRefs, computed, onMounted } from 'vue' import { useStore } from '@/store' import { TaskItem } from '@/store/state' import { MutationType } from '@/store/mutations' export default { name: 'EditModal', props: { id: { type: Number, required: true } }, setup(props: any) { const state = reactive({ title: '', description: '', createdBy: '', assignedTo: '' }) const store = useStore() const getTaskById = computed(() => store.getters.getTaskById(Number(props.id))) const setFields = () => { const task = store.getters.getTaskById(Number(props.id)) if(task) { state.title = task.title state.createdBy = task.createdBy state.assignedTo = task.assignedTo state.description = task.description } } onMounted(() => { setFields() }); const updateTask = () => { if ( state.title === '' || state.description === '' || state.createdBy === '' || state.assignedTo === '' ) return const task: TaskItem = { id: props.id, title: state.title, description: state.description, createdBy: state.createdBy, assignedTo: state.assignedTo, completed: false, editing: false } store.commit(MutationType.UpdateTask, task) state.title = '' state.createdBy = '' state.assignedTo = '' state.description = '' } const closeModal = () => { store.commit(MutationType.SetEditModal, {showModal: false, taskId: undefined}) } return { closeModal, ...toRefs(state), updateTask } } } </script> <style scoped> label { color: #ffffff; } h1 { color: #ffffff; text-align: center; font-size: 2rem; margin-bottom: 3rem; } </style>
In order to get the task properties already displayed in the form fields when the component is mounted, we had to call the setFields
method on the onMounted
lifecycle hook. The setFields
triggers the getTaskById
getters method to fetch a task from the state by its id, then update the properties in the reactive object with the fetched task properties.
Now you can run the application with:
npm run serve
Conclusion
We have used most of Vuex 4 major feature releases including its robust typing for the store and excellent integration with the Composition API in Vue 3 to build a dynamic task management application. We have practically explored the features of the Vue 3 composition API.
Also, we have seen how the createLogger
function is used to log the mutations and state to the browser’s console making debugging the store easy even without the Vue developer tool.
I hope you have learned a great deal from this tutorial. Do reach out in the comment section below if you have any questions or suggestions.
Here is the github repo for the task management application.
Very insightful article
Thank you Janet. I am glad you found it useful.
easy to understand, thanks
Nice 👍
Thanks for this great article. Is it possible to have different modules to?
You’re welcome Jan! Yes it is possible
This is an incredible and informative post, it’s practically a course by itself. Thanks a lot!
You’re welcome Jean
Very helpfull, thank you!
One question: Do you see any problem in making the payload parameter in the ActionAugments type nullable?
For one of my actions i dont need any payload and just want to fire a mutation but i get the error that payload wasnt provided.
I can get rid of this error by doing “payload?: Parameters[1]” just wanted to confirm somehwere if this is the right way or if i might dont see something 🙂
Thanks in advance!