I was wondering how to make an infinite scroll feature that already present on Inertia.js v2.0 and Laravel support already work out of the box. But how is the support on AdonisJS? The official AdonisJS’s Inertia adapter hasn’t supported this feature yet but according to Virk (Core member of AdonisJS team) the scroll() method will be out in the next version of the adapter. So how to implement it in the current version of AdonisJS? Let’s get started.

Initialize Project

  1. Initialize the repo

    $ npm init adonisjs@latest infinite-scroll-inertia -- --kit=inertia --db=sqlite
    

    I just want to start easy on this example, you can modify the database and the authentication mechanism.

  2. Choose the authentication guard. I chose skip in this step.

  3. Choose the frontend library. I chose Vue 3.

  4. SSR? I chose false. Set it to true if you want by pressing y.

  5. Wait the setup.

Backend Part

Let’s start by creating the User model.

$ node ace make:model user -mfc # migration, factory and controller

So now we have the User model and its factory. Let’s attach some props to User.

// models/user.ts
import { DateTime } from 'luxon'
import { BaseModel, column } from '@adonisjs/lucid/orm'

export default class User extends BaseModel {
  @column({ isPrimary: true })
  declare id: number

  @column()
  declare name: string

  @column()
  declare email: string

  @column.dateTime({ autoCreate: true })
  declare createdAt: DateTime

  @column.dateTime({ autoCreate: true, autoUpdate: true })
  declare updatedAt: DateTime
}

Modify the migration file:

// <timestamp>_create_users_table.ts
import { BaseSchema } from '@adonisjs/lucid/schema'

export default class extends BaseSchema {
  protected tableName = 'users'

  async up() {
    this.schema.createTable(this.tableName, (table) => {
      table.increments('id')
      table.string('name')
      table.string('email')
      table.timestamp('created_at')
      table.timestamp('updated_at')
    })
  }

  async down() {
    this.schema.dropTable(this.tableName)
  }
}

Modify the factory:

// factories/user_factory.ts
import factory from '@adonisjs/lucid/factories'
import User from '#models/user'

export const UserFactory = factory
  .define(User, async ({ faker }) => {
    return {
      name: faker.person.firstName(),
      email: faker.internet.email()
    }

  })
  .build()

Create the seeder:

$ node ace make:seeder main

Use the factory on seeder:

// seeders/main_seeder.ts
import { BaseSeeder } from '@adonisjs/lucid/seeders'

import { UserFactory } from '#database/factories/user_factory'

export default class extends BaseSeeder {
  async run() {
    await UserFactory.createMany(50);
  }
}

Edit the UserController:

// controllers/user_controller.ts
import type { HttpContext } from '@adonisjs/core/http'

import User from "#models/user";

export default class UsersController {

  async index({ inertia, request }: HttpContext) {
    const page = request.input('page', 1)
    const perPage = request.input('perPage', 10)
    console.log('page', page)

    const users = await User.query().paginate(page, perPage);

    return inertia.render('users/index', {
      users: inertia.merge(() => users.toJSON().data),
      usersMeta: users.getMeta()
    })
  }
}

Add the route on routes.ts:

// start/routes.ts

/*
|--------------------------------------------------------------------------
| Routes file
|--------------------------------------------------------------------------
|
| The routes file is used for defining the HTTP routes.
|
*/

import router from '@adonisjs/core/services/router'

const UsersController = () => import('#controllers/users_controller')

router.on('/').renderInertia('home')

router.get('users', [UsersController, 'index'])

Do the migration:

$ node ace migration:fresh --seed

Now let’s head to the frontend.

Frontend Part

Let’s add the loading icon:

$ npm i lucide-vue-next

And add a simple helper called SimplePaginatorDtoMetaContract from @adocasts.com/dto/types:

$ npm i @adocasts.com/dto

Create users/index.vue on inertia/pages/ dir.

// pages/users/index.vue
<script setup lang="ts">
import { WhenVisible } from '@inertiajs/vue3'
import { ref, computed, watchEffect } from 'vue'
import { SimplePaginatorDtoMetaContract } from '@adocasts.com/dto/types'
import { Loader2 } from 'lucide-vue-next'

import type User from '#models/user'

const props = defineProps<{
  users: User[]
  usersMeta: SimplePaginatorDtoMetaContract
}>()

const users = ref(props.users)

const hasMorePages = computed(() => {
  const { currentPage, lastPage } = props.usersMeta
  return currentPage < lastPage
})

watchEffect(() => (users.value = props.users))

const whenVisibleParams = computed(() => ({
  only: ['users', 'usersMeta'],
  preserveUrl: true,
  data: {
    page: props.usersMeta.currentPage + 1,
  },
}))
</script>

<template>
  <div class="w-full max-w-screen-md mx-auto border rounded-xl p-4">
    <h1 class="text-2xl font-bold mb-4">Users</h1>

    <table class="w-full border-collapse">
      <thead>
        <tr class="border-b">
          <th class="text-left p-2">ID</th>
          <th class="text-left p-2">Name</th>
          <th class="text-left p-2">Email</th>
        </tr>
      </thead>

      <tbody v-if="users?.length">
        <tr v-for="user in users" :key="user.id" class="border-b hover:bg-gray-50">
          <td class="p-2">{{ user.id }}</td>
          <td class="p-2">{{ user.name }}</td>
          <td class="p-2">{{ user.email }}</td>
        </tr>

        <WhenVisible :params="whenVisibleParams" :always="hasMorePages">
          <template #fallback>
            <tr>
              <td colspan="3" class="text-center p-4 flex justify-center items-center gap-2">
                <Loader2 class="w-4 h-4 animate-spin text-muted-foreground" />
                <span>Loading more...</span>
              </td>
            </tr>
          </template>
        </WhenVisible>
      </tbody>

      <tbody v-else>
        <tr>
          <td colspan="3" class="text-center p-4">No users found.</td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

Since I’m not an expert with Vue, this implementation needs a slight improvement. The infinite scroll depends on the zoom level on the browser. Feel free to contact me to fix it or just rewrite this blog.

So that’s how we implement the InertiaJS infinite scroll with AdonisJS. Hope this tut helped you.