How to paginate with DTOs?

@aaron-ford
@aaron-ford Adocasts Plus

I am trying to implement pagination in my user index page, but I think it is breaking because the pagination logic adds meta data into the user data, so the UserDTO.fromArray(users) ends up returning an empty array. I was reading online that one way to handle it is make a fromPaginator function in the User DTO class. Is that the best way to handle it? What is the recommended way we paginate when using DTOs?
Here's the logic in my user controller that is coming back empty:

async index({ request, inertia, auth }: HttpContext) {
    const page = request.input('page', 1)
    const users = await User.query().orderBy('username', 'asc').paginate(page, 10)

    return inertia.render('admin/users/index', {
      users: UserDto.fromArray(users),
    })
  }
Copied!
Create a free account to join in on the discussion
  1. @tomgobich

    Hey Aaron!

    Yeah, adding a fromPaginator would be one way to do it! If you're using the @adocasts.com/dto package, that method will be baked into the DTO already. If you're not using this package, but taking a similar approach, you're welcome to grab the SimplePaginatorDto from there and add it into your DTO or BaseDto.

    const page = request.input('page', 1)
    const paginator = await User.query().orderBy('username', 'asc').paginate(page, 10)
    
    paginator.baseUrl(router.makeUrl('users.index'))
    
    const users = UserDto.fromPaginator(paginator, { 
      start: paginator.firstPage,
      end: paginator.lastPage,
    })
    Copied!
    • app
    • controllers
    • users_controller.ts
    <script setup lang="ts">
    import type { SimplePaginatorDtoContract } from '@adcasts.com/dto/types'
    import type { UserDto } from '#dtos/user'
    
    const props = defineProps({
      users: SimplePaginatorDtoContract<UserDto>
    })
    
    const arrayOfUsers = props.users.data
    const paginationInfo = props.users.meta
    </script>
    Copied!

    By adding the start and end it'll populate a users.meta.pagesInRange array similar to the getUrlsForRange would.

    Alternatively, you can split them and just use the SimplePaginatorMetaKeys type, it should match the serialize meta. That'd look something like the below. However, with this, you'd need to build the paginated URLs on the client-side or pass it in separately.

    import { SimplePaginatorMetaKeys } from '@adonisjs/lucid/types/querybuilder'
    
    // ...
    
    const page = request.input('page', 1)
    const paginator = await User.query().orderBy('username', 'asc').paginate(page, 10)
    
    paginator.baseUrl(router.makeUrl('users.index'))
    
    return inertia.render('admin/users/index', {
      users: UserDto.fromArray(paginator.all()),
      meta: paginator.getMeta() as SimplePaginatorMetaKeys,
      pagesInRange: paginator.getUrlsForRange(paginator.firstPage, paginator.lastPage)
    })
    Copied!
    • app
    • controllers
    • users_controller.ts

    If needed in multiple places you could also wrap that into a DTO of its own using generics, that would look something relatively similar to the SimplePaginatorDto, though.

    Next, you might have a DTO that looks something like the below, where you're just simply returning objects. For that, you can just tack a fromPaginator method on there that results in a similar, typed, structure to the paginator. Something similar to the below seems to be the general approach that is recommended by others. I work a lot with C#, which is why my approach to DTOs is a little more… descriptive lol.

    import { SimplePaginatorMetaKeys } from '@adonisjs/lucid/types/querybuilder'
    
    export default class UserDto {
      static fromModel(user: User) {
        return {
          id: user.id,
          username: user.username,
          // ...
        }
      }
    
      static fromArray(users: User[]) {
        return users.map((user) => this.fromModel(user))
      }
    
      static fromPaginator(paginator: ModelPaginatorContract<User>) {
        return {
          data: this.fromArray(paginator.all()),
          meta: paginator.getMeta() as SimplePaginatorMetaKeys
        }
      }
    }
    Copied!

    Lastly, as a heads up, AdonisJS 7 will be introducing HTTP Transformers as a serialization handler to improve end-to-end type safety. Doesn't solve anything for you at the moment, but just wanted to share so you're aware of it in the event you think you might want to transition from DTOs to those in the future.

    If you'd like more help, feel free to share your UserDto and we can try to find a good solution!

    0
    1. Responding to tomgobich
      @aaron-ford

      Thanks! I am using your dto package, and had just stumbled upon the knowledge you had built the fromPaginator function into it, but this helped me get it working. I have the initial results showing up, and the links on the bottom (though I have to get them working now). It looks like Shadcn has a pagination component, so I think I'll try setting that up.

      When is AdonisJS 7 coming out?

      Thanks for your help!

      1
      1. Responding to aaron-ford
        @tomgobich

        Awesome, glad to hear you were able to get it working!

        Yes, Shadcn's pagination component works pretty great! You can find a complete example (in Vue) in the production PlotMyCourse repo in case it helps. Their component just needs to know the total, current page, and rows per page and it'll give you the array of pages to display via slot data.

        <template>
          <Pagination
            v-model:page="lessons.meta.currentPage"
            v-slot="{ page }"
            :total="lessons.meta.total"
            :sibling-count="1"
            :items-per-page="lessons.meta.perPage"
            show-edges
          >
            <PaginationList v-slot="{ items }" class="flex items-center gap-1">
              <PaginationFirst :href="lessons.meta.firstPageUrl" />
              <PaginationPrev :href="lessons.meta.previousPageUrl" />
        
              <template v-for="(item, index) in items">
                <PaginationListItem v-if="item.type === 'page'" :key="index" :value="item.value" as-child>
                 <Button ...> ... </Button>
                </PaginationListItem>
              </template>
            </PaginationList>
          </Pagination>
        </template>
        Copied!

        I don't think there is an anticipated or goal date for AdonisJS 7 yet, at least not publicly, but there's been a bunch of movement lately with things getting tagged to next versions for it.

        Anytime Aaron!!

        0
        1. Responding to tomgobich
          @aaron-ford

          Are there more lessons for PlotMyCourse other than the building with AdonisJS 6 and Inertia? Or are these changes you made outside the course for your personal production repo? Looks like there are a lot of interesting and useful things I can reference for my own project. The numbered links work for me, but using the arrows to try and move from page to page or jump to the start or end don't work. I'm using the fromPaginator as shown above:

          const props = defineProps<{
              users: SimplePaginatorDtoContract<UserDto>
          }>()
          
          const users = ref(props.users.data)
          const paginationInfo = props.users.meta
          Copied!

          And I copied the pagination from the production PlotMyCourse repo, replacing the lessons.meta with paginationInfo. The UI updates and highlights the right numbers, but there must not be a link being generated to actually change them. Hovering over them shows no link, but hovering over the numbers does. The controller is adding the start and end as directed:

          async index({ request, inertia, auth }: HttpContext) {
              const page = request.input('page', 1)
              const paginator = await User.query().orderBy('username', 'asc').paginate(page, 10)
          
              paginator.baseUrl(router.makeUrl('users.index'))
              //paginator.queryString(request.qs())
          
              return inertia.render('admin/users/index', {
                users: UserDto.fromPaginator(paginator, {
                  start: paginator.firstPage,
                  end: paginator.lastPage,
                }),
              })
            }
          Copied!

          I've been going through my code and comparing it to yours. Other than the fact you use an action to get your pagination based off supplied filters, it seems similar from what I can tell. Any idea why the links wouldn't generate?

          1
          1. Responding to aaron-ford
            @tomgobich

            Yeah, these were changes made for production after the Building with AdonisJS & Inertia series was planned. If you think these pagination/filter additions (or any others) would be helpful to have as lessons, I can take a look at making a series covering it!

            Apologies, looking into what I have in that repo further, it looks like I mutated the first, next, last, prev components to:

            1. Accept an href prop

            2. Use an Inertia link to navigate to the provided href

            <script setup lang="ts">
            import { type HTMLAttributes, computed } from 'vue'
            import { PaginationFirst, type PaginationFirstProps } from 'radix-vue'
            import { ChevronsLeft } from 'lucide-vue-next'
            import { Button } from '~/components/ui/button'
            import { cn } from '~/lib/utils'
            
            const props = withDefaults(
              defineProps<PaginationFirstProps & { 
                class?: HTMLAttributes['class']; 
                href: string 
              }>(),
              {
                asChild: true,
              }
            )
            
            // ...
            </script>
            
            <template>
              <PaginationFirst v-bind="delegatedProps">
                <Button :class="cn('w-8 h-8 p-0', props.class)" variant="outline">
                  <Link :href="href">
                    <ChevronsLeft class="h-4 w-4" />
                  </Link>
                </Button>
              </PaginationFirst>
            </template>
            Copied!

            You should be able to find these at ~/inertia/components/ui/pagination. Alternatively, if you don't want to change the source, you can add as-child onto the component and add the link & icon as a child to the component.

            0
New Discussion
Topic