# Designing the Contacts Table | 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 will learn how to implement the Contacts table to display all our contacts. We will also implement virtual (infinite) scrolling.

---

## Tags

- [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)** (Current Article)
- [Designing the Contacts Table (Part 2) | Full-Stack Google Contacts Clone with Adonis.js/Node.js and Quasar (Vue.js)](/llms/technical-blog/article/j0iscua9gg8ekuz8ifh6chpn/designing-the-contacts-table-part-2-full-stack-google-contacts-clone-with-adonis-js-node-js-and-quasar-vue-js.md)
- [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

In this lesson, we will learn how to implement the [Quasar Table component](https://quasar.dev/vue-components/table) for displaying all our contacts. We will also implement virtual scrolling on the table for mimicking infinite scrolling for the contacts. This way, we won't have an obvious pagination (though the virtual scrolling implements pagination under the hood) with a manual pagination component. This is how the contacts table on the real Google Contacts app is implemented.

I have made use of mock data free from [Mockaroo](https://www.mockaroo.com/) to test the Contacts table. The mock data (of course) has the same fields (shape) as the form on the New Contact page. When we implement the backend and replace the mock data, we will fetch data from the backend with the same shape.

At the end of this lesson, your contacts table should work as demonstrated in the video below and you should understand how to implement the `QTable` component by reading the entire lesson.

%\[https://vimeo.com/606546112]

Start by creating a new branch of your project:

```bash
# Make sure you are within your project
git checkout -b 05-the-contacts-table
```

## Extra Setup

1. Let's fix some inconsistencies between `Prettier` and `Eslint` by install the `eslint-config-prettier` extension for `eslint`

Make sure you are in the root directory of your project, then:

```bash
cd ui # change into the `ui` directory
yarn add eslint-config-prettier -D # install the packages for `eslint-config-prettier` as a dev dependency
```

2. Copy-and-paste the entire content of this [`.eslintrc.js` file](https://github.com/ndianabasi/google-contacts/blob/05-the-contacts-table/ui/.eslintrc.js) into the `ui/.eslintrc.js` file.

3. Create a `.gitattributes` in the root of your project. Copy-and-paste the following in the created `.gitattributes` file. This will fix any issues with `git` and EOL (End of Line) markers.
   ```text
       *.js text eol=lf
       *.ts text eol=lf
   ```

Save all your files.

> Pre-emptive Warning: I noticed a several degradation of performance when the Vue DevTools is opened after virtual scrolling was implemented on the `QTable` component. If you notice that scrolling of the Contacts table is slow or seizing, close the Chrome DevTools completely.

## Recommended reads

It is highly recommended that you [study the `QTable` component](https://quasar.dev/vue-components/table) on the Quasar Docs. Study the `QTable` API and the dozens of use cases for the component on the page.

## Designing the Contacts form

Before we beginning discussion how the Contacts table will be designed, please created some important files. I will explain their purposes and you will understand how they are used as you read along.

### Create The Mock Data File

Create and open `ui/src/data/Google_Contacts_Clone_Mock_Data.ts` file which will contain our mock data. The mock data will be used to test our `Contacts table` in the absence of real data from the database. After creating the file, copy-and-paste all the content of [this snapshot](https://github.com/ndianabasi/google-contacts/blob/05-the-contacts-table/ui/src/data/Google_Contacts_Clone_Mock_Data.ts) into the `Google_Contacts_Clone_Mock_Data.ts` file.

### Modify the Type Index File

Open `ui/src/types/index.ts`. Add the following lines to the end of the file. Also make reference to [this snapshot](https://github.com/ndianabasi/google-contacts/blob/05-the-contacts-table/ui/src/types/index.ts) .

```ts
export interface Contact {
  id: string;
  firstName: string;
  surname: string;
  company?: string | null | undefined;
  jobTitle?: string | null | undefined;
  email1: string;
  email2?: string | null | undefined;
  phoneNumber1: string;
  phoneNumber2?: string | null | undefined;
  country?: string | null | undefined;
  streetAddressLine1?: string | null | undefined;
  streetAddressLine2?: string | null | undefined;
  city?: string | null | undefined;
  state?: string | null | undefined;
  birthday?: string | null | undefined;
  website?: string | null | undefined;
  notes?: string | null | undefined;
}

type SortStringToBooleanFn = (arg1: string, arg2: string) => boolean;
type SortStringToNumberFn = (arg1: string, arg2: string) => number;
type SortNumberFn = (arg1: number, arg2: number) => number;

export interface TableColumn {
  name: string;
  label: string;
  align?: string;
  sortable?: boolean;
  sort?: SortStringToBooleanFn | SortNumberFn | SortStringToNumberFn;
  field: string | ((row: TableRow) => unknown) | unknown;
  required?: boolean; // Use of `required` is important to avoid breaking QTable
  format?: unknown;
}

export interface VirtualScrollCtx {
  to: number;
  ref: {
    refresh: () => void;
  };
}
```

We just added the interfaces required to sufficient type our `column definition` file and some function calls for `QTable`.

The `Contact` interface defines the properties which each contact should have. This properties are used to define each object representing a contact in our mock data. Open the `ui/src/data/Google_Contacts_Clone_Mock_Data.ts` and compare the properties with the `Contact` interface. Some properties of the `Contact` interface are optional e.g. `company?: string | null | undefined;`. Since the property is optional, it is important to define its type as either a `string` or `null` or `undefined`. Meaning that we could supply a `null` value or omit that property completed when defining a `Contact` or assign an actual string value and you won't get TypeScript compilation errors.

> It is important to adequately type all objects, functions, classes, etc. which you introduce into your codebase. It might appear as though you are being slowed down initially. However, in the long run, it boosts your confidence while coding and drastically reduces mistakes. If you try typing the contents of the `ui/src/data/table-definitions/contacts.ts` file discussed below, you will see how TypeScript correctly suggests the properties of the objects.

The `TableColumn` interface is used to define the types of the properties of each column for our Contacts table. Two properties of the `TableColumn` are worthy of note and you will see how they are applied in the item 3 below. There are `field` and `format` properties. They have the following typings:

```ts
  field: string | ((row: TableRow) => unknown) | unknown;
  ...
  format?: unknown;
```

The `field` property is a required field which could be assigned a `string` directly or assigned a function. When you assign a `string` to `field`, `QTable` will map the value of that column for each row directly without any modification. However, if you assign a function, you have the flexibility to determine is returned, and when combined with the `format` field, you can manipulate your data before displaying.

The `format` property is optional and it used to manipulate/format your data before displaying it. For example, if you have a `Timestamp` column like `createdAt`, you could use the `format` property to manipulate the timestamp and make it display the time in the timezone of the user.

The `sortable` property is used to determine which column can be sorted or not. We will look at sorting when we implemented server-side data.

### Create the Column Definition File

Create and open `ui/src/data/table-definitions/contacts.ts` file which will hold the column definitions for our `Contacts table`. The column definition declares the properties of each column on our `Contacts table`. It is absolutely required by the `QTable` component. After create the `contacts.ts` file, copy-and-paste all the content of [this snapshot](https://github.com/ndianabasi/google-contacts/blob/05-the-contacts-table/ui/src/data/table-definitions/contacts.ts) into it.

The file exports an array of `TableColumn`s. `TableColumn` is an interface imported from `ui/src/types/index.ts` which was created above and defines the shape of each column in our column definition file, `ui/src/data/table-definitions/contacts.ts`. The array looks like this:

```ts
const columns: Array<TableColumn> = [
  {
    name: "profilePicture",
    label: "Profile Picture",
    align: "center",
    field: "profilePicture",
    sortable: false,
  },
  {
    name: "firstName",
    align: "center",
    label: "First Name",
    field: "firstName",
    sortable: true,
  },
  {
    name: "surname",
    align: "center",
    label: "Surname",
    field: "surname",
    sortable: true,
  },
  {
    name: "email1",
    align: "center",
    label: "Email 1",
    field: "email1",
    sortable: true,
  },
  {
    name: "phoneNumber1",
    align: "center",
    label: "Phone Number 1",
    field: "phoneNumber1",
    sortable: true,
  },
  {
    name: "jobTitleAndCompany",
    align: "center",
    label: "Job Title & Company",
    field: (row: Contact) => row,
    format: (row: Contact): string | null => {
      if (!row) return null;
      return row?.jobTitle && row?.company
        ? `${row.jobTitle} at ${row.company}`
        : row?.jobTitle && !row?.company
        ? `${row.jobTitle}`
        : !row?.jobTitle && row?.company
        ? `${row.company}`
        : "";
    },
    sortable: false,
  },
];
```

As you may have noticed, all columns but `jobTitleAndCompany` have their `field` property set to the name of the column. This means that for each object (row) in the mock data (`ui/src/data/Google_Contacts_Clone_Mock_Data.ts`), the value of each column will be mapped directly to the corresponding property in the mock data without further manipulations. Since, there is no further manipulations, the `format` property is not necessary for such columns.

However, for the `jobTitleAndCompany` column, there is need for further manipulation of the data because we want to merge the `company` and `jobTitle` properties of the `Contact` interface and get a composite column. `jobTitleAndCompany` is not a natural property of the `Contact` interface. To this effect, we introduce a function as the value of the `field` property. The function will take the current `Contact` row as its argument and return the same row. We are doing this to override the default behaviour of the `field` property which returns the `string` value defined in the data. The value returned by `field` (the entire `Contact` row) becomes the argument of the `format` property. So, we define a function which takes the `Contact` row and after some manipulations of the `company` and `jobTitle` properties of `Contact` will return either a `string` or `null`. The value returned by the `format` property is the final string which will be rendered for that column and row.

Next, we will make a brief detour to the `ui/src/layouts/MainLayout.vue` file and remove the `class` attribute attached to the `q-layout` component as shown:

```diff
<template>
-  <q-layout view="hHh Lpr lff" class="bg-grey-1">
+  <q-layout view="hHh Lpr lff">
...
   </q-layout>
</template>
```

This removes the light grey background from the entire layout and gives the app a plain white background.

### Creating the `Contacts` table

Now, we will create the actual `Contacts table` using the `QTable` component from the Quasar framework.

Open `ui/src/pages/Index.vue` file. If you recall, this is the component file responsible for rendering the `home` page. Check the `ui/src/router/routes.ts` file to re-confirm this. I will strong suggest that you type out all the entire changes in the `ui/src/pages/Index.vue` file. Make reference to this [snaphost for the Index.vue](https://github.com/ndianabasi/google-contacts/blob/05-the-contacts-table/ui/src/pages/Index.vue) file.

Beginning from the `template` section. Completely replace all the contents wrapped with `<template>...</template>`. Your template section should look like this afterwards:

```html
<template>
  <q-page class="row justify-center justify-evenly">
    <div class="q-px-md full-width">
      <q-table
        v-model:selected="selected"
        :rows="rows"
        :columns="columns"
        :loading="loading"
        row-key="id"
        virtual-scroll
        :virtual-scroll-item-size="48"
        :virtual-scroll-sticky-size-start="48"
        :pagination="pagination"
        :rows-per-page-options="[0]"
        binary-state-sort
        @virtual-scroll="onScroll"
        selection="multiple"
        flat
        class="sticky-table-header"
      >
        <template v-slot:top-row>
          <q-tr>
            <q-td colspan="100%"> Starred Contacts (xx) </q-td>
          </q-tr>
        </template>

        <template v-slot:body="props">
          <q-tr :props="props">
            <q-td auto-width>
              <q-checkbox v-model="props.selected" />
            </q-td>
            <q-td v-for="col in props.cols" :key="col.name" :props="props">
              <q-avatar v-if="col.name === 'profilePicture'">
                <img src="https://cdn.quasar.dev/img/avatar.png" />
              </q-avatar>
              <span v-else>
                {{ col.value }}
              </span>
            </q-td>
          </q-tr>
        </template>
      </q-table>
    </div>
  </q-page>
</template>
```

These are changes introduced:

1. For the `q-page` component, the class "items-center" was changed to "justify-center". The former centers both vertically and horizontally (i.e. places everything at the dead-center of the page) while the latter only centers horizontally which is how a page is normally centered.

```diff
- <q-page class="row items-center justify-evenly">
+ <q-page class="row justify-center justify-evenly">
```

2. We introduced a `div` with a class of `full-width` so that the table can stretched across the entire width of the `q-page`. The class `q-px-md` keeps a medium padding on the both sides of the page.

3. Then, we introduced the `q-table` component. The `q-table` component has a `v-model` with an argument. This is specifically used for two-way binding of the `selected` prop with the `selected` ref at Line 62 (i.e. `v-model:selected="selected"`). This means that the `q-table` component as an internal prop named `selected`. That internal prop can be modified by syncing the value of the `selected` array ref defined on Line 62. This is parent-to-child update. Our `Index.vue` component is the parent while the `QTable` component is the child. You can see this hierarchy when you spec the component with the Vue devtool. On the other hand, the `QTable` component can update its internal `selected` prop and emit an `update:select` event with the new value of the `selected` prop as the event payload. The parent component (our `Index.vue`) will pickup the event and update the `selected` property at Line 88 with the payload from the event. This two-way sync is what `v-model:selected="selected" stands for. Read more about  `[v-model](https://v3.vuejs.org/guide/component-basics.html#using-v-model-on-components)`and`[v-model with arguments here](https://v3.vuejs.org/guide/component-custom-events.html#v-model-arguments)\`.

4. Additionally, `q-table` takes the following props (for now, we could add more later). Read more about these props [here](https://quasar.dev/vue-components/table#qtable-api).
   1. The `column` prop accepts an array of objects which defines the properties of each column to be rendered in the table. We assign the `column` array import from `ui/src/data/table-definitions/contacts.ts`. We discussed in the section, `Create the Column Definition File`.
   2. The `row` prop accepts an array of objects which defines all our contacts. We discussed this in the section, `Create The Mock Data File`.

5. The `loading` prop accepts a `boolean` value which indicates whether the table is in loading state or not. The `ref` on Line 61 is assigned to it.

6. The `row-key` prop defines which property in our data object will be used as the unique key for each row of data. If you open `ui/src/data/Google_Contacts_Clone_Mock_Data.ts`, each row as a unique `id` property. The `row-key` must be assigned to a property which is unique across the entire dataset.

7. The `virtual-scroll` prop is a `boolean` prop which activates the use of virtual (infinite) scrolling within our table.

8. The `virtual-scroll-item-size` prop accept a value which indicates the minimum pixel height of a row to be rendered. If you inspect the height of each row of the table, you should set the value of this prop close to that pixel height.

9. The `virtual-scroll-sticky-size-start` prop accept a vaule which indicates the height of the sticky header, if present.

10. The `pagination` prop accepts an object which defines the settings for the table pagination. Note that, even though we are using virtual scrolling, there is pagination behind the screen as will be explain later.

11. The `rows-per-page-options` prop defines the value of the dropdown which will be used for selecting the number of rows per page. A single value of "0" in the array indicates the option for displaying all rows at once. This is important for virtual scrolling.

12. The `binary-state-sort` prop is a `boolean` prop which indicates that we want to use only two states for sorting. That is, a column can either be sorted `descending` or `ascending`. Nothing in between.

13. The `selection` prop takes three options: `single`, `multiple`, or `none`. It is used to enable single or multiple selections or disable selection completely.

14. That `flat` prop is a boolean prop which indicates that the table should rendered with the `flat` design (no shadows).

15. A single class: `sticky-table-header` is attached to the `q-table` component. The class enables the sticky header for the table. See the `style` section at the end of the file.

16. The `q-table` component currently has one event listener: `@virtual-scroll`. The `virtual-scroll` event is fired when extra rows are needed as scroll down the table. This event listener calls the `onScroll` function which loads additional rows into our `rows` prop when extra rows are needed. The mechanism of the `onScroll` will be discussed later.

This lesson is already too long. In the next lesson, we will continue the discussion from the `script` section.


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