Connecting UI Components to the Vuex Store | Full-Stack Google Contacts Clone with AdonisJS Framework (Node.js) and Quasar Framework (Vue.js)
In this lesson, we will connect our user interface components to the Vuex store. The components are the ui/src/pages/Index.vue
which is the homepage component used for displaying the contacts table, ui/src/pages/contacts/CreateContact.vue
used for creating and editing a contact, and ui/src/pages/contacts/ViewContact.vue
used for displaying the details of a contact.
We will continue with our git branch: 17-connect-components-to-vuex-store
. If you aren't using that branch, do the following:
# Make sure you are within your project
git checkout 17-connect-components-to-vuex-store
Pointing the ui/src/pages/Index.vue
component at the Vuex Store
Open the ui/src/pages/Index.vue
file. Refer to this snapshot of the file. Copy-and-paste the content of the snapshot into the ui/src/pages/Index.vue
file. The template
section remains on changed. So we focus on the script
section.
From Line 127 to 129, we disable some eslint
rules for the entire script
section. This is because the types of Vuex methods and modules are not compatible when they interact.
From Line 130 to 138, we import additional hooks and types from vue
. At Line 139, we import the Contact
interface from ui/src/types.ts
.
At Line 141, we import the useStore
hook/function from the Vuex store. The useStore
hook will be used to instantiate the store inside our component.
At Line 147, we call useStore()
to initialise our store
.
At Line 148, we make the loading
ref to be true
by default. This ensures that when the contacts table is rendered, the loading indicator will be shown by default.
At Line 149, we set pageSize
to 50
. This is the number of rows which will be fetch for each page of contacts requested from the API server.
At Line 150, we initialise the nextPage
ref to 1
. The first page to be first is page 1. This value will be incremented after each page of contacts is fetched.
At Line 151, we define the tableRef
which will be used to store the reference to the QTable
component. This tableRef
will be used later to call methods defined by QTable
.
At Line 153, we replace the rows
computed ref with the contacts
computed ref. The contacts
computed ref is used to automatically fetch and store the value of the contacts/contactList
Vuex getter. The contacts/contactList
getter contains all the contacts currently fetched from the API server for display on the contacts table.
const contacts = computed(
() => store.getters["contacts/contactList"] as Contact[]
);
At Line 157, we define a computed ref totalContacts
to store the value of the contacts/totalContacts
getter.
const totalContacts = computed(
() => store.getters["contacts/totalContacts"] as number
);
From Line 161 to 173, we employ the watchEffect
hook so that the contacts/LOAD_CONTACTS
action is dispatched immediately the component is created. Since the Vuex actions are promises, we have await
them. The contacts/contactList
action attaches a payload containing the nextPage
and pageSize
properties.
const stopContactListEffect = watchEffect(async () => {
await store
.dispatch("contacts/LOAD_CONTACTS", {
nextPage: nextPage.value,
pageSize,
})
.then(() => {
void nextTick(() => {
tableRef.value?.refresh();
loading.value = false;
});
});
});
Also, the watchEffect
hook is stored in the constant stopContactListEffect
. At Line 230, we call the onBeforeUnmount
hook with the stopContactListEffect
as the handler. This removes the watchEffect
from the component and prevent memory leaks. The watchEffect
hook will automatically add variables used within it to its dependencies. If any of the values change, the watchEffect
hook will re-run and lead to new contacts being fetched from the API server. By default, the watchEffect
hook will run when the component is created in the setup
hook.
When the contacts/LOAD_CONTACTS
action is resolved, from Line 167, we call the nextTick
hook and instruct the table to refresh (tableRef.value?.refresh();
) and disable the loading state of the table (loading.value = false;
). The nextTick
hook is important so that the new value of the contacts/contactList
getter is completely propagated and stored in the contacts
computed ref at Line 153 before we refresh the table. Additionally, the contacts/LOAD_CONTACTS
action will also update the totalContacts
store property which will be propagated to the totalContacts
computed ref at Line 157.
At Line 175, we define a constant lastPage
, which stores the last page of all contacts we have based on the values of pageSize
and totalContacts
variables.
At Line 177, we modify the onScroll
function which is attached to the QTable
component as the handler of the virtual-scroll
event (@virtual-scroll="onScroll"
). See Line 28.
+ const onScroll = function ({ to, ref: ref2 }: VirtualScrollCtx): void {
if (
loading.value !== true &&
nextPage.value < lastPage &&
- to === lastIndex
+ to === nextPage.value * pageSize - 1
) {
+ tableRef.value = ref2;
loading.value = true;
-
- setTimeout(() => {
- nextPage.value++;
- void nextTick(() => {
- ref2.refresh();
- loading.value = false;
- });
- }, 500);
+ nextPage.value++;
}
};
The most important change above is the replacement of the setTimeout
function (which was used to simulate an asynchronous operation) with nextPage.value++
. When nextPage.value
is incremented, it triggers the re-run of the watchEffect
hook (because nextPage
is a dependency in the watchEffect
hook) at Line 161 which leads to the dispatch of the contacts/LOAD_CONTACTS
action and population of the contacts table with new contacts for rendering.
At Line 217, we assign our contacts
computed property to the rows
property of the object returned from our setup
hook.
At Line 243 and 244, we update thepagination
property as shown below:
pagination: {
rowsPerPage: 0,
- rowsNumber: rows.value.length,
+ page: nextPage.value,
+ rowsNumber: totalContacts.value,
},
This concludes the changes in the ui/src/pages/Index.vue
component.
Pointing the ui/src/pages/contacts/CreateContact.vue
component at the Vuex Store
Open the ui/src/pages/contacts/CreateContact.vue
file. Refer to this snapshot of the file. Copy-and-paste the content of the snapshot into the ui/src/pages/contacts/CreateContact.vue
file. The template
section remains on changed. So we focus on the script
section.
At Line 56, we disable the no-misused-promises
eslint rule. At Line 64, we import the nextTick
hook from vue
. At Line 70, we import the useStore
hook from our store
.
Within the setup
hook, at Line 94, the useStore
hook is called and used to initialise the store
constant.
At Line 274, we replace the contact
reactive object with the currentContact
computed property which stores the value of the contacts/currentContact
getter from our Vuex store.
At Line 283, within the watchEffect
hook, we introduce the contacts/LOAD_CURRENT_CONTACT
action which is immediately dispatched when the component is created. The action sends the contactId
prop as the id
of the contact being viewed or edited. Remember that the contactIdprop is automatically set by
vue-routerwhen that route is visited. Then the action is resolved, we use the
nextTick` hook to execute the iteration in the next process tick.
await store
.dispatch("contacts/LOAD_CURRENT_CONTACT", props.contactId)
.then(() => {
void nextTick(() => {
Object.keys(currentContact.value).forEach((key) => {
if (key !== "id") {
form[key].value = currentContact.value[key];
}
});
});
});
The iteration above (Lines 287 to 291) loops through the keys (omitting the id
key) in the currentContact
computed ref and assigns the value of the keys to the value
property of each property in the form
reactive object. This ensures that the fields on our form are populated with the values from the currentContact
computed property.
This concludes the modifications in the ui/src/pages/contacts/CreateContact.vue
component.
Pointing the ui/src/pages/contacts/ViewContact.vue
component at the Vuex Store
Open the ui/src/pages/contacts/ViewContact.vue
file. Refer to this snapshot of the file. Copy-and-paste the content of the snapshot into the ui/src/pages/contacts/ViewContact.vue
file. The template
section remains on changed. So we focus on the script
section.
From Lines 201 to 210, we define the ContactData
type:
type ContactData = Array<{
icon: string;
text: string | undefined | null | Array<string | null | undefined>;
label: string;
key: string;
side?: string | undefined;
sideColor?: string | undefined;
clampLines?: number | "none";
linkAs?: "email" | "tel" | "website";
}>;
At Line 205, we remove the reactive
hook from vue
as it is no longer need. We also remove the contacts
import from Google_Contacts_Clone_Mock_Data
.
At Line 223, we import the useStore
hook from our Vuex store.
At Line 235, we instantiate the store
reference and the currentContact
computed ref
const store = useStore();
const currentContact = computed(
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
() => store.getters["contacts/currentContact"] as Contact
);
At Line 242, within the watchEffect
hook, we call the contacts/LOAD_CURRENT_CONTACT
action with the contactId
prop as the id
payload to load the current contact for the route.
At Line 247, we update the fullName
computed property to concatenate the firstName
and lastName
from the currentContact
computed property. Same for the jobDescription
computed property at Lines 251 to 253.
From Line 256, we update the text property of each contactData
object to read from the currentContact
computed property.
At Line 333 to 339, we return currentCurrent
, fullName
, contactData
, jobDescription
, and isNullArray
from the setup
hook so that they are all available in the template
section.
Save all files, serve the frontend, and test the functionalities on the contact tables, and view and edit a contact.
cd ui
yarn serve
This concludes this lesson. In the next lesson, we will connect the frontend of the Google Contacts Clone app to the API server.
Commit, merge with the master branch, and push to the remote repository (GitHub).
git add .
git commit -m "feat(api): complete connection of components to vuex store"
git push origin 17-connect-components-to-vuex-store
git checkout master
git merge master 17-connect-components-to-vuex-store
git push origin master