protonemedia / inertiajs-tables-laravel-query-builder

Inertia.js Tables for Laravel Query Builder
https://protone.media/en/blog/introducing-inertiajs-tables-a-datatables-like-package-for-laravel-query-builder
MIT License
435 stars 123 forks source link

Pagination: changing page submits two requests #119

Open mbeckerle-xqueue opened 1 year ago

mbeckerle-xqueue commented 1 year ago

I was about to investigate why tables tend to flicker on page change and set up a very simple table.

My controller:

public function test(Request $request) {
    $elements = QueryBuilder::for(Something::class)
        ->defaultSort('name')
        ->paginate(perPage: request('perPage')??25);

    return Inertia::render('Something/Test', [
        'elements' => $elements,
    ])->table(function (InertiaTable $table) {
        $table
            ->perPageOptions([25, 50, 100, 200])
            ->defaultSort('name')
            ->column(key: 'name', label: 'Name');
    });
}

My vue with table:

<script setup>
    import AppLayout from '@/Test/Layouts/AppLayout.vue';

    import { Table } from "@protonemedia/inertiajs-tables-laravel-query-builder";

    const props = defineProps({
        elements: Object,
    });  
</script>

<template>

    <AppLayout>
        <Table :resource="elements" />
    </AppLayout>

</template>

By default it shows 25. When I select 50 as perPage, it shows me 50. When I now go to page 2, it triggers two calls, which retrieves data from DB, twice. Once with correct perPage parameter and once without.

image

  1. /test?page=2
  2. /test?page=2&perPage=50&sort=name

After digging a bit in code I found the problem in Table.vue providing function "function visit(url)" to Pagination.vue as onClick handler:

<slot
        v-if="resourceMeta.total > queryBuilderProps.perPageOptions[0]"
        name="**visit**"
        :on-click="onSetPageChange"
        :has-data="hasData"
        :meta="resourceMeta"
        :per-page-options="queryBuilderProps.perPageOptions"
        :on-per-page-change="onPerPageChange"
      >
        <Pagination
          :on-click="**visit**"
          :has-data="hasData"
          :meta="resourceMeta"
          :per-page-options="queryBuilderProps.perPageOptions"
          :on-per-page-change="onPerPageChange"
        />
</slot>

As soon as one of the numbers gets clicked, it calls visit, which calls the passed URL. the URL however is a static set of URLs, dot being modified when perPage or any filter gets updated. Instead urls of the following kind are called: https://myserver/test?page=2

In "visit" method, the mnodel is updated: queryBuilderData.value.page = queryBuilderProps.value.page and this triggers

watch(queryBuilderData, () => {
    visit(location.pathname + "?" +  generateNewQueryString())
}, {deep: true})

which finally calls the second visit with the correct parameters, refreshing the page with correct values.

I am not too deep into this project though, so I open it as an issue and maybe someone finds a better solution. For me to avoid those problems, I created a "CustomizedTable.vue" and added a new method, which is called on page change and which only updates the model, thus calling visit implicitely, once, instead of calling visit also explicitely. function onSetPageChange(url) {

let pageName= $inertia.page.props.queryBuilderProps[props.name].pageName??'page';

    let tmp = url.split('?');
    if (tmp.length == 2) {
        tmp = tmp[1];
    } else {
        tmp = "";
        console.log("Expected URL to contain a single ?");
    }

    // Using Proxy class is fastest solution to date
    // See https://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript
    const params = new Proxy(new URLSearchParams(tmp), {
        get: (searchParams, prop) => searchParams.get(prop),
    });

    let value = params[pageName];

    queryBuilderData.value.cursor = null;
    queryBuilderData.value.page = value;
}

I guess there are better ways to do this, I did not want to abuse the label, so maybe the whole data model should be extended. Then I passed this method to Pagination like:

<Pagination
          :on-click="onSetPageChange"

In the end I still ended up with a second call (this time with identical parameters) as visit() changes the parameters which triggers the watch. As the parameters were equal, I just catched it with s stupid if statement in the onSuccess method of visit():

if (queryBuilderData.value.cursor != queryBuilderProps.value.cursor) queryBuilderData.value.cursor = queryBuilderProps.value.cursor
if (queryBuilderData.value.page != queryBuilderProps.value.page) queryBuilderData.value.page = queryBuilderProps.value.page

This is not very clean code but did the job now and as I spend a LOT of hours today on debugging why my tables behave strangely, I will probably use this workaround unil there is a nicely done fix.

patrocle commented 1 year ago

Hello,

Same thing here, default sort is append or params are reordered alphabetically.

Capture d’écran 2023-02-15 à 14 11 35 Capture d’écran 2023-02-15 à 14 17 17
patrocle commented 1 year ago

Hi, I made a PR #126