Designing the Contacts Table (Part 2) | Full-Stack Google Contacts Clone with Adonis.js/Node.js and Quasar (Vue.js)

Designing the Contacts Table (Part 2) | Full-Stack Google Contacts Clone with Adonis.js/Node.js and Quasar (Vue.js)

Table of contents

Due to the length of this lesson, I had to split it into two parts. The first part surprisingly reached a 12-minute read. I think that was too long.

In this second part, we will continue with discussion on how the Contacts table is built.

We remove some redundant import and bring in new ones:

<script lang="ts">
- import { Todo, Meta } from "components/models";
- import ExampleComponent from "components/CompositionComponent.vue";
- import { defineComponent, ref } from "vue";
+ import { defineComponent, ref, computed, nextTick } from "vue";
+ import { VirtualScrollCtx } from "../types";
+ import { contacts } from "../data/Google_Contacts_Clone_Mock_Data";
+ import columns from "../data/table-definitions/contacts";

The imports are self-explanatory.

We define some constants within the script scope. The constants could have equally being defined within the setup function.

<script lang="ts">
+ const pageSize = 50;
+ const lastPage = Math.ceil(contacts.length / pageSize);

We remove the redundant todo array ref.

Our entire script function should look like this:

<script lang="ts">
import { defineComponent, ref, computed, nextTick } from "vue";
import { VirtualScrollCtx } from "../types";
import { contacts } from "../data/Google_Contacts_Clone_Mock_Data";
import columns from "../data/table-definitions/contacts";

const pageSize = 50;
const lastPage = Math.ceil(contacts.length / pageSize);

export default defineComponent({
  name: "HomePage",
  components: {},
  setup() {
    const nextPage = ref(2);
    const loading = ref(false);
    const selected = ref([]);
    const rows = computed(() =>
      contacts.slice(0, pageSize * (nextPage.value - 1))
    const onScroll = function ({ to, ref: ref2 }: VirtualScrollCtx): void {
      const lastIndex = rows.value.length - 1;
      if (
        loading.value !== true &&
        nextPage.value < lastPage &&
        to === lastIndex
      ) {
        loading.value = true;
        setTimeout(() => {
          void nextTick(() => {
            loading.value = false;
        }, 500);
    return {
      pagination: {
        rowsPerPage: 0,
        rowsNumber: rows.value.length,

So what's going on inside the setup function?

  1. When we begin scrolling on the table and approach the end of the initial number of rows loaded (the pageSize constant defined the initial amount of rows), the virtual-scroll event will be fired and the onScroll function executed. The main thing happening is that the computed ref, rows, is computed by slicing the contacts array from index 0 to pageSize * (nextPage.value - 1) when thelastIndexconst inside theonScrollfunction is initiated. For example, when the table is first rendered, rows from index 0 to index 49 (50 rows) will be extracted and returned toq-tablefor rendering. In the next firing of the event, rows from 0 to index 99 (100 rows) will be returned toq-table). So the array consumed by therowsprop ofq-tablekeeps getting longer as we scroll down. This is the magic behindvirtual scrolling` and this behaviour is very different from the normal pagination which returns a constant number of rows for every new page requested and these new (longer) rows replaces the existing ones.

  2. However, if we do not increment the nextPage ref, virtual scrolling won't work because the same number of rows will be returned each time. So, we increment nextPage at Line 79 but only after checking that the table is not in a loading state (loading.value !== true) and that we haven't gotten to the last page (nextPage.value < lastPage) and that variable to equals const lastIndex (to === lastIndex). The to variable is a property of the event payload sent when the virtual-scroll event is emitted. It is passed into the onScroll function as the only argument. We destructure the payload to get to and ref (the payload contains other properties. Read the QTable API). The to variable represents the next page of rows which should be loaded, while the ref variable which is renamed to ref2 to avoid colliding with the ref variable imported from the vue library represents the current component i.e. QTable. If the if condition is satisfied, the table is put into a loading state (Line 76) while the setTimeout function simulates a 500 milliseconds delay before the nextPage ref is increment, the QTable component is refreshed (ref2.refresh()) and loading is set to false again.

    The setTimeout function is used to simulate an asynchronous operation in a real-life scenario where you have to fetch the new rows from the database.

    Note that nextTick is used to refresh the table after the next DOM update after nextPage is incremented. The proper use of nextTick can help resolve DOM update issue and ensure that a particular constant or variable is properly updated before it is read. Read more about nextTick here.

  3. On Line 96, the pagination object is returned with rowsPerPage always being 0 and the rowsNumber always being the length of the rows array. Recall that rowsPerPage being 0 indicates that we want to always display all the rows within the page. That is, skip the traditional pagination strategy.

The QTable slots used

Within the template section, we have used two slots: top-row and body slots. Read more about these slots here.

  1. The top-row slot is used to override the content of the very first row of the QTable component with a custom content. This is the slot we used to render the text: Starred Contacts (xx). The placeholder xx will be replaced with an actual number down the line.

  2. The body slot is used to customise the rendering of the body of the QTable component. For our use-case, this was necessary to inject a component, QAvatar, for rendering the profilePicture column. This is how the profile pictures are displayed. The body slot exposes the props object from QTable which contains (but not limited) to the following properties:

   key : Any, // Row's key
   row : Object, // Row object
   rowIndex : Number, // Row's index (0 based) in the filtered and sorted table
   cols : Object, // Column definitions
   selected: Boolean, // Is row selected? Can directly be assigned new Boolean value which changes selection state

We loop through all our columns props.cols. When props.cols === 'profilePicture', we render the QAvatar component. Else, we interpolate the value of that column (col.value) with the span element.

Customising the body slot breaks down the checkbox functionality. So, we have manually include the checkbox via the QCheckbox component within the first q-td component and assign props.selected as the value of the v-model for the QCheckbox component to check when the checkbox is checked or not.

So, this covered everything about how the Contact table was designed/developed. In the next lesson, we will customise the QTable a little more and improve some other aspects of the UI a little more before we go into the backend scope of this series.

Save all your files, commit and merge with the master branch.

git add .
git commit -m "feat(ui): complete design of the contacts table"
git push --set-upstream origin 05-the-contacts-table
git checkout master
git merge master 05-the-contacts-table
git push