@serhiitupilow/nuxt-table 0.1.4 → 0.1.5

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
@@ -109,6 +109,8 @@ import {
109
109
  type NuxtTableClassNames,
110
110
  type NuxtTableColumn,
111
111
  type NuxtTableColumnOrderChange,
112
+ type NuxtTableManualFilterChange,
113
+ type NuxtTableManualSortChange,
112
114
  type TableRow,
113
115
  type UseNuxtTableOptions,
114
116
  type ValueResolver,
@@ -134,14 +136,18 @@ import {
134
136
 
135
137
  ### Events
136
138
 
137
- | Event | Payload | Description |
138
- | --------------------- | ---------------------------- | ----------------------------------------------- |
139
- | `column-order-change` | `NuxtTableColumnOrderChange` | Emitted after successful drag-and-drop reorder. |
139
+ | Event | Payload | Description |
140
+ | ---------------------- | ----------------------------- | ---------------------------------------------------------------------------------- |
141
+ | `column-order-change` | `NuxtTableColumnOrderChange` | Emitted after successful drag-and-drop reorder. |
142
+ | `manual-sort-change` | `NuxtTableManualSortChange` | Emitted when sorting is changed for a column with `sortFunction`. |
143
+ | `manual-filter-change` | `NuxtTableManualFilterChange` | Emitted when filter value changes for a column with `filterFunction` / `filterFn`. |
140
144
 
141
145
  ### Behavior notes
142
146
 
143
147
  - Filtering is applied before sorting.
144
148
  - Sorting cycles by click: `asc -> desc -> off`.
149
+ - If a column has `filterFunction` / `filterFn`, built-in filtering is disabled for that column and table emits `manual-filter-change`.
150
+ - If a column has `sortFunction`, built-in sorting is disabled for that column and table emits `manual-sort-change`.
145
151
  - Column width has a minimum of `140px`.
146
152
  - Empty state text: `No rows match the current filters.`
147
153
  - Rendering is table-only (no built-in toolbar/summary controls).
@@ -161,12 +167,26 @@ interface NuxtTableColumn {
161
167
  sortDescComponent?: Component;
162
168
  sortDefaultComponent?: Component;
163
169
  sortKey?: ValueResolver;
170
+ sortFunction?: (
171
+ leftRow: TableRow,
172
+ rightRow: TableRow,
173
+ column: NuxtTableColumn,
174
+ tableRows: TableRow[],
175
+ direction: "asc" | "desc",
176
+ ) => number;
164
177
  filterKey?: ValueResolver;
165
178
  formatter?: (value: unknown, row: TableRow) => string;
179
+ filterFunction?: (
180
+ row: TableRow,
181
+ filterValue: unknown,
182
+ column: NuxtTableColumn,
183
+ tableRows: TableRow[],
184
+ ) => boolean;
166
185
  filterFn?: (
167
186
  row: TableRow,
168
187
  filterValue: unknown,
169
188
  column: NuxtTableColumn,
189
+ tableRows: TableRow[],
170
190
  ) => boolean;
171
191
  cellComponent?: Component;
172
192
  filterComponent?: Component;
@@ -179,10 +199,12 @@ interface NuxtTableColumn {
179
199
 
180
200
  - `key`: primary accessor path for display value. Supports dot notation through resolvers (for example: `user.profile.name`) when used by `sortKey`/`filterKey`.
181
201
  - `sortKey`: alternate accessor/function used for sorting.
202
+ - `sortFunction`: enables manual sort mode for that column. Table emits `manual-sort-change` and does not sort rows automatically.
182
203
  - `sortAscComponent` / `sortDescComponent` / `sortDefaultComponent`: optional sort button content per state. If not provided, defaults are `Asc`, `Desc`, and `Sort`.
183
204
  - `filterKey`: alternate accessor/function used for default text filtering.
184
205
  - `formatter`: transforms display value for default body rendering (`<span>{{ value }}</span>`).
185
- - `filterFn`: custom row-level filter logic. If set, it overrides default string `includes` filtering for that column.
206
+ - `filterFunction`: enables manual filter mode for that column. Table emits `manual-filter-change` and does not filter rows automatically.
207
+ - `filterFn`: legacy alias for `filterFunction` (same manual behavior).
186
208
  - `cellComponent`: custom body renderer receives `row`, `column`, and `value`.
187
209
  - `filterComponent`: custom header filter renderer receives `modelValue` and `column`, and should emit `update:model-value`.
188
210
 
@@ -318,6 +340,245 @@ const columns: NuxtTableColumn[] = [
318
340
 
319
341
  If these components are not provided, the table automatically uses the default labels.
320
342
 
343
+ ### Detailed `manual-filter-change` and `manual-sort-change` flow
344
+
345
+ When `filterFunction` / `sortFunction` is set, table switches that column to manual mode and emits `manual-filter-change` / `manual-sort-change`. You control final dataset in parent component.
346
+
347
+ ```vue
348
+ <script setup lang="ts">
349
+ import { computed, ref } from "vue";
350
+ import type {
351
+ NuxtTableColumn,
352
+ NuxtTableManualFilterChange,
353
+ NuxtTableManualSortChange,
354
+ } from "@serhiitupilow/nuxt-table/runtime";
355
+
356
+ type TicketRow = {
357
+ id: number;
358
+ title: string;
359
+ priority: "low" | "medium" | "high" | "critical";
360
+ status: "todo" | "in_progress" | "done";
361
+ createdAt: string;
362
+ };
363
+
364
+ const allRows = ref<TicketRow[]>([
365
+ {
366
+ id: 1,
367
+ title: "Fix auth flow",
368
+ priority: "high",
369
+ status: "in_progress",
370
+ createdAt: "2026-02-18",
371
+ },
372
+ {
373
+ id: 2,
374
+ title: "Write docs",
375
+ priority: "low",
376
+ status: "done",
377
+ createdAt: "2026-02-10",
378
+ },
379
+ {
380
+ id: 3,
381
+ title: "Release v1",
382
+ priority: "critical",
383
+ status: "todo",
384
+ createdAt: "2026-02-20",
385
+ },
386
+ ]);
387
+
388
+ const columns: NuxtTableColumn[] = [
389
+ { key: "id", label: "ID", sortable: true },
390
+ { key: "title", label: "Title", sortable: true, filterable: true },
391
+ {
392
+ key: "status",
393
+ label: "Status",
394
+ filterable: true,
395
+ filterFunction: () => true,
396
+ },
397
+ {
398
+ key: "priority",
399
+ label: "Priority",
400
+ sortable: true,
401
+ sortFunction: () => 0,
402
+ },
403
+ ];
404
+
405
+ const manualStatusFilter = ref<string>("");
406
+ const manualSort = ref<{ key: string; direction: "asc" | "desc" | null }>({
407
+ key: "",
408
+ direction: null,
409
+ });
410
+
411
+ const rows = computed(() => {
412
+ const statusFiltered = allRows.value.filter((row) => {
413
+ if (!manualStatusFilter.value) {
414
+ return true;
415
+ }
416
+
417
+ return row.status === manualStatusFilter.value;
418
+ });
419
+
420
+ if (manualSort.value.key !== "priority" || !manualSort.value.direction) {
421
+ return statusFiltered;
422
+ }
423
+
424
+ const rank: Record<TicketRow["priority"], number> = {
425
+ low: 0,
426
+ medium: 1,
427
+ high: 2,
428
+ critical: 3,
429
+ };
430
+
431
+ const directionMultiplier = manualSort.value.direction === "asc" ? 1 : -1;
432
+
433
+ return [...statusFiltered].sort((left, right) => {
434
+ return (rank[left.priority] - rank[right.priority]) * directionMultiplier;
435
+ });
436
+ });
437
+
438
+ function onManualFilterChange(payload: NuxtTableManualFilterChange) {
439
+ if (payload.columnKey !== "status") {
440
+ return;
441
+ }
442
+
443
+ manualStatusFilter.value = String(payload.value ?? "").trim();
444
+ }
445
+
446
+ function onManualSortChange(payload: NuxtTableManualSortChange) {
447
+ manualSort.value = {
448
+ key: payload.columnKey,
449
+ direction: payload.direction,
450
+ };
451
+ }
452
+ </script>
453
+
454
+ <template>
455
+ <NuxtTable
456
+ :columns="columns"
457
+ :rows="rows"
458
+ @manual-filter-change="onManualFilterChange"
459
+ @manual-sort-change="onManualSortChange"
460
+ />
461
+ </template>
462
+ ```
463
+
464
+ Notes:
465
+
466
+ - In manual mode, the table does not transform rows for that column; you pass already transformed `rows` from outside.
467
+ - Use payload `columnKey`, `value`, `direction`, `rows`, and `filters` from events to build server/client-side query logic.
468
+ - `filterFn` remains supported as a legacy alias, but `filterFunction` is preferred.
469
+
470
+ ### Server-side `manual-filter-change` / `manual-sort-change` example
471
+
472
+ Use manual events to request data from backend and pass ready rows back to table.
473
+
474
+ ```vue
475
+ <script setup lang="ts">
476
+ import { ref } from "vue";
477
+ import type {
478
+ NuxtTableColumn,
479
+ NuxtTableManualFilterChange,
480
+ NuxtTableManualSortChange,
481
+ } from "@serhiitupilow/nuxt-table/runtime";
482
+
483
+ type UserRow = {
484
+ id: number;
485
+ name: string;
486
+ status: "active" | "paused";
487
+ createdAt: string;
488
+ };
489
+
490
+ const rows = ref<UserRow[]>([]);
491
+ const loading = ref(false);
492
+
493
+ const query = ref<{
494
+ page: number;
495
+ pageSize: number;
496
+ status: string;
497
+ sortKey: string;
498
+ sortDirection: "asc" | "desc" | "";
499
+ }>({
500
+ page: 1,
501
+ pageSize: 20,
502
+ status: "",
503
+ sortKey: "",
504
+ sortDirection: "",
505
+ });
506
+
507
+ const columns: NuxtTableColumn[] = [
508
+ { key: "id", label: "ID", sortable: true },
509
+ { key: "name", label: "Name", sortable: true, filterable: true },
510
+ {
511
+ key: "status",
512
+ label: "Status",
513
+ filterable: true,
514
+ filterFunction: () => true,
515
+ },
516
+ {
517
+ key: "createdAt",
518
+ label: "Created",
519
+ sortable: true,
520
+ sortFunction: () => 0,
521
+ },
522
+ ];
523
+
524
+ async function fetchRows() {
525
+ loading.value = true;
526
+
527
+ try {
528
+ const data = await $fetch<UserRow[]>("/api/users", {
529
+ query: {
530
+ page: query.value.page,
531
+ pageSize: query.value.pageSize,
532
+ status: query.value.status,
533
+ sortKey: query.value.sortKey,
534
+ sortDirection: query.value.sortDirection,
535
+ },
536
+ });
537
+
538
+ rows.value = data;
539
+ } finally {
540
+ loading.value = false;
541
+ }
542
+ }
543
+
544
+ function onManualFilterChange(payload: NuxtTableManualFilterChange) {
545
+ if (payload.columnKey === "status") {
546
+ query.value.status = String(payload.value ?? "").trim();
547
+ query.value.page = 1;
548
+ }
549
+
550
+ fetchRows();
551
+ }
552
+
553
+ function onManualSortChange(payload: NuxtTableManualSortChange) {
554
+ query.value.sortKey = payload.columnKey;
555
+ query.value.sortDirection = payload.direction ?? "";
556
+ query.value.page = 1;
557
+
558
+ fetchRows();
559
+ }
560
+
561
+ await fetchRows();
562
+ </script>
563
+
564
+ <template>
565
+ <NuxtTable
566
+ :columns="columns"
567
+ :rows="rows"
568
+ @manual-filter-change="onManualFilterChange"
569
+ @manual-sort-change="onManualSortChange"
570
+ />
571
+
572
+ <p v-if="loading">Loading...</p>
573
+ </template>
574
+ ```
575
+
576
+ What happens here:
577
+
578
+ - `filterFunction` and `sortFunction` switch corresponding columns to manual mode.
579
+ - Table emits `manual-filter-change` / `manual-sort-change` instead of transforming `rows` internally.
580
+ - Parent updates query params, calls API, and passes backend result as new `rows`.
581
+
321
582
  ### Custom filter component
322
583
 
323
584
  ```vue
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@serhiitupilow/nuxt-table",
3
3
  "configKey": "nuxtTable",
4
- "version": "0.1.4",
4
+ "version": "0.1.5",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -1,4 +1,4 @@
1
- import type { NuxtTableClassNames, NuxtTableColumn, NuxtTableColumnOrderChange, TableRow } from "../types/table.js";
1
+ import type { NuxtTableClassNames, NuxtTableColumn, NuxtTableColumnOrderChange, NuxtTableManualFilterChange, NuxtTableManualSortChange, TableRow } from "../types/table.js";
2
2
  type __VLS_Props = {
3
3
  columns: NuxtTableColumn[];
4
4
  rows: TableRow[];
@@ -13,8 +13,12 @@ type __VLS_Props = {
13
13
  };
14
14
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
15
15
  columnOrderChange: (payload: NuxtTableColumnOrderChange) => any;
16
+ manualSortChange: (payload: NuxtTableManualSortChange) => any;
17
+ manualFilterChange: (payload: NuxtTableManualFilterChange) => any;
16
18
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
17
19
  onColumnOrderChange?: ((payload: NuxtTableColumnOrderChange) => any) | undefined;
20
+ onManualSortChange?: ((payload: NuxtTableManualSortChange) => any) | undefined;
21
+ onManualFilterChange?: ((payload: NuxtTableManualFilterChange) => any) | undefined;
18
22
  }>, {
19
23
  storageKey: string;
20
24
  rowKey: string | ((row: TableRow, index: number) => string | number);
@@ -13,7 +13,7 @@ const props = defineProps({
13
13
  enableColumnResize: { type: Boolean, required: false, default: true },
14
14
  classNames: { type: Object, required: false }
15
15
  });
16
- const emit = defineEmits(["columnOrderChange"]);
16
+ const emit = defineEmits(["columnOrderChange", "manualSortChange", "manualFilterChange"]);
17
17
  const defaultClassNames = {
18
18
  root: "nuxt-table",
19
19
  toolbar: "nuxt-table__toolbar",
@@ -73,6 +73,12 @@ const {
73
73
  enableColumnDnd: toRef(props, "enableColumnDnd"),
74
74
  onColumnOrderChange: (payload) => {
75
75
  emit("columnOrderChange", payload);
76
+ },
77
+ onManualSortChange: (payload) => {
78
+ emit("manualSortChange", payload);
79
+ },
80
+ onManualFilterChange: (payload) => {
81
+ emit("manualFilterChange", payload);
76
82
  }
77
83
  });
78
84
  const displayedColumns = computed(() => {
@@ -1,4 +1,4 @@
1
- import type { NuxtTableClassNames, NuxtTableColumn, NuxtTableColumnOrderChange, TableRow } from "../types/table.js";
1
+ import type { NuxtTableClassNames, NuxtTableColumn, NuxtTableColumnOrderChange, NuxtTableManualFilterChange, NuxtTableManualSortChange, TableRow } from "../types/table.js";
2
2
  type __VLS_Props = {
3
3
  columns: NuxtTableColumn[];
4
4
  rows: TableRow[];
@@ -13,8 +13,12 @@ type __VLS_Props = {
13
13
  };
14
14
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
15
15
  columnOrderChange: (payload: NuxtTableColumnOrderChange) => any;
16
+ manualSortChange: (payload: NuxtTableManualSortChange) => any;
17
+ manualFilterChange: (payload: NuxtTableManualFilterChange) => any;
16
18
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
17
19
  onColumnOrderChange?: ((payload: NuxtTableColumnOrderChange) => any) | undefined;
20
+ onManualSortChange?: ((payload: NuxtTableManualSortChange) => any) | undefined;
21
+ onManualFilterChange?: ((payload: NuxtTableManualFilterChange) => any) | undefined;
18
22
  }>, {
19
23
  storageKey: string;
20
24
  rowKey: string | ((row: TableRow, index: number) => string | number);
@@ -41,8 +41,9 @@ export function useNuxtTable(options) {
41
41
  if (!isFilterActive(filterValue)) {
42
42
  return true;
43
43
  }
44
- if (column.filterFn) {
45
- return column.filterFn(row, filterValue, column);
44
+ const customFilterFunction = column.filterFunction ?? column.filterFn;
45
+ if (customFilterFunction) {
46
+ return true;
46
47
  }
47
48
  const candidate = resolveColumnValue(
48
49
  row,
@@ -62,8 +63,13 @@ export function useNuxtTable(options) {
62
63
  if (!activeColumn) {
63
64
  return filteredRows.value;
64
65
  }
65
- const directionMultiplier = sortState.value.direction === "asc" ? 1 : -1;
66
+ const sortDirection = sortState.value.direction;
67
+ const directionMultiplier = sortDirection === "asc" ? 1 : -1;
66
68
  const accessor = activeColumn.sortKey ?? activeColumn.key;
69
+ const customSortFunction = activeColumn.sortFunction;
70
+ if (customSortFunction) {
71
+ return filteredRows.value;
72
+ }
67
73
  return [...filteredRows.value].sort((leftRow, rightRow) => {
68
74
  const leftValue = resolveColumnValue(leftRow, accessor);
69
75
  const rightValue = resolveColumnValue(rightRow, accessor);
@@ -212,16 +218,58 @@ export function useNuxtTable(options) {
212
218
  }
213
219
  if (!sortState.value || sortState.value.key !== column.key) {
214
220
  sortState.value = { key: column.key, direction: "asc" };
221
+ if (column.sortFunction) {
222
+ options.onManualSortChange?.({
223
+ columnKey: column.key,
224
+ direction: "asc",
225
+ column,
226
+ rows: [...options.rows.value],
227
+ filters: { ...filters.value }
228
+ });
229
+ }
215
230
  return;
216
231
  }
217
232
  if (sortState.value.direction === "asc") {
218
233
  sortState.value = { key: column.key, direction: "desc" };
234
+ if (column.sortFunction) {
235
+ options.onManualSortChange?.({
236
+ columnKey: column.key,
237
+ direction: "desc",
238
+ column,
239
+ rows: [...options.rows.value],
240
+ filters: { ...filters.value }
241
+ });
242
+ }
219
243
  return;
220
244
  }
221
245
  sortState.value = null;
246
+ if (column.sortFunction) {
247
+ options.onManualSortChange?.({
248
+ columnKey: column.key,
249
+ direction: null,
250
+ column,
251
+ rows: [...options.rows.value],
252
+ filters: { ...filters.value }
253
+ });
254
+ }
222
255
  }
223
256
  function setFilter(columnKey, value) {
224
257
  filters.value[columnKey] = value;
258
+ const column = columnsByKey.value.get(columnKey);
259
+ if (!column) {
260
+ return;
261
+ }
262
+ const customFilterFunction = column.filterFunction ?? column.filterFn;
263
+ if (!customFilterFunction) {
264
+ return;
265
+ }
266
+ options.onManualFilterChange?.({
267
+ columnKey,
268
+ value,
269
+ column,
270
+ rows: [...options.rows.value],
271
+ filters: { ...filters.value }
272
+ });
225
273
  }
226
274
  function toggleColumn(columnKey) {
227
275
  if (enabledColumnKeys.value.includes(columnKey)) {
@@ -1,2 +1,2 @@
1
- export { useNuxtTable } from './composables/useNuxtTable.js';
2
- export type { NuxtTableClassNames, NuxtTableColumn, NuxtTableColumnOrderChange, TableRow, UseNuxtTableOptions, ValueResolver, } from './types/table.js';
1
+ export { useNuxtTable } from "./composables/useNuxtTable.js";
2
+ export type { NuxtTableClassNames, NuxtTableColumn, NuxtTableColumnOrderChange, NuxtTableManualFilterChange, NuxtTableManualSortChange, TableRow, UseNuxtTableOptions, ValueResolver, } from "./types/table.js";
@@ -10,9 +10,11 @@ export interface NuxtTableColumn {
10
10
  sortDescComponent?: Component;
11
11
  sortDefaultComponent?: Component;
12
12
  sortKey?: ValueResolver;
13
+ sortFunction?: (leftRow: TableRow, rightRow: TableRow, column: NuxtTableColumn, tableRows: TableRow[], direction: "asc" | "desc") => number;
13
14
  filterKey?: ValueResolver;
14
15
  formatter?: (value: unknown, row: TableRow) => string;
15
- filterFn?: (row: TableRow, filterValue: unknown, column: NuxtTableColumn) => boolean;
16
+ filterFunction?: (row: TableRow, filterValue: unknown, column: NuxtTableColumn, tableRows: TableRow[]) => boolean;
17
+ filterFn?: (row: TableRow, filterValue: unknown, column: NuxtTableColumn, tableRows: TableRow[]) => boolean;
16
18
  cellComponent?: Component;
17
19
  filterComponent?: Component;
18
20
  headerClassName?: string;
@@ -24,6 +26,20 @@ export interface NuxtTableColumnOrderChange {
24
26
  fromIndex: number;
25
27
  toIndex: number;
26
28
  }
29
+ export interface NuxtTableManualSortChange {
30
+ columnKey: string;
31
+ direction: "asc" | "desc" | null;
32
+ column: NuxtTableColumn;
33
+ rows: TableRow[];
34
+ filters: Record<string, unknown>;
35
+ }
36
+ export interface NuxtTableManualFilterChange {
37
+ columnKey: string;
38
+ value: unknown;
39
+ column: NuxtTableColumn;
40
+ rows: TableRow[];
41
+ filters: Record<string, unknown>;
42
+ }
27
43
  export interface NuxtTableClassNames {
28
44
  root: string;
29
45
  toolbar: string;
@@ -56,4 +72,6 @@ export interface UseNuxtTableOptions {
56
72
  rowKey: Ref<string | ((row: TableRow, index: number) => string | number)>;
57
73
  enableColumnDnd: Ref<boolean>;
58
74
  onColumnOrderChange?: (payload: NuxtTableColumnOrderChange) => void;
75
+ onManualSortChange?: (payload: NuxtTableManualSortChange) => void;
76
+ onManualFilterChange?: (payload: NuxtTableManualFilterChange) => void;
59
77
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serhiitupilow/nuxt-table",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Nuxt module with a functional table component (sorting, filtering, column visibility, resize, optional DnD)",
5
5
  "type": "module",
6
6
  "license": "MIT",