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

**Author:** Ndianabasi Udonkang  
**Published:** 2021-09-16

In this lesson, we complete our discussion on how to design the contacts table with the QTable component from the Quasar framework.

---

## Tags

- [JavaScript](/llms/technical-blog/tag/javascript.md)
- [Nodejs](/llms/technical-blog/tag/nodejs.md)
- [Vuejs](/llms/technical-blog/tag/vuejs.md)
- [Adonisjs](/llms/technical-blog/tag/adonisjs.md)
- [Quasar Framework](/llms/technical-blog/tag/quasar-framework.md)

## Part of Series: [Full-Stack Google Contacts Clone with Node.js (Adonisjs Framework) and Vue.js (Quasar Framework)](/llms/technical-blog/series/clnwu9bc991hiu3wo2vwmw45/full-stack-google-contacts-clone-with-node-js-adonisjs-framework-and-vue-js-quasar-framework.md)

This article is part of the **[Full-Stack Google Contacts Clone with Node.js (Adonisjs Framework) and Vue.js (Quasar Framework)](/llms/technical-blog/series/clnwu9bc991hiu3wo2vwmw45/full-stack-google-contacts-clone-with-node-js-adonisjs-framework-and-vue-js-quasar-framework.md)** series.

### Articles in this Series:

- [Introduction to Full-Stack Google Contacts Clone with Node.js (Adonisjs Framework) and Vuejs (Quasar Framework)](/llms/technical-blog/article/xe5mlxg43s9rpx1pxsvfdp3r/introduction-to-full-stack-google-contacts-clone-with-node-js-adonisjs-framework-and-vuejs-quasar-framework.md)
- [Workspace Setup | Full-Stack Google Contacts Clone with Adonis.js/Node.js and Quasar (Vue.js)](/llms/technical-blog/article/fo8str8yjpkz5dygxg875bpw/workspace-setup-full-stack-google-contacts-clone-with-adonis-js-node-js-and-quasar-vue-js.md)
- [Frontend Overview | Full-Stack Google Contacts Clone with Adonis.js/Node.js and Quasar (Vue.js)](/llms/technical-blog/article/nxgmng6fhf1cwjk30coflro2/frontend-overview-full-stack-google-contacts-clone-with-adonis-js-node-js-and-quasar-vue-js.md)
- [The Left Drawer | Full-Stack Google Contacts Clone with Adonis.js/Node.js and Quasar (Vue.js)](/llms/technical-blog/article/m6fagvy2il5h65llxxbbl9os/the-left-drawer-full-stack-google-contacts-clone-with-adonis-js-node-js-and-quasar-vue-js.md)
- [Improve The Header | Full-Stack Google Contacts Clone with Adonis.js/Node.js and Quasar (Vue.js)](/llms/technical-blog/article/jjwbxcg450jmkkcrrmkmwvwj/improve-the-header-full-stack-google-contacts-clone-with-adonis-js-node-js-and-quasar-vue-js.md)
- [New Contact Form Design | Full-Stack Google Contacts Clone with Adonis.js/Node.js and Quasar (Vue.js)](/llms/technical-blog/article/ebguwpkntk3empakunrki4dq/new-contact-form-design-full-stack-google-contacts-clone-with-adonis-js-node-js-and-quasar-vue-js.md)
- [Validating the Contact Form with Vuelidate | Full-Stack Google Contacts Clone with Adonis.js/Node.js and Quasar (Vue.js)](/llms/technical-blog/article/z0xr9m2ljler2cwnpsw2ycwa/validating-the-contact-form-with-vuelidate-full-stack-google-contacts-clone-with-adonis-js-node-js-and-quasar-vue-js.md)
- [Designing the Contacts Table | Full-Stack Google Contacts Clone with Adonis.js/Node.js and Quasar (Vue.js)](/llms/technical-blog/article/h2p1x1z406ib38zu2dci3jur/designing-the-contacts-table-full-stack-google-contacts-clone-with-adonis-js-node-js-and-quasar-vue-js.md)
- **Designing the Contacts Table (Part 2) | Full-Stack Google Contacts Clone with Adonis.js/Node.js and Quasar (Vue.js)** (Current Article)
- [Refactoring the Main Layout | Full-Stack Google Contacts Clone with Adonis.js/Node.js and Quasar (Vue.js)](/llms/technical-blog/article/uktmlqsivwtr3cvnpld59k9q/refactoring-the-main-layout-full-stack-google-contacts-clone-with-adonis-js-node-js-and-quasar-vue-js.md)
- [Improving User Experience with the Contacts Table | Full-Stack Google Contacts Clone with Adonis.js/Node.js and Quasar (Vue.js)](/llms/technical-blog/article/dzs812auf61ej7qu2cg726dh/improving-user-experience-with-the-contacts-table-full-stack-google-contacts-clone-with-adonis-js-node-js-and-quasar-vue-js.md)
- [Designing the Contact Details Page | Full-Stack Google Contacts Clone with Adonis.js/Node.js and Quasar (Vue.js)](/llms/technical-blog/article/kf9pz97n63fvgfrcy5viwzrg/designing-the-contact-details-page-full-stack-google-contacts-clone-with-adonis-js-node-js-and-quasar-vue-js.md)
- [Creating the Contact Edit Page | Full-Stack Google Contacts Clone with Adonis.js/Node.js and Quasar (Vue.js)](/llms/technical-blog/article/fmr90prj84c9zn0633jc368g/creating-the-contact-edit-page-full-stack-google-contacts-clone-with-adonis-js-node-js-and-quasar-vue-js.md)
- [How Software Backends Work | Full-Stack Google Contacts Clone with AdonisJs (Node.js) and Quasar Framework (Vue.js)](/llms/technical-blog/article/aca8fcrghz70nn1wqg3uzbum/how-software-backends-work-full-stack-google-contacts-clone-with-adonis-js-node-js-and-quasar-framework-vue-js.md)
- [Setting Up The Backend | Full-Stack Google Contacts Clone with AdonisJs (Node.js) and Quasar Framework (Vue.js)](/llms/technical-blog/article/fxb49fmz9ljbwrimbnnzudfv/setting-up-the-backend-full-stack-google-contacts-clone-with-adonis-js-node-js-and-quasar-framework-vue-js.md)
- [Why Choose the AdonisJs Framework? | Full-Stack Google Contacts Clone with AdonisJs (Node.js) and Quasar Framework (Vue.js)](/llms/technical-blog/article/lnhm5oudx34yo2869gmcms37/why-choose-the-adonis-js-framework-full-stack-google-contacts-clone-with-adonis-js-node-js-and-quasar-framework-vue-js.md)
- [Setting Up Our API Server with AdonisJs Framework | Full-Stack Google Contacts Clone with AdonisJs (Node.js) and Quasar Framework (Vue.js)](/llms/technical-blog/article/x1jgmet84aqj9qggu5mulkh0/setting-up-our-api-server-with-adonis-js-framework-full-stack-google-contacts-clone-with-adonis-js-node-js-and-quasar-framework-vue-js.md)
- [Setting Up Postman with the API Server | Full-Stack Google Contacts Clone with AdonisJS (Node.js) and Quasar Framework (Vue.js)](/llms/technical-blog/article/v4qehksislosmuy92qgjr4ct/setting-up-postman-with-the-api-server-full-stack-google-contacts-clone-with-adonis-js-node-js-and-quasar-framework-vue-js.md)
- [The Model-View-Controller Design Pattern in AdonisJS | Full-Stack Google Contacts Clone with AdonisJS (Node.js) and Quasar Framework (Vue.js)](/llms/technical-blog/article/h5wqql65iejop5xrplujw458/the-model-view-controller-design-pattern-in-adonis-js-full-stack-google-contacts-clone-with-adonis-js-node-js-and-quasar-framework-vue-js.md)
- [Create Column Definitions & Insert New Contacts | Full-Stack Google Contacts Clone with AdonisJS (Node.js) and Quasar Framework (Vue.js)](/llms/technical-blog/article/asc2emsho4u73r6jb2gxjddw/create-column-definitions-and-insert-new-contacts-full-stack-google-contacts-clone-with-adonis-js-node-js-and-quasar-framework-vue-js.md)
- [Data Validation & Sanitisation with AdonisJS | Full-Stack Google Contacts Clone with AdonisJS (Node.js) and Quasar Framework (Vue.js)](/llms/technical-blog/article/bcey0ac7m5sbs9hqy1h824sl/data-validation-and-sanitisation-with-adonis-js-full-stack-google-contacts-clone-with-adonis-js-node-js-and-quasar-framework-vue-js.md)
- [Creating a Middleware and Updating a Contact | Full-Stack Google Contacts Clone with AdonisJS (Node.js) and Quasar Framework (Vue.js)](/llms/technical-blog/article/oqxzmtgfy1r3oewpnedc7wf6/creating-a-middleware-and-updating-a-contact-full-stack-google-contacts-clone-with-adonis-js-node-js-and-quasar-framework-vue-js.md)
- [Fetching and Deleting a Contact with AdonisJS | Full-Stack Google Contacts Clone with AdonisJS Framework (Node.js) and Quasar Framework (Vue.js)](/llms/technical-blog/article/p6bh0ponq43u3vi6o8uww84x/fetching-and-deleting-a-contact-with-adonis-js-full-stack-google-contacts-clone-with-adonis-js-framework-node-js-and-quasar-framework-vue-js.md)
- [Using Model Factories and Seeders in AdonisJS | Full-Stack Google Contacts Clone with AdonisJS Framework (Node.js) and Quasar Framework (Vue.js)](/llms/technical-blog/article/cfotveuxbgjtdp12l6pzcb2y/using-model-factories-and-seeders-in-adonis-js-full-stack-google-contacts-clone-with-adonis-js-framework-node-js-and-quasar-framework-vue-js.md)
- [Creating and Registering a Vuex Module | Full-Stack Google Contacts Clone with AdonisJS Framework (Node.js) and Quasar Framework (Vue.js)](/llms/technical-blog/article/cw96ewnzujgsz2e2garhbuzb/creating-and-registering-a-vuex-module-full-stack-google-contacts-clone-with-adonis-js-framework-node-js-and-quasar-framework-vue-js.md)
- [Connecting UI Components to the Vuex Store | Full-Stack Google Contacts Clone with AdonisJS Framework (Node.js) and Quasar Framework (Vue.js)](/llms/technical-blog/article/fdyqp17yehbnqrjvjvab4b5b/connecting-ui-components-to-the-vuex-store-full-stack-google-contacts-clone-with-adonis-js-framework-node-js-and-quasar-framework-vue-js.md)
- [Connecting the Frontend to the API Server | Full-Stack Google Contacts Clone with AdonisJS Framework (Node.js) and Quasar Framework (Vue.js)](/llms/technical-blog/article/fk4262359gg3g7f0jn95fn9b/connecting-the-frontend-to-the-api-server-full-stack-google-contacts-clone-with-adonis-js-framework-node-js-and-quasar-framework-vue-js.md)
- [Creating and Updating Contacts From the Frontend | Full-Stack Google Contacts Clone with AdonisJS Framework (Node.js) and Quasar Framework (Vue.js)](/llms/technical-blog/article/t15w2s3ao9jo74l96stjo7z6/creating-and-updating-contacts-from-the-frontend-full-stack-google-contacts-clone-with-adonis-js-framework-node-js-and-quasar-framework-vue-js.md)
- [Uploading Files and Creating Avatars for Contacts With AdonisJS and Axios](/llms/technical-blog/article/hg9ilaubqesm8angg0rhrjp9/uploading-files-and-creating-avatars-for-contacts-with-adonis-js-and-axios.md)
- [Deleting a Contact with AdonisJS and Vuejs](/llms/technical-blog/article/zdr4pxmgcax2dsfnwimhty0g/deleting-a-contact-with-adonis-js-and-vuejs.md)

---

## Article Content

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:

```diff
<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.

```diff
<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:

```ts
<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(() => {
          nextPage.value++;
          void nextTick(() => {
            ref2.refresh();
            loading.value = false;
          });
        }, 500);
      }
    };
    return {
      selected,
      columns,
      rows,
      nextPage,
      loading,
      pagination: {
        rowsPerPage: 0,
        rowsNumber: rows.value.length,
      },
      onScroll,
    };
  },
});
</script>
```

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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice) the `contacts` array from index 0 to `pageSize * (nextPage.value - 1) when the `lastIndex`const inside the`onScroll`function is initiated. For example, when the table is first rendered, rows from index 0 to index 49 (50 rows) will be extracted and returned to`q-table`for rendering. In the next firing of the event, rows from 0 to index 99 (100 rows) will be returned to`q-table`). So the array consumed by the `rows`prop of`q-table`keeps getting longer as we scroll down. This is the magic behind`virtual 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](https://v3.vuejs.org/api/global-api.html#nexttick).

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](https://quasar.dev/vue-components/table#qtable-api).

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:

```ts
{
   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.

```bash
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
```


*This document was generated from the live article page on https://ndianabasi.com/technical-blog/article/j0iscua9gg8ekuz8ifh6chpn/designing-the-contacts-table-part-2-full-stack-google-contacts-clone-with-adonis-js-node-js-and-quasar-vue-js • 2026-06-07*
