@serhiitupilow/nuxt-table 0.1.4 → 0.1.6

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 sort state changes and `@manual-sort-change` listener is provided. |
143
+ | `manual-filter-change` | `NuxtTableManualFilterChange` | Emitted when filter value changes and `@manual-filter-change` listener is provided. |
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 `@manual-filter-change` is provided, built-in filtering is disabled.
150
+ - If `@manual-sort-change` is provided, built-in sorting is disabled.
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).
@@ -163,11 +169,6 @@ interface NuxtTableColumn {
163
169
  sortKey?: ValueResolver;
164
170
  filterKey?: ValueResolver;
165
171
  formatter?: (value: unknown, row: TableRow) => string;
166
- filterFn?: (
167
- row: TableRow,
168
- filterValue: unknown,
169
- column: NuxtTableColumn,
170
- ) => boolean;
171
172
  cellComponent?: Component;
172
173
  filterComponent?: Component;
173
174
  headerClassName?: string;
@@ -182,7 +183,6 @@ interface NuxtTableColumn {
182
183
  - `sortAscComponent` / `sortDescComponent` / `sortDefaultComponent`: optional sort button content per state. If not provided, defaults are `Asc`, `Desc`, and `Sort`.
183
184
  - `filterKey`: alternate accessor/function used for default text filtering.
184
185
  - `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.
186
186
  - `cellComponent`: custom body renderer receives `row`, `column`, and `value`.
187
187
  - `filterComponent`: custom header filter renderer receives `modelValue` and `column`, and should emit `update:model-value`.
188
188
 
@@ -318,6 +318,224 @@ const columns: NuxtTableColumn[] = [
318
318
 
319
319
  If these components are not provided, the table automatically uses the default labels.
320
320
 
321
+ ### Detailed `manual-filter-change` and `manual-sort-change` flow
322
+
323
+ When `@manual-filter-change` / `@manual-sort-change` listeners are passed, table switches to manual mode and emits these events. You control final dataset in parent component.
324
+
325
+ ```vue
326
+ <script setup lang="ts">
327
+ import { computed, ref } from "vue";
328
+ import type {
329
+ NuxtTableColumn,
330
+ NuxtTableManualFilterChange,
331
+ NuxtTableManualSortChange,
332
+ } from "@serhiitupilow/nuxt-table/runtime";
333
+
334
+ type TicketRow = {
335
+ id: number;
336
+ title: string;
337
+ priority: "low" | "medium" | "high" | "critical";
338
+ status: "todo" | "in_progress" | "done";
339
+ createdAt: string;
340
+ };
341
+
342
+ const allRows = ref<TicketRow[]>([
343
+ {
344
+ id: 1,
345
+ title: "Fix auth flow",
346
+ priority: "high",
347
+ status: "in_progress",
348
+ createdAt: "2026-02-18",
349
+ },
350
+ {
351
+ id: 2,
352
+ title: "Write docs",
353
+ priority: "low",
354
+ status: "done",
355
+ createdAt: "2026-02-10",
356
+ },
357
+ {
358
+ id: 3,
359
+ title: "Release v1",
360
+ priority: "critical",
361
+ status: "todo",
362
+ createdAt: "2026-02-20",
363
+ },
364
+ ]);
365
+
366
+ const columns: NuxtTableColumn[] = [
367
+ { key: "id", label: "ID", sortable: true },
368
+ { key: "title", label: "Title", sortable: true, filterable: true },
369
+ { key: "status", label: "Status", filterable: true },
370
+ { key: "priority", label: "Priority", sortable: true },
371
+ ];
372
+
373
+ const manualStatusFilter = ref<string>("");
374
+ const manualSort = ref<{ key: string; direction: "asc" | "desc" | null }>({
375
+ key: "",
376
+ direction: null,
377
+ });
378
+
379
+ const rows = computed(() => {
380
+ const statusFiltered = allRows.value.filter((row) => {
381
+ if (!manualStatusFilter.value) {
382
+ return true;
383
+ }
384
+
385
+ return row.status === manualStatusFilter.value;
386
+ });
387
+
388
+ if (manualSort.value.key !== "priority" || !manualSort.value.direction) {
389
+ return statusFiltered;
390
+ }
391
+
392
+ const rank: Record<TicketRow["priority"], number> = {
393
+ low: 0,
394
+ medium: 1,
395
+ high: 2,
396
+ critical: 3,
397
+ };
398
+
399
+ const directionMultiplier = manualSort.value.direction === "asc" ? 1 : -1;
400
+
401
+ return [...statusFiltered].sort((left, right) => {
402
+ return (rank[left.priority] - rank[right.priority]) * directionMultiplier;
403
+ });
404
+ });
405
+
406
+ function onManualFilterChange(payload: NuxtTableManualFilterChange) {
407
+ if (payload.columnKey !== "status") {
408
+ return;
409
+ }
410
+
411
+ manualStatusFilter.value = String(payload.value ?? "").trim();
412
+ }
413
+
414
+ function onManualSortChange(payload: NuxtTableManualSortChange) {
415
+ manualSort.value = {
416
+ key: payload.columnKey,
417
+ direction: payload.direction,
418
+ };
419
+ }
420
+ </script>
421
+
422
+ <template>
423
+ <NuxtTable
424
+ :columns="columns"
425
+ :rows="rows"
426
+ @manual-filter-change="onManualFilterChange"
427
+ @manual-sort-change="onManualSortChange"
428
+ />
429
+ </template>
430
+ ```
431
+
432
+ Notes:
433
+
434
+ - In manual mode, the table does not transform rows internally; you pass already transformed `rows` from outside.
435
+ - Use payload `columnKey`, `value`, `direction`, `rows`, and `filters` from events to build server/client-side query logic.
436
+
437
+ ### Server-side `manual-filter-change` / `manual-sort-change` example
438
+
439
+ Use manual events to request data from backend and pass ready rows back to table.
440
+
441
+ ```vue
442
+ <script setup lang="ts">
443
+ import { ref } from "vue";
444
+ import type {
445
+ NuxtTableColumn,
446
+ NuxtTableManualFilterChange,
447
+ NuxtTableManualSortChange,
448
+ } from "@serhiitupilow/nuxt-table/runtime";
449
+
450
+ type UserRow = {
451
+ id: number;
452
+ name: string;
453
+ status: "active" | "paused";
454
+ createdAt: string;
455
+ };
456
+
457
+ const rows = ref<UserRow[]>([]);
458
+ const loading = ref(false);
459
+
460
+ const query = ref<{
461
+ page: number;
462
+ pageSize: number;
463
+ status: string;
464
+ sortKey: string;
465
+ sortDirection: "asc" | "desc" | "";
466
+ }>({
467
+ page: 1,
468
+ pageSize: 20,
469
+ status: "",
470
+ sortKey: "",
471
+ sortDirection: "",
472
+ });
473
+
474
+ const columns: NuxtTableColumn[] = [
475
+ { key: "id", label: "ID", sortable: true },
476
+ { key: "name", label: "Name", sortable: true, filterable: true },
477
+ { key: "status", label: "Status", filterable: true },
478
+ { key: "createdAt", label: "Created", sortable: true },
479
+ ];
480
+
481
+ async function fetchRows() {
482
+ loading.value = true;
483
+
484
+ try {
485
+ const data = await $fetch<UserRow[]>("/api/users", {
486
+ query: {
487
+ page: query.value.page,
488
+ pageSize: query.value.pageSize,
489
+ status: query.value.status,
490
+ sortKey: query.value.sortKey,
491
+ sortDirection: query.value.sortDirection,
492
+ },
493
+ });
494
+
495
+ rows.value = data;
496
+ } finally {
497
+ loading.value = false;
498
+ }
499
+ }
500
+
501
+ function onManualFilterChange(payload: NuxtTableManualFilterChange) {
502
+ if (payload.columnKey === "status") {
503
+ query.value.status = String(payload.value ?? "").trim();
504
+ query.value.page = 1;
505
+ }
506
+
507
+ fetchRows();
508
+ }
509
+
510
+ function onManualSortChange(payload: NuxtTableManualSortChange) {
511
+ query.value.sortKey = payload.columnKey;
512
+ query.value.sortDirection = payload.direction ?? "";
513
+ query.value.page = 1;
514
+
515
+ fetchRows();
516
+ }
517
+
518
+ await fetchRows();
519
+ </script>
520
+
521
+ <template>
522
+ <NuxtTable
523
+ :columns="columns"
524
+ :rows="rows"
525
+ @manual-filter-change="onManualFilterChange"
526
+ @manual-sort-change="onManualSortChange"
527
+ />
528
+
529
+ <p v-if="loading">Loading...</p>
530
+ </template>
531
+ ```
532
+
533
+ What happens here:
534
+
535
+ - Passing `@manual-filter-change` and `@manual-sort-change` switches the table to manual mode.
536
+ - Table emits `manual-filter-change` / `manual-sort-change` instead of transforming `rows` internally.
537
+ - Parent updates query params, calls API, and passes backend result as new `rows`.
538
+
321
539
  ### Custom filter component
322
540
 
323
541
  ```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.6",
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);
@@ -1,5 +1,5 @@
1
1
  <script setup>
2
- import { computed, toRef } from "vue";
2
+ import { computed, toRef, useAttrs } from "vue";
3
3
  import { useNuxtTable } from "../composables/useNuxtTable";
4
4
  const props = defineProps({
5
5
  columns: { type: Array, required: true },
@@ -13,7 +13,14 @@ 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
+ const attrs = useAttrs();
18
+ const hasManualSortChangeListener = computed(() => {
19
+ return Boolean(attrs.onManualSortChange);
20
+ });
21
+ const hasManualFilterChangeListener = computed(() => {
22
+ return Boolean(attrs.onManualFilterChange);
23
+ });
17
24
  const defaultClassNames = {
18
25
  root: "nuxt-table",
19
26
  toolbar: "nuxt-table__toolbar",
@@ -73,7 +80,13 @@ const {
73
80
  enableColumnDnd: toRef(props, "enableColumnDnd"),
74
81
  onColumnOrderChange: (payload) => {
75
82
  emit("columnOrderChange", payload);
76
- }
83
+ },
84
+ onManualSortChange: hasManualSortChangeListener.value ? (payload) => {
85
+ emit("manualSortChange", payload);
86
+ } : void 0,
87
+ onManualFilterChange: hasManualFilterChangeListener.value ? (payload) => {
88
+ emit("manualFilterChange", payload);
89
+ } : void 0
77
90
  });
78
91
  const displayedColumns = computed(() => {
79
92
  if (!props.enabledColumns) {
@@ -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);
@@ -20,6 +20,10 @@ export function useNuxtTable(options) {
20
20
  const headerElements = ref({});
21
21
  const columnWidths = ref({});
22
22
  const activeResize = ref(null);
23
+ const isManualSortMode = computed(() => Boolean(options.onManualSortChange));
24
+ const isManualFilterMode = computed(
25
+ () => Boolean(options.onManualFilterChange)
26
+ );
23
27
  const availableColumnKeys = computed(
24
28
  () => options.columns.value.map((column) => column.key)
25
29
  );
@@ -41,8 +45,8 @@ export function useNuxtTable(options) {
41
45
  if (!isFilterActive(filterValue)) {
42
46
  return true;
43
47
  }
44
- if (column.filterFn) {
45
- return column.filterFn(row, filterValue, column);
48
+ if (isManualFilterMode.value) {
49
+ return true;
46
50
  }
47
51
  const candidate = resolveColumnValue(
48
52
  row,
@@ -62,8 +66,12 @@ export function useNuxtTable(options) {
62
66
  if (!activeColumn) {
63
67
  return filteredRows.value;
64
68
  }
65
- const directionMultiplier = sortState.value.direction === "asc" ? 1 : -1;
69
+ const sortDirection = sortState.value.direction;
70
+ const directionMultiplier = sortDirection === "asc" ? 1 : -1;
66
71
  const accessor = activeColumn.sortKey ?? activeColumn.key;
72
+ if (isManualSortMode.value) {
73
+ return filteredRows.value;
74
+ }
67
75
  return [...filteredRows.value].sort((leftRow, rightRow) => {
68
76
  const leftValue = resolveColumnValue(leftRow, accessor);
69
77
  const rightValue = resolveColumnValue(rightRow, accessor);
@@ -212,16 +220,57 @@ export function useNuxtTable(options) {
212
220
  }
213
221
  if (!sortState.value || sortState.value.key !== column.key) {
214
222
  sortState.value = { key: column.key, direction: "asc" };
223
+ if (isManualSortMode.value) {
224
+ options.onManualSortChange?.({
225
+ columnKey: column.key,
226
+ direction: "asc",
227
+ column,
228
+ rows: [...options.rows.value],
229
+ filters: { ...filters.value }
230
+ });
231
+ }
215
232
  return;
216
233
  }
217
234
  if (sortState.value.direction === "asc") {
218
235
  sortState.value = { key: column.key, direction: "desc" };
236
+ if (isManualSortMode.value) {
237
+ options.onManualSortChange?.({
238
+ columnKey: column.key,
239
+ direction: "desc",
240
+ column,
241
+ rows: [...options.rows.value],
242
+ filters: { ...filters.value }
243
+ });
244
+ }
219
245
  return;
220
246
  }
221
247
  sortState.value = null;
248
+ if (isManualSortMode.value) {
249
+ options.onManualSortChange?.({
250
+ columnKey: column.key,
251
+ direction: null,
252
+ column,
253
+ rows: [...options.rows.value],
254
+ filters: { ...filters.value }
255
+ });
256
+ }
222
257
  }
223
258
  function setFilter(columnKey, value) {
224
259
  filters.value[columnKey] = value;
260
+ const column = columnsByKey.value.get(columnKey);
261
+ if (!column) {
262
+ return;
263
+ }
264
+ if (!isManualFilterMode.value) {
265
+ return;
266
+ }
267
+ options.onManualFilterChange?.({
268
+ columnKey,
269
+ value,
270
+ column,
271
+ rows: [...options.rows.value],
272
+ filters: { ...filters.value }
273
+ });
225
274
  }
226
275
  function toggleColumn(columnKey) {
227
276
  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";
@@ -12,7 +12,6 @@ export interface NuxtTableColumn {
12
12
  sortKey?: ValueResolver;
13
13
  filterKey?: ValueResolver;
14
14
  formatter?: (value: unknown, row: TableRow) => string;
15
- filterFn?: (row: TableRow, filterValue: unknown, column: NuxtTableColumn) => boolean;
16
15
  cellComponent?: Component;
17
16
  filterComponent?: Component;
18
17
  headerClassName?: string;
@@ -24,6 +23,20 @@ export interface NuxtTableColumnOrderChange {
24
23
  fromIndex: number;
25
24
  toIndex: number;
26
25
  }
26
+ export interface NuxtTableManualSortChange {
27
+ columnKey: string;
28
+ direction: "asc" | "desc" | null;
29
+ column: NuxtTableColumn;
30
+ rows: TableRow[];
31
+ filters: Record<string, unknown>;
32
+ }
33
+ export interface NuxtTableManualFilterChange {
34
+ columnKey: string;
35
+ value: unknown;
36
+ column: NuxtTableColumn;
37
+ rows: TableRow[];
38
+ filters: Record<string, unknown>;
39
+ }
27
40
  export interface NuxtTableClassNames {
28
41
  root: string;
29
42
  toolbar: string;
@@ -56,4 +69,6 @@ export interface UseNuxtTableOptions {
56
69
  rowKey: Ref<string | ((row: TableRow, index: number) => string | number)>;
57
70
  enableColumnDnd: Ref<boolean>;
58
71
  onColumnOrderChange?: (payload: NuxtTableColumnOrderChange) => void;
72
+ onManualSortChange?: (payload: NuxtTableManualSortChange) => void;
73
+ onManualFilterChange?: (payload: NuxtTableManualFilterChange) => void;
59
74
  }
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.6",
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",