Skip to content

Add 'page' field to paginator links #56603

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 11, 2025

Conversation

compico
Copy link
Contributor

@compico compico commented Aug 10, 2025

Problem description

When building a Vue.js (or any SPA) client consuming a Laravel API, the LengthAwarePaginator::linkCollection() method returns an array of links containing:

[
    'url' => string|null,
    'label' => string,
    'active' => bool,
]

In many frontend use-cases, we don’t actually need the url — we only care about the page number so we can make an API call like:

fetch(`/api/entity?page=${page}`)

Currently, the page number has to be extracted from the url on the client side, which:

  • Adds unnecessary parsing logic,
  • Makes the code less readable,
  • Can break if the URL format changes,
  • Is inconvenient when API calls are wrapped in multiple layers of abstraction (TypeScript clients, base URLs, interceptors, etc.).

Proposed solution

Include an additional page key in each link returned by LengthAwarePaginator::linkCollection(), e.g.:

[
    'url' => null,
    'label' => '« Previous',
    'page' => null,
    'active' => false,
],
[
    'url' => 'https://example.com?page=1',
    'label' => '1',
    'page' => 1,
    'active' => true,
],
[
    'url' => 'https://example.com?page=2',
    'label' => '2',
    'page' => 2,
    'active' => false,
],
[
    'url' => 'https://example.com?page=2',
    'label' => 'Next »',
    'page' => 2,
    'active' => false,
],

Where:

  • page is an integer for navigable links,
  • null for disabled links (e.g., ..., previous when on the first page, next when on the last page).

This provides the frontend with a ready-to-use page number, avoids URL parsing, and doesn’t break backward compatibility — all existing keys remain unchanged.

@compico
Copy link
Contributor Author

compico commented Aug 10, 2025

I checked the failed tests. The ones related to my PR are already fixed. As far as I understand, the other failed tests are not related to my PR.

@rodrigopedra
Copy link
Contributor

  • Why do you need to parse the URL?
  • Shouldn't the URL already be the one you need?

@compico
Copy link
Contributor Author

compico commented Aug 10, 2025

  • Why do you need to parse the URL?

    • Shouldn't the URL already be the one you need?

Technically, yes — the URL coming from the backend works fine and is exactly what we need.
I’ll show how pagination works in Vue.js.

paginator vue component

<template>
    <nav class="pagination">
        <ul>
            <li v-for="link in parsedLinks" :key="link.key">
                <button
                    v-if="link.url"
                    :class="{ active: link.active }"
                    @click="$emit('change', link.page)"
                >
                    {{ link.label }}
                </button>
                <span v-else>{{ link.label }}</span>
            </li>
        </ul>
    </nav>
</template>

<script setup>
    import { computed } from 'vue'

    const props = defineProps({
        links: {
            type: Array,
            required: true
        }
    })

    const emit = defineEmits(['change'])

    const parsedLinks = computed(() => {
        return props.links.map((l, idx) => {
            let label = l.label;
            if (label.includes('&laquo;')) label = '«';
            if (label.includes('&raquo;')) label = '»';
            return {
                key: idx,
                label,
                url: l.url,
                active: l.active,
                page: l.url ? parseInt(new URL(l.url).searchParams.get('page')) || 1 : null // parsing url
            };
        });
    })
</script>

user api service

import http from './httpClient' // has base_url and headers to api service

export function getUsers(page = 1) {
  return http.get('/users', { params: { page } });
}

user list with pagination

<template>
  <div>
    <h1>Users</h1>
    <ul>
      <li v-for="user in users" :key="user.id">
        {{ user.name }}
      </li>
    </ul>
    <Pagination
      v-if="pagination.links"
      :links="pagination.links"
      @change="fetchUsers"
    />
  </div>
</template>

<script setup>
import { onMounted, ref } from 'vue';
import Pagination from '@/components/Pagination.vue';
import { getUsers } from '@/api/users';

const users = ref([]);
const pagination = ref({});

async function fetchUsers(page = 1) {
  const { data } = await getUsers(page);
  users.value = data.data;
  pagination.value = data.meta || {};
  pagination.value.links = data.meta?.links || [];
}

onMounted(() => {
  fetchUsers();
});
</script>

Overall, my change doesn’t really change the game much, but it does make working with pagination a bit easier.

@compico
Copy link
Contributor Author

compico commented Aug 10, 2025

And it’s also much more convenient when working with vue-router

@rodrigopedra
Copy link
Contributor

Ah ok, didn't think about this use case.

If using Inertia, you could emit the URL -- instead of the page number --from the pagination component, as the routing is server based.

But for SPAs or mobile apps, I can see the benefit of having the page number already in the response.

@taylorotwell taylorotwell merged commit 59c0cec into laravel:12.x Aug 11, 2025
24 of 60 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants