@toniel/laravel-tanstack-pagination 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -94,13 +94,15 @@ Main composable for handling Laravel pagination.
94
94
  | `search` | `Ref<string>` | Current search query |
95
95
  | `sortBy` | `Ref<string \| null>` | Current sort column |
96
96
  | `sortDirection` | `Ref<'asc' \| 'desc'>` | Sort direction |
97
- | `filters` | `ComputedRef<PaginationFilters>` | All active filters |
97
+ | `filters` | `ComputedRef<PaginationFilters>` | All active filters (including custom filters) |
98
+ | `customFilters` | `Ref<Record<string, any>>` | Custom filter state (for external use) |
98
99
  | `isLoading` | `Ref<boolean>` | Loading state from TanStack Query |
99
100
  | `error` | `Ref<Error \| null>` | Error state from TanStack Query |
100
101
  | `handlePageChange` | `(page: number) => void` | Change page handler |
101
102
  | `handlePerPageChange` | `(perPage: number) => void` | Change per page handler |
102
103
  | `handleSearchChange` | `(search: string) => void` | Search change handler (debounced) |
103
104
  | `handleSortChange` | `(column: string) => void` | Toggle sort on column |
105
+ | `handleFilterChange` | `(filters: Record<string, any>) => void` | Bulk update custom filters |
104
106
  | `setFilter` | `(key: string, value: any) => void` | Add custom filter |
105
107
  | `removeFilter` | `(key: string) => void` | Remove custom filter |
106
108
  | `resetFilters` | `() => void` | Reset all filters to defaults |
@@ -257,6 +259,158 @@ npm install @toniel/laravel-tanstack-datatable
257
259
 
258
260
  See: https://www.npmjs.com/package/@toniel/laravel-tanstack-datatable
259
261
 
262
+ ## 🔍 Filtering
263
+
264
+ The `usePagination` composable provides comprehensive filter management with proper state persistence across page changes.
265
+
266
+ ### Single Filter Operations
267
+
268
+ ```typescript
269
+ const {
270
+ setFilter,
271
+ removeFilter,
272
+ customFilters,
273
+ refetch,
274
+ } = usePagination(fetchFn, { queryKey: 'users' })
275
+
276
+ // Set a single filter
277
+ setFilter('status', 'active')
278
+ setFilter('category', 'A')
279
+
280
+ // Remove a filter
281
+ removeFilter('status')
282
+
283
+ // The query will automatically refetch when filters change
284
+ ```
285
+
286
+ ### Bulk Filter Operations
287
+
288
+ ```typescript
289
+ const {
290
+ handleFilterChange,
291
+ customFilters,
292
+ refetch,
293
+ } = usePagination(fetchFn, { queryKey: 'users' })
294
+
295
+ // Bulk update multiple filters at once
296
+ handleFilterChange({
297
+ status: 'active',
298
+ category: 'A',
299
+ year: '2024'
300
+ })
301
+
302
+ // Or pass partial updates (will merge with existing)
303
+ handleFilterChange({
304
+ ...customFilters.value,
305
+ status: 'inactive'
306
+ })
307
+ ```
308
+
309
+ ### Filter State Persistence
310
+
311
+ Custom filters are properly included in the TanStack Query cache key, ensuring:
312
+ - Filters persist when changing pages
313
+ - Filters persist when changing per page
314
+ - Correct cache invalidation on filter changes
315
+ - Back/forward navigation works correctly
316
+
317
+ ```typescript
318
+ // Filters are automatically included in queryKey
319
+ const { filters } = usePagination(fetchFn, { queryKey: 'users' })
320
+
321
+ // filters.value = { page: 1, per_page: 10, search: '', status: 'active', category: 'A' }
322
+ ```
323
+
324
+ ### Reset Filters
325
+
326
+ ```typescript
327
+ const {
328
+ resetFilters,
329
+ customFilters,
330
+ search,
331
+ perPage,
332
+ } = usePagination(fetchFn, { queryKey: 'users', defaultPerPage: 20 })
333
+
334
+ // Reset all filters to defaults
335
+ resetFilters()
336
+
337
+ // After reset:
338
+ // - customFilters = {}
339
+ // - search = ''
340
+ // - perPage = 20
341
+ // - page = 1
342
+ ```
343
+
344
+ ### Example: Complex Filter Form
345
+
346
+ ```typescript
347
+ <script setup lang="ts">
348
+ import { ref, computed } from 'vue'
349
+ import { usePagination } from '@toniel/laravel-tanstack-pagination'
350
+
351
+ // Filter form state (separate from composable)
352
+ const filterForm = ref({
353
+ status: '',
354
+ category: '',
355
+ year: new Date().getFullYear().toString(),
356
+ month: '',
357
+ })
358
+
359
+ const {
360
+ tableData,
361
+ pagination,
362
+ customFilters,
363
+ setFilter,
364
+ removeFilter,
365
+ handlePageChange,
366
+ refetch,
367
+ } = usePagination(
368
+ (filters) => axios.get('/api/transactions', { params: filters }),
369
+ { queryKey: 'transactions', defaultPerPage: 20 }
370
+ )
371
+
372
+ // Apply filters
373
+ const applyFilters = () => {
374
+ // Clear existing custom filters first
375
+ if (filterForm.value.status) {
376
+ setFilter('status', filterForm.value.status)
377
+ } else {
378
+ removeFilter('status')
379
+ }
380
+
381
+ if (filterForm.value.category) {
382
+ setFilter('category', filterForm.value.category)
383
+ } else {
384
+ removeFilter('category')
385
+ }
386
+
387
+ if (filterForm.value.year) {
388
+ setFilter('year', filterForm.value.year)
389
+ } else {
390
+ removeFilter('year')
391
+ }
392
+
393
+ if (filterForm.value.month) {
394
+ setFilter('month', filterForm.value.month)
395
+ } else {
396
+ removeFilter('month')
397
+ }
398
+
399
+ refetch()
400
+ }
401
+
402
+ // Clear all filters
403
+ const clearFilters = () => {
404
+ filterForm.value = { status: '', category: '', year: new Date().getFullYear().toString(), month: '' }
405
+ removeFilter('status')
406
+ removeFilter('category')
407
+ removeFilter('year')
408
+ removeFilter('month')
409
+ refetch()
410
+ }
411
+ </script>
412
+ ```
413
+
260
414
  ## 🔧 Laravel Backend Setup
261
415
 
262
416
  Your Laravel API should return paginated responses in this format:
package/dist/index.d.mts CHANGED
@@ -5,12 +5,12 @@ import { Ref, ComputedRef } from 'vue';
5
5
 
6
6
  type LaravelPaginationResponse<T = any> = {
7
7
  data: T[];
8
- links: Array<{
8
+ links: {
9
9
  first: string | null;
10
10
  last: string | null;
11
11
  next: string | null;
12
12
  prev: string | null;
13
- }>;
13
+ };
14
14
  meta: {
15
15
  current_page: number;
16
16
  from: number;
@@ -44,6 +44,7 @@ interface UsePaginationOptions<T> extends Omit<UseQueryOptions<LaravelPagination
44
44
  defaultPerPage?: number;
45
45
  defaultSearch?: string;
46
46
  defaultSort?: SortState;
47
+ debounceMs?: number;
47
48
  }
48
49
  declare function usePagination<T = any>(fetchFn: (filters: PaginationFilters) => Promise<LaravelPaginationResponse<T>>, options: UsePaginationOptions<T>): {
49
50
  tableData: vue.ComputedRef<T[]>;
@@ -52,10 +53,11 @@ declare function usePagination<T = any>(fetchFn: (filters: PaginationFilters) =>
52
53
  handlePerPageChange: (newPerPage: number) => void;
53
54
  handleSearchChange: (newSearch: string) => void;
54
55
  handleSortChange: (column: string) => void;
56
+ handleFilterChange: (newFilters: Record<string, any>) => void;
55
57
  resetFilters: () => void;
56
58
  setFilter: (key: string, value: any) => void;
57
59
  removeFilter: (key: string) => void;
58
- getNumberingColumn: (options?: any) => {
60
+ getNumberingColumn: (columnOptions?: any) => {
59
61
  accessorKey: string;
60
62
  header: string;
61
63
  enableSorting: boolean;
@@ -97,10 +99,12 @@ declare function usePagination<T = any>(fetchFn: (filters: PaginationFilters) =>
97
99
  suspense: () => Promise<_tanstack_vue_query.QueryObserverResult<LaravelPaginationResponse<T>, Error>>;
98
100
  currentPage: vue.Ref<number, number>;
99
101
  perPage: vue.Ref<number, number>;
102
+ currentPerPage: vue.Ref<number, number>;
100
103
  search: vue.Ref<string, string>;
101
104
  sortBy: vue.Ref<string | null, string | null>;
102
105
  sortDirection: vue.Ref<"asc" | "desc", "asc" | "desc">;
103
106
  filters: vue.ComputedRef<PaginationFilters>;
107
+ customFilters: vue.Ref<Record<string, any>, Record<string, any>>;
104
108
  } | {
105
109
  tableData: vue.ComputedRef<T[]>;
106
110
  pagination: vue.ComputedRef<LaravelPaginationResponse<T> | null>;
@@ -108,10 +112,11 @@ declare function usePagination<T = any>(fetchFn: (filters: PaginationFilters) =>
108
112
  handlePerPageChange: (newPerPage: number) => void;
109
113
  handleSearchChange: (newSearch: string) => void;
110
114
  handleSortChange: (column: string) => void;
115
+ handleFilterChange: (newFilters: Record<string, any>) => void;
111
116
  resetFilters: () => void;
112
117
  setFilter: (key: string, value: any) => void;
113
118
  removeFilter: (key: string) => void;
114
- getNumberingColumn: (options?: any) => {
119
+ getNumberingColumn: (columnOptions?: any) => {
115
120
  accessorKey: string;
116
121
  header: string;
117
122
  enableSorting: boolean;
@@ -153,10 +158,12 @@ declare function usePagination<T = any>(fetchFn: (filters: PaginationFilters) =>
153
158
  suspense: () => Promise<_tanstack_vue_query.QueryObserverResult<LaravelPaginationResponse<T>, Error>>;
154
159
  currentPage: vue.Ref<number, number>;
155
160
  perPage: vue.Ref<number, number>;
161
+ currentPerPage: vue.Ref<number, number>;
156
162
  search: vue.Ref<string, string>;
157
163
  sortBy: vue.Ref<string | null, string | null>;
158
164
  sortDirection: vue.Ref<"asc" | "desc", "asc" | "desc">;
159
165
  filters: vue.ComputedRef<PaginationFilters>;
166
+ customFilters: vue.Ref<Record<string, any>, Record<string, any>>;
160
167
  } | {
161
168
  tableData: vue.ComputedRef<T[]>;
162
169
  pagination: vue.ComputedRef<LaravelPaginationResponse<T> | null>;
@@ -164,10 +171,11 @@ declare function usePagination<T = any>(fetchFn: (filters: PaginationFilters) =>
164
171
  handlePerPageChange: (newPerPage: number) => void;
165
172
  handleSearchChange: (newSearch: string) => void;
166
173
  handleSortChange: (column: string) => void;
174
+ handleFilterChange: (newFilters: Record<string, any>) => void;
167
175
  resetFilters: () => void;
168
176
  setFilter: (key: string, value: any) => void;
169
177
  removeFilter: (key: string) => void;
170
- getNumberingColumn: (options?: any) => {
178
+ getNumberingColumn: (columnOptions?: any) => {
171
179
  accessorKey: string;
172
180
  header: string;
173
181
  enableSorting: boolean;
@@ -209,10 +217,12 @@ declare function usePagination<T = any>(fetchFn: (filters: PaginationFilters) =>
209
217
  suspense: () => Promise<_tanstack_vue_query.QueryObserverResult<LaravelPaginationResponse<T>, Error>>;
210
218
  currentPage: vue.Ref<number, number>;
211
219
  perPage: vue.Ref<number, number>;
220
+ currentPerPage: vue.Ref<number, number>;
212
221
  search: vue.Ref<string, string>;
213
222
  sortBy: vue.Ref<string | null, string | null>;
214
223
  sortDirection: vue.Ref<"asc" | "desc", "asc" | "desc">;
215
224
  filters: vue.ComputedRef<PaginationFilters>;
225
+ customFilters: vue.Ref<Record<string, any>, Record<string, any>>;
216
226
  } | {
217
227
  tableData: vue.ComputedRef<T[]>;
218
228
  pagination: vue.ComputedRef<LaravelPaginationResponse<T> | null>;
@@ -220,10 +230,11 @@ declare function usePagination<T = any>(fetchFn: (filters: PaginationFilters) =>
220
230
  handlePerPageChange: (newPerPage: number) => void;
221
231
  handleSearchChange: (newSearch: string) => void;
222
232
  handleSortChange: (column: string) => void;
233
+ handleFilterChange: (newFilters: Record<string, any>) => void;
223
234
  resetFilters: () => void;
224
235
  setFilter: (key: string, value: any) => void;
225
236
  removeFilter: (key: string) => void;
226
- getNumberingColumn: (options?: any) => {
237
+ getNumberingColumn: (columnOptions?: any) => {
227
238
  accessorKey: string;
228
239
  header: string;
229
240
  enableSorting: boolean;
@@ -265,10 +276,12 @@ declare function usePagination<T = any>(fetchFn: (filters: PaginationFilters) =>
265
276
  suspense: () => Promise<_tanstack_vue_query.QueryObserverResult<LaravelPaginationResponse<T>, Error>>;
266
277
  currentPage: vue.Ref<number, number>;
267
278
  perPage: vue.Ref<number, number>;
279
+ currentPerPage: vue.Ref<number, number>;
268
280
  search: vue.Ref<string, string>;
269
281
  sortBy: vue.Ref<string | null, string | null>;
270
282
  sortDirection: vue.Ref<"asc" | "desc", "asc" | "desc">;
271
283
  filters: vue.ComputedRef<PaginationFilters>;
284
+ customFilters: vue.Ref<Record<string, any>, Record<string, any>>;
272
285
  } | {
273
286
  tableData: vue.ComputedRef<T[]>;
274
287
  pagination: vue.ComputedRef<LaravelPaginationResponse<T> | null>;
@@ -276,10 +289,11 @@ declare function usePagination<T = any>(fetchFn: (filters: PaginationFilters) =>
276
289
  handlePerPageChange: (newPerPage: number) => void;
277
290
  handleSearchChange: (newSearch: string) => void;
278
291
  handleSortChange: (column: string) => void;
292
+ handleFilterChange: (newFilters: Record<string, any>) => void;
279
293
  resetFilters: () => void;
280
294
  setFilter: (key: string, value: any) => void;
281
295
  removeFilter: (key: string) => void;
282
- getNumberingColumn: (options?: any) => {
296
+ getNumberingColumn: (columnOptions?: any) => {
283
297
  accessorKey: string;
284
298
  header: string;
285
299
  enableSorting: boolean;
@@ -321,10 +335,12 @@ declare function usePagination<T = any>(fetchFn: (filters: PaginationFilters) =>
321
335
  suspense: () => Promise<_tanstack_vue_query.QueryObserverResult<LaravelPaginationResponse<T>, Error>>;
322
336
  currentPage: vue.Ref<number, number>;
323
337
  perPage: vue.Ref<number, number>;
338
+ currentPerPage: vue.Ref<number, number>;
324
339
  search: vue.Ref<string, string>;
325
340
  sortBy: vue.Ref<string | null, string | null>;
326
341
  sortDirection: vue.Ref<"asc" | "desc", "asc" | "desc">;
327
342
  filters: vue.ComputedRef<PaginationFilters>;
343
+ customFilters: vue.Ref<Record<string, any>, Record<string, any>>;
328
344
  } | {
329
345
  tableData: vue.ComputedRef<T[]>;
330
346
  pagination: vue.ComputedRef<LaravelPaginationResponse<T> | null>;
@@ -332,10 +348,11 @@ declare function usePagination<T = any>(fetchFn: (filters: PaginationFilters) =>
332
348
  handlePerPageChange: (newPerPage: number) => void;
333
349
  handleSearchChange: (newSearch: string) => void;
334
350
  handleSortChange: (column: string) => void;
351
+ handleFilterChange: (newFilters: Record<string, any>) => void;
335
352
  resetFilters: () => void;
336
353
  setFilter: (key: string, value: any) => void;
337
354
  removeFilter: (key: string) => void;
338
- getNumberingColumn: (options?: any) => {
355
+ getNumberingColumn: (columnOptions?: any) => {
339
356
  accessorKey: string;
340
357
  header: string;
341
358
  enableSorting: boolean;
@@ -377,10 +394,12 @@ declare function usePagination<T = any>(fetchFn: (filters: PaginationFilters) =>
377
394
  suspense: () => Promise<_tanstack_vue_query.QueryObserverResult<LaravelPaginationResponse<T>, Error>>;
378
395
  currentPage: vue.Ref<number, number>;
379
396
  perPage: vue.Ref<number, number>;
397
+ currentPerPage: vue.Ref<number, number>;
380
398
  search: vue.Ref<string, string>;
381
399
  sortBy: vue.Ref<string | null, string | null>;
382
400
  sortDirection: vue.Ref<"asc" | "desc", "asc" | "desc">;
383
401
  filters: vue.ComputedRef<PaginationFilters>;
402
+ customFilters: vue.Ref<Record<string, any>, Record<string, any>>;
384
403
  };
385
404
 
386
405
  interface NumberingOptions {
package/dist/index.d.ts CHANGED
@@ -5,12 +5,12 @@ import { Ref, ComputedRef } from 'vue';
5
5
 
6
6
  type LaravelPaginationResponse<T = any> = {
7
7
  data: T[];
8
- links: Array<{
8
+ links: {
9
9
  first: string | null;
10
10
  last: string | null;
11
11
  next: string | null;
12
12
  prev: string | null;
13
- }>;
13
+ };
14
14
  meta: {
15
15
  current_page: number;
16
16
  from: number;
@@ -44,6 +44,7 @@ interface UsePaginationOptions<T> extends Omit<UseQueryOptions<LaravelPagination
44
44
  defaultPerPage?: number;
45
45
  defaultSearch?: string;
46
46
  defaultSort?: SortState;
47
+ debounceMs?: number;
47
48
  }
48
49
  declare function usePagination<T = any>(fetchFn: (filters: PaginationFilters) => Promise<LaravelPaginationResponse<T>>, options: UsePaginationOptions<T>): {
49
50
  tableData: vue.ComputedRef<T[]>;
@@ -52,10 +53,11 @@ declare function usePagination<T = any>(fetchFn: (filters: PaginationFilters) =>
52
53
  handlePerPageChange: (newPerPage: number) => void;
53
54
  handleSearchChange: (newSearch: string) => void;
54
55
  handleSortChange: (column: string) => void;
56
+ handleFilterChange: (newFilters: Record<string, any>) => void;
55
57
  resetFilters: () => void;
56
58
  setFilter: (key: string, value: any) => void;
57
59
  removeFilter: (key: string) => void;
58
- getNumberingColumn: (options?: any) => {
60
+ getNumberingColumn: (columnOptions?: any) => {
59
61
  accessorKey: string;
60
62
  header: string;
61
63
  enableSorting: boolean;
@@ -97,10 +99,12 @@ declare function usePagination<T = any>(fetchFn: (filters: PaginationFilters) =>
97
99
  suspense: () => Promise<_tanstack_vue_query.QueryObserverResult<LaravelPaginationResponse<T>, Error>>;
98
100
  currentPage: vue.Ref<number, number>;
99
101
  perPage: vue.Ref<number, number>;
102
+ currentPerPage: vue.Ref<number, number>;
100
103
  search: vue.Ref<string, string>;
101
104
  sortBy: vue.Ref<string | null, string | null>;
102
105
  sortDirection: vue.Ref<"asc" | "desc", "asc" | "desc">;
103
106
  filters: vue.ComputedRef<PaginationFilters>;
107
+ customFilters: vue.Ref<Record<string, any>, Record<string, any>>;
104
108
  } | {
105
109
  tableData: vue.ComputedRef<T[]>;
106
110
  pagination: vue.ComputedRef<LaravelPaginationResponse<T> | null>;
@@ -108,10 +112,11 @@ declare function usePagination<T = any>(fetchFn: (filters: PaginationFilters) =>
108
112
  handlePerPageChange: (newPerPage: number) => void;
109
113
  handleSearchChange: (newSearch: string) => void;
110
114
  handleSortChange: (column: string) => void;
115
+ handleFilterChange: (newFilters: Record<string, any>) => void;
111
116
  resetFilters: () => void;
112
117
  setFilter: (key: string, value: any) => void;
113
118
  removeFilter: (key: string) => void;
114
- getNumberingColumn: (options?: any) => {
119
+ getNumberingColumn: (columnOptions?: any) => {
115
120
  accessorKey: string;
116
121
  header: string;
117
122
  enableSorting: boolean;
@@ -153,10 +158,12 @@ declare function usePagination<T = any>(fetchFn: (filters: PaginationFilters) =>
153
158
  suspense: () => Promise<_tanstack_vue_query.QueryObserverResult<LaravelPaginationResponse<T>, Error>>;
154
159
  currentPage: vue.Ref<number, number>;
155
160
  perPage: vue.Ref<number, number>;
161
+ currentPerPage: vue.Ref<number, number>;
156
162
  search: vue.Ref<string, string>;
157
163
  sortBy: vue.Ref<string | null, string | null>;
158
164
  sortDirection: vue.Ref<"asc" | "desc", "asc" | "desc">;
159
165
  filters: vue.ComputedRef<PaginationFilters>;
166
+ customFilters: vue.Ref<Record<string, any>, Record<string, any>>;
160
167
  } | {
161
168
  tableData: vue.ComputedRef<T[]>;
162
169
  pagination: vue.ComputedRef<LaravelPaginationResponse<T> | null>;
@@ -164,10 +171,11 @@ declare function usePagination<T = any>(fetchFn: (filters: PaginationFilters) =>
164
171
  handlePerPageChange: (newPerPage: number) => void;
165
172
  handleSearchChange: (newSearch: string) => void;
166
173
  handleSortChange: (column: string) => void;
174
+ handleFilterChange: (newFilters: Record<string, any>) => void;
167
175
  resetFilters: () => void;
168
176
  setFilter: (key: string, value: any) => void;
169
177
  removeFilter: (key: string) => void;
170
- getNumberingColumn: (options?: any) => {
178
+ getNumberingColumn: (columnOptions?: any) => {
171
179
  accessorKey: string;
172
180
  header: string;
173
181
  enableSorting: boolean;
@@ -209,10 +217,12 @@ declare function usePagination<T = any>(fetchFn: (filters: PaginationFilters) =>
209
217
  suspense: () => Promise<_tanstack_vue_query.QueryObserverResult<LaravelPaginationResponse<T>, Error>>;
210
218
  currentPage: vue.Ref<number, number>;
211
219
  perPage: vue.Ref<number, number>;
220
+ currentPerPage: vue.Ref<number, number>;
212
221
  search: vue.Ref<string, string>;
213
222
  sortBy: vue.Ref<string | null, string | null>;
214
223
  sortDirection: vue.Ref<"asc" | "desc", "asc" | "desc">;
215
224
  filters: vue.ComputedRef<PaginationFilters>;
225
+ customFilters: vue.Ref<Record<string, any>, Record<string, any>>;
216
226
  } | {
217
227
  tableData: vue.ComputedRef<T[]>;
218
228
  pagination: vue.ComputedRef<LaravelPaginationResponse<T> | null>;
@@ -220,10 +230,11 @@ declare function usePagination<T = any>(fetchFn: (filters: PaginationFilters) =>
220
230
  handlePerPageChange: (newPerPage: number) => void;
221
231
  handleSearchChange: (newSearch: string) => void;
222
232
  handleSortChange: (column: string) => void;
233
+ handleFilterChange: (newFilters: Record<string, any>) => void;
223
234
  resetFilters: () => void;
224
235
  setFilter: (key: string, value: any) => void;
225
236
  removeFilter: (key: string) => void;
226
- getNumberingColumn: (options?: any) => {
237
+ getNumberingColumn: (columnOptions?: any) => {
227
238
  accessorKey: string;
228
239
  header: string;
229
240
  enableSorting: boolean;
@@ -265,10 +276,12 @@ declare function usePagination<T = any>(fetchFn: (filters: PaginationFilters) =>
265
276
  suspense: () => Promise<_tanstack_vue_query.QueryObserverResult<LaravelPaginationResponse<T>, Error>>;
266
277
  currentPage: vue.Ref<number, number>;
267
278
  perPage: vue.Ref<number, number>;
279
+ currentPerPage: vue.Ref<number, number>;
268
280
  search: vue.Ref<string, string>;
269
281
  sortBy: vue.Ref<string | null, string | null>;
270
282
  sortDirection: vue.Ref<"asc" | "desc", "asc" | "desc">;
271
283
  filters: vue.ComputedRef<PaginationFilters>;
284
+ customFilters: vue.Ref<Record<string, any>, Record<string, any>>;
272
285
  } | {
273
286
  tableData: vue.ComputedRef<T[]>;
274
287
  pagination: vue.ComputedRef<LaravelPaginationResponse<T> | null>;
@@ -276,10 +289,11 @@ declare function usePagination<T = any>(fetchFn: (filters: PaginationFilters) =>
276
289
  handlePerPageChange: (newPerPage: number) => void;
277
290
  handleSearchChange: (newSearch: string) => void;
278
291
  handleSortChange: (column: string) => void;
292
+ handleFilterChange: (newFilters: Record<string, any>) => void;
279
293
  resetFilters: () => void;
280
294
  setFilter: (key: string, value: any) => void;
281
295
  removeFilter: (key: string) => void;
282
- getNumberingColumn: (options?: any) => {
296
+ getNumberingColumn: (columnOptions?: any) => {
283
297
  accessorKey: string;
284
298
  header: string;
285
299
  enableSorting: boolean;
@@ -321,10 +335,12 @@ declare function usePagination<T = any>(fetchFn: (filters: PaginationFilters) =>
321
335
  suspense: () => Promise<_tanstack_vue_query.QueryObserverResult<LaravelPaginationResponse<T>, Error>>;
322
336
  currentPage: vue.Ref<number, number>;
323
337
  perPage: vue.Ref<number, number>;
338
+ currentPerPage: vue.Ref<number, number>;
324
339
  search: vue.Ref<string, string>;
325
340
  sortBy: vue.Ref<string | null, string | null>;
326
341
  sortDirection: vue.Ref<"asc" | "desc", "asc" | "desc">;
327
342
  filters: vue.ComputedRef<PaginationFilters>;
343
+ customFilters: vue.Ref<Record<string, any>, Record<string, any>>;
328
344
  } | {
329
345
  tableData: vue.ComputedRef<T[]>;
330
346
  pagination: vue.ComputedRef<LaravelPaginationResponse<T> | null>;
@@ -332,10 +348,11 @@ declare function usePagination<T = any>(fetchFn: (filters: PaginationFilters) =>
332
348
  handlePerPageChange: (newPerPage: number) => void;
333
349
  handleSearchChange: (newSearch: string) => void;
334
350
  handleSortChange: (column: string) => void;
351
+ handleFilterChange: (newFilters: Record<string, any>) => void;
335
352
  resetFilters: () => void;
336
353
  setFilter: (key: string, value: any) => void;
337
354
  removeFilter: (key: string) => void;
338
- getNumberingColumn: (options?: any) => {
355
+ getNumberingColumn: (columnOptions?: any) => {
339
356
  accessorKey: string;
340
357
  header: string;
341
358
  enableSorting: boolean;
@@ -377,10 +394,12 @@ declare function usePagination<T = any>(fetchFn: (filters: PaginationFilters) =>
377
394
  suspense: () => Promise<_tanstack_vue_query.QueryObserverResult<LaravelPaginationResponse<T>, Error>>;
378
395
  currentPage: vue.Ref<number, number>;
379
396
  perPage: vue.Ref<number, number>;
397
+ currentPerPage: vue.Ref<number, number>;
380
398
  search: vue.Ref<string, string>;
381
399
  sortBy: vue.Ref<string | null, string | null>;
382
400
  sortDirection: vue.Ref<"asc" | "desc", "asc" | "desc">;
383
401
  filters: vue.ComputedRef<PaginationFilters>;
402
+ customFilters: vue.Ref<Record<string, any>, Record<string, any>>;
384
403
  };
385
404
 
386
405
  interface NumberingOptions {
package/dist/index.js CHANGED
@@ -31,7 +31,7 @@ function useTableNumbering() {
31
31
  const createNumberingColumn = (pagination, perPage, options = {}) => {
32
32
  const {
33
33
  header = "No.",
34
- className = "text-center text-ubd-ocean-700 dark:text-ubd-ocean-200 font-medium",
34
+ className = "",
35
35
  width = "60px",
36
36
  enableSorting = false
37
37
  } = options;
@@ -97,19 +97,32 @@ function useTableNumbering() {
97
97
  // src/composables/usePagination.ts
98
98
  var import_vue_query = require("@tanstack/vue-query");
99
99
  var import_vue2 = require("vue");
100
+ var VALID_SORT_COLUMN = /^[a-zA-Z0-9_.]+$/;
100
101
  function usePagination(fetchFn, options) {
101
102
  const currentPage = (0, import_vue2.ref)(1);
102
103
  const perPage = (0, import_vue2.ref)(options.defaultPerPage || 10);
103
104
  const search = (0, import_vue2.ref)(options.defaultSearch || "");
105
+ const debouncedSearch = (0, import_vue2.ref)(options.defaultSearch || "");
104
106
  const sortBy = (0, import_vue2.ref)(options.defaultSort?.column || null);
105
107
  const sortDirection = (0, import_vue2.ref)(options.defaultSort?.direction || "asc");
108
+ const customFilters = (0, import_vue2.ref)({});
109
+ let searchTimeout;
110
+ (0, import_vue2.watch)(search, (newValue) => {
111
+ clearTimeout(searchTimeout);
112
+ searchTimeout = setTimeout(() => {
113
+ debouncedSearch.value = newValue;
114
+ currentPage.value = 1;
115
+ }, options.debounceMs ?? 300);
116
+ });
117
+ (0, import_vue2.onScopeDispose)(() => clearTimeout(searchTimeout));
106
118
  const filters = (0, import_vue2.computed)(() => {
107
119
  const baseFilters = {
108
120
  page: currentPage.value,
109
121
  per_page: perPage.value,
110
- search: search.value
122
+ search: debouncedSearch.value,
123
+ ...customFilters.value
111
124
  };
112
- if (sortBy.value) {
125
+ if (sortBy.value && VALID_SORT_COLUMN.test(sortBy.value)) {
113
126
  baseFilters[`sort[${sortBy.value}]`] = sortDirection.value;
114
127
  }
115
128
  return baseFilters;
@@ -118,22 +131,24 @@ function usePagination(fetchFn, options) {
118
131
  ...options,
119
132
  queryKey: [options.queryKey, filters],
120
133
  queryFn: () => fetchFn(filters.value),
134
+ placeholderData: import_vue_query.keepPreviousData,
121
135
  refetchOnWindowFocus: false
122
136
  });
123
137
  const tableData = (0, import_vue2.computed)(() => queryResult.data.value?.data || []);
124
138
  const pagination = (0, import_vue2.computed)(() => queryResult.data.value || null);
125
139
  const handlePageChange = (page) => {
126
- currentPage.value = page;
140
+ const lastPage = pagination.value?.meta?.last_page ?? Infinity;
141
+ currentPage.value = Math.min(Math.max(1, Math.floor(page)), lastPage);
127
142
  };
128
143
  const handlePerPageChange = (newPerPage) => {
129
- perPage.value = newPerPage;
144
+ perPage.value = Math.max(1, Math.floor(newPerPage));
130
145
  currentPage.value = 1;
131
146
  };
132
147
  const handleSearchChange = (newSearch) => {
133
148
  search.value = newSearch;
134
- currentPage.value = 1;
135
149
  };
136
150
  const handleSortChange = (column) => {
151
+ if (!VALID_SORT_COLUMN.test(column)) return;
137
152
  if (sortBy.value === column) {
138
153
  sortDirection.value = sortDirection.value === "asc" ? "desc" : "asc";
139
154
  } else {
@@ -143,39 +158,41 @@ function usePagination(fetchFn, options) {
143
158
  currentPage.value = 1;
144
159
  };
145
160
  const setFilter = (key, value) => {
146
- filters.value[key] = value;
161
+ customFilters.value = { ...customFilters.value, [key]: value };
147
162
  currentPage.value = 1;
148
163
  };
149
164
  const removeFilter = (key) => {
150
- delete filters.value[key];
165
+ const { [key]: _, ...rest } = customFilters.value;
166
+ customFilters.value = rest;
167
+ currentPage.value = 1;
168
+ };
169
+ const handleFilterChange = (newFilters) => {
170
+ customFilters.value = { ...newFilters };
151
171
  currentPage.value = 1;
152
172
  };
153
173
  const resetFilters = () => {
154
174
  currentPage.value = 1;
155
175
  perPage.value = options.defaultPerPage || 10;
156
176
  search.value = options.defaultSearch || "";
177
+ debouncedSearch.value = options.defaultSearch || "";
157
178
  sortBy.value = options.defaultSort?.column || null;
158
179
  sortDirection.value = options.defaultSort?.direction || "asc";
180
+ customFilters.value = {};
159
181
  };
160
- let searchTimeout;
161
- (0, import_vue2.watch)(search, () => {
162
- clearTimeout(searchTimeout);
163
- searchTimeout = window.setTimeout(() => {
164
- currentPage.value = 1;
165
- }, 300);
166
- });
167
182
  const { createNumberingColumn } = useTableNumbering();
168
- const getNumberingColumn = (options2 = {}) => {
169
- return createNumberingColumn(pagination, perPage, options2);
183
+ const getNumberingColumn = (columnOptions = {}) => {
184
+ return createNumberingColumn(pagination, perPage, columnOptions);
170
185
  };
171
186
  return {
172
187
  // State
173
188
  currentPage,
174
189
  perPage,
190
+ currentPerPage: perPage,
175
191
  search,
176
192
  sortBy,
177
193
  sortDirection,
178
194
  filters,
195
+ customFilters,
179
196
  // Query result
180
197
  ...queryResult,
181
198
  // Computed
@@ -186,6 +203,7 @@ function usePagination(fetchFn, options) {
186
203
  handlePerPageChange,
187
204
  handleSearchChange,
188
205
  handleSortChange,
206
+ handleFilterChange,
189
207
  resetFilters,
190
208
  setFilter,
191
209
  removeFilter,
package/dist/index.mjs CHANGED
@@ -4,7 +4,7 @@ function useTableNumbering() {
4
4
  const createNumberingColumn = (pagination, perPage, options = {}) => {
5
5
  const {
6
6
  header = "No.",
7
- className = "text-center text-ubd-ocean-700 dark:text-ubd-ocean-200 font-medium",
7
+ className = "",
8
8
  width = "60px",
9
9
  enableSorting = false
10
10
  } = options;
@@ -68,21 +68,34 @@ function useTableNumbering() {
68
68
  }
69
69
 
70
70
  // src/composables/usePagination.ts
71
- import { useQuery } from "@tanstack/vue-query";
72
- import { computed, ref, watch } from "vue";
71
+ import { useQuery, keepPreviousData } from "@tanstack/vue-query";
72
+ import { computed, onScopeDispose, ref, watch } from "vue";
73
+ var VALID_SORT_COLUMN = /^[a-zA-Z0-9_.]+$/;
73
74
  function usePagination(fetchFn, options) {
74
75
  const currentPage = ref(1);
75
76
  const perPage = ref(options.defaultPerPage || 10);
76
77
  const search = ref(options.defaultSearch || "");
78
+ const debouncedSearch = ref(options.defaultSearch || "");
77
79
  const sortBy = ref(options.defaultSort?.column || null);
78
80
  const sortDirection = ref(options.defaultSort?.direction || "asc");
81
+ const customFilters = ref({});
82
+ let searchTimeout;
83
+ watch(search, (newValue) => {
84
+ clearTimeout(searchTimeout);
85
+ searchTimeout = setTimeout(() => {
86
+ debouncedSearch.value = newValue;
87
+ currentPage.value = 1;
88
+ }, options.debounceMs ?? 300);
89
+ });
90
+ onScopeDispose(() => clearTimeout(searchTimeout));
79
91
  const filters = computed(() => {
80
92
  const baseFilters = {
81
93
  page: currentPage.value,
82
94
  per_page: perPage.value,
83
- search: search.value
95
+ search: debouncedSearch.value,
96
+ ...customFilters.value
84
97
  };
85
- if (sortBy.value) {
98
+ if (sortBy.value && VALID_SORT_COLUMN.test(sortBy.value)) {
86
99
  baseFilters[`sort[${sortBy.value}]`] = sortDirection.value;
87
100
  }
88
101
  return baseFilters;
@@ -91,22 +104,24 @@ function usePagination(fetchFn, options) {
91
104
  ...options,
92
105
  queryKey: [options.queryKey, filters],
93
106
  queryFn: () => fetchFn(filters.value),
107
+ placeholderData: keepPreviousData,
94
108
  refetchOnWindowFocus: false
95
109
  });
96
110
  const tableData = computed(() => queryResult.data.value?.data || []);
97
111
  const pagination = computed(() => queryResult.data.value || null);
98
112
  const handlePageChange = (page) => {
99
- currentPage.value = page;
113
+ const lastPage = pagination.value?.meta?.last_page ?? Infinity;
114
+ currentPage.value = Math.min(Math.max(1, Math.floor(page)), lastPage);
100
115
  };
101
116
  const handlePerPageChange = (newPerPage) => {
102
- perPage.value = newPerPage;
117
+ perPage.value = Math.max(1, Math.floor(newPerPage));
103
118
  currentPage.value = 1;
104
119
  };
105
120
  const handleSearchChange = (newSearch) => {
106
121
  search.value = newSearch;
107
- currentPage.value = 1;
108
122
  };
109
123
  const handleSortChange = (column) => {
124
+ if (!VALID_SORT_COLUMN.test(column)) return;
110
125
  if (sortBy.value === column) {
111
126
  sortDirection.value = sortDirection.value === "asc" ? "desc" : "asc";
112
127
  } else {
@@ -116,39 +131,41 @@ function usePagination(fetchFn, options) {
116
131
  currentPage.value = 1;
117
132
  };
118
133
  const setFilter = (key, value) => {
119
- filters.value[key] = value;
134
+ customFilters.value = { ...customFilters.value, [key]: value };
120
135
  currentPage.value = 1;
121
136
  };
122
137
  const removeFilter = (key) => {
123
- delete filters.value[key];
138
+ const { [key]: _, ...rest } = customFilters.value;
139
+ customFilters.value = rest;
140
+ currentPage.value = 1;
141
+ };
142
+ const handleFilterChange = (newFilters) => {
143
+ customFilters.value = { ...newFilters };
124
144
  currentPage.value = 1;
125
145
  };
126
146
  const resetFilters = () => {
127
147
  currentPage.value = 1;
128
148
  perPage.value = options.defaultPerPage || 10;
129
149
  search.value = options.defaultSearch || "";
150
+ debouncedSearch.value = options.defaultSearch || "";
130
151
  sortBy.value = options.defaultSort?.column || null;
131
152
  sortDirection.value = options.defaultSort?.direction || "asc";
153
+ customFilters.value = {};
132
154
  };
133
- let searchTimeout;
134
- watch(search, () => {
135
- clearTimeout(searchTimeout);
136
- searchTimeout = window.setTimeout(() => {
137
- currentPage.value = 1;
138
- }, 300);
139
- });
140
155
  const { createNumberingColumn } = useTableNumbering();
141
- const getNumberingColumn = (options2 = {}) => {
142
- return createNumberingColumn(pagination, perPage, options2);
156
+ const getNumberingColumn = (columnOptions = {}) => {
157
+ return createNumberingColumn(pagination, perPage, columnOptions);
143
158
  };
144
159
  return {
145
160
  // State
146
161
  currentPage,
147
162
  perPage,
163
+ currentPerPage: perPage,
148
164
  search,
149
165
  sortBy,
150
166
  sortDirection,
151
167
  filters,
168
+ customFilters,
152
169
  // Query result
153
170
  ...queryResult,
154
171
  // Computed
@@ -159,6 +176,7 @@ function usePagination(fetchFn, options) {
159
176
  handlePerPageChange,
160
177
  handleSearchChange,
161
178
  handleSortChange,
179
+ handleFilterChange,
162
180
  resetFilters,
163
181
  setFilter,
164
182
  removeFilter,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toniel/laravel-tanstack-pagination",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Vue 3 composables for Laravel pagination with TanStack Query",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",