@serhiitupilow/nuxt-table 0.1.3 → 0.1.4

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
@@ -1,37 +1,41 @@
1
1
  # @serhiitupilow/nuxt-table
2
2
 
3
- Nuxt module that provides a single `NuxtTable` component with:
3
+ A Nuxt module that registers a global `NuxtTable` component for data tables with:
4
4
 
5
- - sorting
6
- - filtering
7
- - column visibility manager
8
- - column resize
5
+ - client-side sorting
6
+ - client-side filtering
9
7
  - optional drag-and-drop column reordering
10
- - class-based styling (no Tailwind classes in component templates)
8
+ - optional column resize
9
+ - persisted column order/visibility/widths in `localStorage`
10
+ - configurable cell/header/filter rendering
11
11
 
12
- ## Install
12
+ ## Requirements
13
13
 
14
- Use any package manager:
14
+ - `nuxt >= 3.11.0`
15
+ - `vue >= 3.4.0`
16
+
17
+ ## Installation
15
18
 
16
19
  ```bash
17
20
  npm i @serhiitupilow/nuxt-table
18
21
  # or
22
+ pnpm add @serhiitupilow/nuxt-table
23
+ # or
19
24
  yarn add @serhiitupilow/nuxt-table
20
25
  # or
21
26
  bun add @serhiitupilow/nuxt-table
22
- # or
23
- pnpm add @serhiitupilow/nuxt-table
24
27
  ```
25
28
 
26
- ## Nuxt config
29
+ ## Nuxt setup
27
30
 
28
31
  ```ts
32
+ // nuxt.config.ts
29
33
  export default defineNuxtConfig({
30
34
  modules: ["@serhiitupilow/nuxt-table"],
31
35
  });
32
36
  ```
33
37
 
34
- Optional module config:
38
+ ### Module options
35
39
 
36
40
  ```ts
37
41
  export default defineNuxtConfig({
@@ -42,21 +46,38 @@ export default defineNuxtConfig({
42
46
  });
43
47
  ```
44
48
 
45
- ## Usage
49
+ | Option | Type | Default | Description |
50
+ | --------------------- | --------- | ------- | ------------------------------------------------------------------------------------------------ |
51
+ | `injectDefaultStyles` | `boolean` | `true` | Injects bundled CSS from the module runtime. Set to `false` if you fully style classes yourself. |
52
+
53
+ ## Quick start
46
54
 
47
55
  ```vue
48
56
  <script setup lang="ts">
49
- import type { NuxtTableColumn } from "@serhiitupilow/nuxt-table/dist/runtime/types/table";
57
+ import type { NuxtTableColumn } from "@serhiitupilow/nuxt-table/runtime";
58
+
59
+ type UserRow = {
60
+ id: number;
61
+ name: string;
62
+ status: "active" | "paused";
63
+ createdAt: string;
64
+ };
50
65
 
51
66
  const columns: NuxtTableColumn[] = [
52
67
  { key: "id", label: "ID", sortable: true, filterable: true },
53
68
  { key: "name", label: "Name", sortable: true, filterable: true },
54
69
  { key: "status", label: "Status", sortable: true, filterable: true },
70
+ {
71
+ key: "createdAt",
72
+ label: "Created",
73
+ sortable: true,
74
+ formatter: (value) => new Date(String(value)).toLocaleDateString(),
75
+ },
55
76
  ];
56
77
 
57
- const rows = [
58
- { id: 1, name: "Alice", status: "active" },
59
- { id: 2, name: "Bob", status: "paused" },
78
+ const rows: UserRow[] = [
79
+ { id: 1, name: "Alice", status: "active", createdAt: "2026-02-01" },
80
+ { id: 2, name: "Bob", status: "paused", createdAt: "2026-02-14" },
60
81
  ];
61
82
 
62
83
  function onColumnOrderChange(payload: {
@@ -80,24 +101,296 @@ function onColumnOrderChange(payload: {
80
101
  </template>
81
102
  ```
82
103
 
104
+ ## Public runtime exports
105
+
106
+ ```ts
107
+ import {
108
+ useNuxtTable,
109
+ type NuxtTableClassNames,
110
+ type NuxtTableColumn,
111
+ type NuxtTableColumnOrderChange,
112
+ type TableRow,
113
+ type UseNuxtTableOptions,
114
+ type ValueResolver,
115
+ } from "@serhiitupilow/nuxt-table/runtime";
116
+ ```
117
+
118
+ ## `NuxtTable` component API
119
+
120
+ ### Props
121
+
122
+ | Prop | Type | Default | Description |
123
+ | -------------------- | -------------------------------------------- | -------------- | ----------------------------------------------------------------------- |
124
+ | `columns` | `NuxtTableColumn[]` | required | Column definitions. |
125
+ | `rows` | `TableRow[]` | required | Data rows. |
126
+ | `enabledColumns` | `string[]` | `undefined` | Explicitly controls visible columns (in the current ordered sequence). |
127
+ | `storageKey` | `string` | `"nuxt-table"` | Prefix for persisted table UI state in `localStorage`. |
128
+ | `rowKey` | `string \| (row, index) => string \| number` | `"id"` | Unique key resolver for row rendering. |
129
+ | `title` | `string` | `"Table"` | Legacy prop kept for compatibility (not currently rendered in UI). |
130
+ | `showToolbar` | `boolean` | `true` | Legacy prop kept for compatibility (toolbar is not currently rendered). |
131
+ | `enableColumnDnd` | `boolean` | `false` | Enables drag-and-drop header reordering. |
132
+ | `enableColumnResize` | `boolean` | `true` | Enables resize handle on header cells. |
133
+ | `classNames` | `Partial<NuxtTableClassNames>` | `{}` | Class overrides for semantic class hooks. |
134
+
135
+ ### Events
136
+
137
+ | Event | Payload | Description |
138
+ | --------------------- | ---------------------------- | ----------------------------------------------- |
139
+ | `column-order-change` | `NuxtTableColumnOrderChange` | Emitted after successful drag-and-drop reorder. |
140
+
141
+ ### Behavior notes
142
+
143
+ - Filtering is applied before sorting.
144
+ - Sorting cycles by click: `asc -> desc -> off`.
145
+ - Column width has a minimum of `140px`.
146
+ - Empty state text: `No rows match the current filters.`
147
+ - Rendering is table-only (no built-in toolbar/summary controls).
148
+ - DnD headers use cursor states: `grab` and `grabbing`.
149
+
150
+ ## Column definition (`NuxtTableColumn`)
151
+
152
+ ```ts
153
+ type ValueResolver = string | ((row: TableRow) => unknown);
154
+
155
+ interface NuxtTableColumn {
156
+ key: string;
157
+ label: string;
158
+ sortable?: boolean;
159
+ filterable?: boolean;
160
+ sortAscComponent?: Component;
161
+ sortDescComponent?: Component;
162
+ sortDefaultComponent?: Component;
163
+ sortKey?: ValueResolver;
164
+ filterKey?: ValueResolver;
165
+ formatter?: (value: unknown, row: TableRow) => string;
166
+ filterFn?: (
167
+ row: TableRow,
168
+ filterValue: unknown,
169
+ column: NuxtTableColumn,
170
+ ) => boolean;
171
+ cellComponent?: Component;
172
+ filterComponent?: Component;
173
+ headerClassName?: string;
174
+ cellClassName?: string;
175
+ }
176
+ ```
177
+
178
+ ### Field details
179
+
180
+ - `key`: primary accessor path for display value. Supports dot notation through resolvers (for example: `user.profile.name`) when used by `sortKey`/`filterKey`.
181
+ - `sortKey`: alternate accessor/function used for sorting.
182
+ - `sortAscComponent` / `sortDescComponent` / `sortDefaultComponent`: optional sort button content per state. If not provided, defaults are `Asc`, `Desc`, and `Sort`.
183
+ - `filterKey`: alternate accessor/function used for default text filtering.
184
+ - `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
+ - `cellComponent`: custom body renderer receives `row`, `column`, and `value`.
187
+ - `filterComponent`: custom header filter renderer receives `modelValue` and `column`, and should emit `update:model-value`.
188
+
189
+ ## Persistence model
190
+
191
+ State is persisted per `storageKey` in `localStorage` with keys:
192
+
193
+ - `${storageKey}:order`
194
+ - `${storageKey}:enabledColumns`
195
+ - `${storageKey}:widths`
196
+
197
+ Persisted values are validated against current `columns`; unknown keys are ignored.
198
+
83
199
  ## Styling
84
200
 
85
- `NuxtTable` uses semantic class names (like `nuxt-table__header-cell`) and receives a `classNames` prop for overrides.
201
+ The component uses semantic class hooks. You can:
202
+
203
+ 1. use injected default styles, and/or
204
+ 2. override classes via `classNames`, and/or
205
+ 3. provide your own global CSS.
86
206
 
87
- You can style globally in your project CSS:
207
+ ### Default class keys (`NuxtTableClassNames`)
88
208
 
89
- ```css
90
- .nuxt-table__header-cell {
91
- background: #f8fafc;
209
+ ```ts
210
+ interface NuxtTableClassNames {
211
+ root: string;
212
+ toolbar: string;
213
+ toolbarTitle: string;
214
+ toolbarActions: string;
215
+ toolbarButton: string;
216
+ columnManager: string;
217
+ columnManagerTitle: string;
218
+ columnManagerItem: string;
219
+ tableWrapper: string;
220
+ table: string;
221
+ tableHead: string;
222
+ tableBody: string;
223
+ bodyRow: string;
224
+ emptyCell: string;
225
+ headerCell: string;
226
+ headerCellDragSource: string;
227
+ headerCellDragOver: string;
228
+ headerTop: string;
229
+ headerLabel: string;
230
+ sortButton: string;
231
+ filterInput: string;
232
+ resizeHandle: string;
233
+ bodyCell: string;
92
234
  }
93
235
  ```
94
236
 
95
- Or override class names from props:
237
+ > Some toolbar-related class keys remain in the public type for compatibility, even though the current component template renders only the table.
238
+
239
+ ### `classNames` example
96
240
 
97
241
  ```vue
98
242
  <NuxtTable
99
243
  :columns="columns"
100
244
  :rows="rows"
101
- :class-names="{ table: 'my-table', headerCell: 'my-header-cell' }"
245
+ :class-names="{
246
+ table: 'my-table',
247
+ headerCell: 'my-header-cell',
248
+ bodyCell: 'my-body-cell',
249
+ filterInput: 'my-filter-input',
250
+ }"
102
251
  />
103
252
  ```
253
+
254
+ ## Advanced examples
255
+
256
+ ### Enable / disable visible columns
257
+
258
+ Use `enabledColumns` to control what is rendered.
259
+
260
+ ```vue
261
+ <script setup lang="ts">
262
+ const enabledColumns = ref<string[]>(["id", "name", "status"]);
263
+
264
+ function toggleStatusColumn() {
265
+ if (enabledColumns.value.includes("status")) {
266
+ enabledColumns.value = enabledColumns.value.filter(
267
+ (key) => key !== "status",
268
+ );
269
+ return;
270
+ }
271
+
272
+ enabledColumns.value = [...enabledColumns.value, "status"];
273
+ }
274
+ </script>
275
+
276
+ <template>
277
+ <button type="button" @click="toggleStatusColumn">
278
+ Toggle status column
279
+ </button>
280
+
281
+ <NuxtTable
282
+ :columns="columns"
283
+ :rows="rows"
284
+ :enabled-columns="enabledColumns"
285
+ />
286
+ </template>
287
+ ```
288
+
289
+ ### Custom sort state components (ASC / DESC / default)
290
+
291
+ ```vue
292
+ <!-- SortAsc.vue -->
293
+ <template><span>↑ ASC</span></template>
294
+
295
+ <!-- SortDesc.vue -->
296
+ <template><span>↓ DESC</span></template>
297
+
298
+ <!-- SortIdle.vue -->
299
+ <template><span>↕ SORT</span></template>
300
+ ```
301
+
302
+ ```ts
303
+ import SortAsc from "~/components/SortAsc.vue";
304
+ import SortDesc from "~/components/SortDesc.vue";
305
+ import SortIdle from "~/components/SortIdle.vue";
306
+
307
+ const columns: NuxtTableColumn[] = [
308
+ {
309
+ key: "name",
310
+ label: "Name",
311
+ sortable: true,
312
+ sortAscComponent: SortAsc,
313
+ sortDescComponent: SortDesc,
314
+ sortDefaultComponent: SortIdle,
315
+ },
316
+ ];
317
+ ```
318
+
319
+ If these components are not provided, the table automatically uses the default labels.
320
+
321
+ ### Custom filter component
322
+
323
+ ```vue
324
+ <!-- StatusFilter.vue -->
325
+ <script setup lang="ts">
326
+ const props = defineProps<{
327
+ modelValue: unknown;
328
+ column: { key: string; label: string };
329
+ }>();
330
+
331
+ const emit = defineEmits<{
332
+ "update:model-value": [value: string];
333
+ }>();
334
+ </script>
335
+
336
+ <template>
337
+ <select
338
+ :value="String(props.modelValue ?? '')"
339
+ @change="
340
+ emit('update:model-value', ($event.target as HTMLSelectElement).value)
341
+ "
342
+ >
343
+ <option value="">All</option>
344
+ <option value="active">Active</option>
345
+ <option value="paused">Paused</option>
346
+ </select>
347
+ </template>
348
+ ```
349
+
350
+ ```ts
351
+ const columns: NuxtTableColumn[] = [
352
+ {
353
+ key: "status",
354
+ label: "Status",
355
+ filterable: true,
356
+ filterComponent: StatusFilter,
357
+ },
358
+ ];
359
+ ```
360
+
361
+ ### Custom cell component
362
+
363
+ ```vue
364
+ <!-- NameCell.vue -->
365
+ <script setup lang="ts">
366
+ const props = defineProps<{
367
+ row: Record<string, unknown>;
368
+ value: unknown;
369
+ }>();
370
+ </script>
371
+
372
+ <template>
373
+ <strong>{{ props.value }}</strong>
374
+ </template>
375
+ ```
376
+
377
+ ```ts
378
+ const columns: NuxtTableColumn[] = [
379
+ {
380
+ key: "name",
381
+ label: "Name",
382
+ cellComponent: NameCell,
383
+ },
384
+ ];
385
+ ```
386
+
387
+ ## Troubleshooting
388
+
389
+ - DnD does nothing: ensure `enableColumnDnd` is `true`.
390
+ - Filters do nothing: ensure column has `filterable: true` or a `filterComponent` that emits `update:model-value`.
391
+ - Unexpected row keys: set a stable `rowKey` function for datasets without `id`.
392
+ - Style conflicts: disable `injectDefaultStyles` and provide full custom CSS.
393
+
394
+ ## License
395
+
396
+ MIT
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.3",
4
+ "version": "0.1.4",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -1 +1 @@
1
- .nuxt-table{display:grid;gap:1rem}.nuxt-table__toolbar{align-items:center;display:flex;gap:.75rem;justify-content:space-between}.nuxt-table__toolbar-title{font-size:1.125rem;font-weight:600;margin:0}.nuxt-table__toolbar-actions{align-items:center;display:flex;gap:.5rem;position:relative}.nuxt-table__toolbar-button{background:#fff;border:1px solid #d1d5db;border-radius:.375rem;cursor:pointer;font-size:.875rem;line-height:1;padding:.5rem .75rem}.nuxt-table__column-manager{background:#fff;border:1px solid #d1d5db;border-radius:.5rem;box-shadow:0 10px 20px rgba(15,23,42,.14);min-width:14rem;padding:.75rem;position:absolute;right:0;top:2.75rem;z-index:30}.nuxt-table__column-manager-title{font-size:.875rem;font-weight:600;margin:0 0 .5rem}.nuxt-table__column-manager-item{align-items:center;cursor:pointer;display:flex;font-size:.875rem;gap:.5rem;margin-bottom:.375rem}.nuxt-table__column-manager-item:last-child{margin-bottom:0}.nuxt-table__wrapper{border:1px solid #d1d5db;border-radius:.5rem;overflow-x:auto}.nuxt-table__table{border-collapse:collapse;min-width:100%;table-layout:fixed;width:100%}.nuxt-table__head{background:#f8fafc}.nuxt-table__header-cell{border-bottom:1px solid #e5e7eb;border-right:1px solid #e5e7eb;padding:.75rem;position:relative;text-align:left;transition:background-color .2s ease,box-shadow .2s ease,transform .2s ease,opacity .2s ease;vertical-align:top}.nuxt-table__header-cell:last-child{border-right:0}.nuxt-table__header-cell--drag-source{box-shadow:inset 0 0 0 1px #9ca3af;opacity:.82}.nuxt-table__header-cell--drag-over{background:#f1f5f9;box-shadow:inset 0 0 0 2px #cbd5e1}.nuxt-table__header-top{align-items:center;display:flex;gap:.5rem;justify-content:space-between;margin-bottom:.5rem}.nuxt-table__header-label{font-weight:600}.nuxt-table__sort-button{background:#fff;border:1px solid #d1d5db;border-radius:.375rem;cursor:pointer;font-size:.75rem;line-height:1;padding:.25rem .5rem}.nuxt-table__filter-input{border:1px solid #d1d5db;border-radius:.375rem;font-size:.875rem;padding:.375rem .5rem;width:100%}.nuxt-table__resize-handle{cursor:col-resize;height:100%;position:absolute;right:0;top:0;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:.5rem}.nuxt-table__body-row{border-bottom:1px solid #e5e7eb}.nuxt-table__body-row:last-child{border-bottom:0}.nuxt-table__body-cell{border-right:1px solid #e5e7eb;padding:.75rem;vertical-align:top}.nuxt-table__body-cell:last-child{border-right:0}.nuxt-table__empty-cell{color:#6b7280;padding:1rem;text-align:center}
1
+ .nuxt-table{display:grid;gap:1rem}.nuxt-table__toolbar{align-items:center;display:flex;gap:.75rem;justify-content:space-between}.nuxt-table__toolbar-title{font-size:1.125rem;font-weight:600;margin:0}.nuxt-table__toolbar-actions{align-items:center;display:flex;gap:.5rem;position:relative}.nuxt-table__toolbar-button{background:#fff;border:1px solid #d1d5db;border-radius:.375rem;cursor:pointer;font-size:.875rem;line-height:1;padding:.5rem .75rem}.nuxt-table__column-manager{position:relative}.nuxt-table__column-manager>summary{list-style:none}.nuxt-table__column-manager>summary::-webkit-details-marker{display:none}.nuxt-table__column-manager[open]{background:#fff;border:1px solid #d1d5db;border-radius:.5rem;box-shadow:0 10px 20px rgba(15,23,42,.14);min-width:14rem;padding:.75rem;position:absolute;right:0;top:0;z-index:30}.nuxt-table__column-manager[open]>summary{margin-bottom:.5rem}.nuxt-table__column-manager-title{font-size:.875rem;font-weight:600;margin:0 0 .5rem}.nuxt-table__column-manager-item{align-items:center;cursor:pointer;display:flex;font-size:.875rem;gap:.5rem;margin-bottom:.375rem}.nuxt-table__column-manager-item:last-child{margin-bottom:0}.nuxt-table__wrapper{border:1px solid #d1d5db;border-radius:.5rem;overflow-x:auto}.nuxt-table__table{border-collapse:collapse;min-width:100%;table-layout:fixed;width:100%}.nuxt-table__head{background:#f8fafc}.nuxt-table__header-cell{border-bottom:1px solid #e5e7eb;border-right:1px solid #e5e7eb;padding:.75rem;position:relative;text-align:left;transition:background-color .2s ease,box-shadow .2s ease,transform .2s ease,opacity .2s ease;vertical-align:top}.nuxt-table__header-cell:last-child{border-right:0}.nuxt-table__header-cell--drag-source{box-shadow:inset 0 0 0 1px #9ca3af;opacity:.82}.nuxt-table__header-cell--drag-over{background:#f1f5f9;box-shadow:inset 0 0 0 2px #cbd5e1}.nuxt-table__header-cell--dnd{cursor:grab}.nuxt-table__header-cell--dragging{cursor:grabbing}.nuxt-table__header-top{align-items:center;display:flex;gap:.5rem;justify-content:space-between;margin-bottom:.5rem}.nuxt-table__header-label{font-weight:600}.nuxt-table__sort-button{background:#fff;border:1px solid #d1d5db;border-radius:.375rem;cursor:pointer;font-size:.75rem;line-height:1;padding:.25rem .5rem}.nuxt-table__filter-input{border:1px solid #d1d5db;border-radius:.375rem;font-size:.875rem;padding:.375rem .5rem;width:100%}.nuxt-table__resize-handle{cursor:col-resize;height:100%;position:absolute;right:0;top:0;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:.5rem}.nuxt-table__body-row{border-bottom:1px solid #e5e7eb}.nuxt-table__body-row:last-child{border-bottom:0}.nuxt-table__body-cell{border-right:1px solid #e5e7eb;padding:.75rem;vertical-align:top}.nuxt-table__body-cell:last-child{border-right:0}.nuxt-table__empty-cell{color:#6b7280;padding:1rem;text-align:center}
@@ -1,7 +1,8 @@
1
- import type { NuxtTableClassNames, NuxtTableColumn, NuxtTableColumnOrderChange, TableRow } from '../types/table.js';
1
+ import type { NuxtTableClassNames, NuxtTableColumn, NuxtTableColumnOrderChange, TableRow } from "../types/table.js";
2
2
  type __VLS_Props = {
3
3
  columns: NuxtTableColumn[];
4
4
  rows: TableRow[];
5
+ enabledColumns?: string[];
5
6
  storageKey?: string;
6
7
  rowKey?: string | ((row: TableRow, index: number) => string | number);
7
8
  title?: string;
@@ -4,6 +4,7 @@ import { useNuxtTable } from "../composables/useNuxtTable";
4
4
  const props = defineProps({
5
5
  columns: { type: Array, required: true },
6
6
  rows: { type: Array, required: true },
7
+ enabledColumns: { type: Array, required: false },
7
8
  storageKey: { type: String, required: false, default: "nuxt-table" },
8
9
  rowKey: { type: [String, Function], required: false, default: "id" },
9
10
  title: { type: String, required: false, default: "Table" },
@@ -49,16 +50,11 @@ const {
49
50
  visibleColumns,
50
51
  sortedRows,
51
52
  filters,
52
- isColumnManagerOpen,
53
- enabledColumnKeys,
54
53
  dragSourceColumnKey,
55
54
  dragOverColumnKey,
56
55
  getSortDirection,
57
56
  toggleSort,
58
57
  setFilter,
59
- clearAllFilters,
60
- toggleColumn,
61
- resetColumns,
62
58
  onHeaderDragStart,
63
59
  onHeaderDragOver,
64
60
  onHeaderDragLeave,
@@ -79,63 +75,23 @@ const {
79
75
  emit("columnOrderChange", payload);
80
76
  }
81
77
  });
78
+ const displayedColumns = computed(() => {
79
+ if (!props.enabledColumns) {
80
+ return visibleColumns.value;
81
+ }
82
+ const enabledKeySet = new Set(props.enabledColumns);
83
+ return orderedColumns.value.filter((column) => enabledKeySet.has(column.key));
84
+ });
82
85
  </script>
83
86
 
84
87
  <template>
85
88
  <div :class="mergedClassNames.root">
86
- <div v-if="props.showToolbar" :class="mergedClassNames.toolbar">
87
- <h2 :class="mergedClassNames.toolbarTitle">{{ props.title }}</h2>
88
-
89
- <div :class="mergedClassNames.toolbarActions">
90
- <button
91
- type="button"
92
- :class="mergedClassNames.toolbarButton"
93
- @click="isColumnManagerOpen = !isColumnManagerOpen"
94
- >
95
- Columns
96
- </button>
97
- <button
98
- type="button"
99
- :class="mergedClassNames.toolbarButton"
100
- @click="clearAllFilters"
101
- >
102
- Clear Filters
103
- </button>
104
- <button
105
- type="button"
106
- :class="mergedClassNames.toolbarButton"
107
- @click="resetColumns"
108
- >
109
- Reset Columns
110
- </button>
111
-
112
- <div
113
- v-if="isColumnManagerOpen"
114
- :class="mergedClassNames.columnManager"
115
- >
116
- <p :class="mergedClassNames.columnManagerTitle">Enable Columns</p>
117
- <label
118
- v-for="column in orderedColumns"
119
- :key="`manager-${column.key}`"
120
- :class="mergedClassNames.columnManagerItem"
121
- >
122
- <input
123
- type="checkbox"
124
- :checked="enabledColumnKeys.includes(column.key)"
125
- @change="toggleColumn(column.key)"
126
- >
127
- <span>{{ column.label }}</span>
128
- </label>
129
- </div>
130
- </div>
131
- </div>
132
-
133
89
  <div :class="mergedClassNames.tableWrapper">
134
90
  <table :class="mergedClassNames.table">
135
91
  <thead :class="mergedClassNames.tableHead">
136
92
  <tr>
137
93
  <NuxtTableHeaderCell
138
- v-for="column in visibleColumns"
94
+ v-for="column in displayedColumns"
139
95
  :key="column.key"
140
96
  :column="column"
141
97
  :filter-value="filters[column.key]"
@@ -165,7 +121,7 @@ const {
165
121
  :class="mergedClassNames.bodyRow"
166
122
  >
167
123
  <NuxtTableBodyCell
168
- v-for="column in visibleColumns"
124
+ v-for="column in displayedColumns"
169
125
  :key="`${resolveRowKey(row, rowIndex)}-${column.key}`"
170
126
  :row="row"
171
127
  :row-key="resolveRowKey(row, rowIndex)"
@@ -177,7 +133,7 @@ const {
177
133
  </tr>
178
134
  <tr v-if="sortedRows.length === 0">
179
135
  <td
180
- :colspan="Math.max(visibleColumns.length, 1)"
136
+ :colspan="Math.max(displayedColumns.length, 1)"
181
137
  :class="mergedClassNames.emptyCell"
182
138
  >
183
139
  No rows match the current filters.
@@ -1,7 +1,8 @@
1
- import type { NuxtTableClassNames, NuxtTableColumn, NuxtTableColumnOrderChange, TableRow } from '../types/table.js';
1
+ import type { NuxtTableClassNames, NuxtTableColumn, NuxtTableColumnOrderChange, TableRow } from "../types/table.js";
2
2
  type __VLS_Props = {
3
3
  columns: NuxtTableColumn[];
4
4
  rows: TableRow[];
5
+ enabledColumns?: string[];
5
6
  storageKey?: string;
6
7
  rowKey?: string | ((row: TableRow, index: number) => string | number);
7
8
  title?: string;
@@ -1,10 +1,10 @@
1
- import type { ComponentPublicInstance } from 'vue';
2
- import type { NuxtTableClassNames, NuxtTableColumn } from '../types/table.js';
1
+ import type { ComponentPublicInstance } from "vue";
2
+ import type { NuxtTableClassNames, NuxtTableColumn } from "../types/table.js";
3
3
  type __VLS_Props = {
4
4
  column: NuxtTableColumn;
5
5
  filterValue: unknown;
6
6
  columnStyle: Record<string, string | undefined>;
7
- sortDirection: 'asc' | 'desc' | null;
7
+ sortDirection: "asc" | "desc" | null;
8
8
  isDragSource: boolean;
9
9
  isDragOver: boolean;
10
10
  isDndEnabled: boolean;
@@ -21,26 +21,58 @@ const emit = defineEmits(["dragStart", "dragOver", "dragLeave", "drop", "dragEnd
21
21
  :class="[
22
22
  props.classNames.headerCell,
23
23
  props.column.headerClassName,
24
+ props.isDndEnabled ? 'nuxt-table__header-cell--dnd' : '',
25
+ props.isDragSource ? 'nuxt-table__header-cell--dragging' : '',
24
26
  props.isDragSource ? props.classNames.headerCellDragSource : '',
25
27
  props.isDragOver && !props.isDragSource ? props.classNames.headerCellDragOver : ''
26
28
  ]"
27
29
  :draggable="props.isDndEnabled"
28
- @dragstart="props.isDndEnabled ? emit('dragStart', props.column.key) : void 0"
29
- @dragover.prevent="props.isDndEnabled ? emit('dragOver', props.column.key) : void 0"
30
- @dragenter.prevent="props.isDndEnabled ? emit('dragOver', props.column.key) : void 0"
31
- @dragleave="props.isDndEnabled ? emit('dragLeave', props.column.key) : void 0"
30
+ @dragstart="
31
+ props.isDndEnabled ? emit('dragStart', props.column.key) : void 0
32
+ "
33
+ @dragover.prevent="
34
+ props.isDndEnabled ? emit('dragOver', props.column.key) : void 0
35
+ "
36
+ @dragenter.prevent="
37
+ props.isDndEnabled ? emit('dragOver', props.column.key) : void 0
38
+ "
39
+ @dragleave="
40
+ props.isDndEnabled ? emit('dragLeave', props.column.key) : void 0
41
+ "
32
42
  @drop="props.isDndEnabled ? emit('drop', props.column.key) : void 0"
33
43
  @dragend="props.isDndEnabled ? emit('dragEnd') : void 0"
34
44
  >
35
45
  <div :class="props.classNames.headerTop">
36
- <span :class="props.classNames.headerLabel">{{ props.column.label }}</span>
46
+ <span :class="props.classNames.headerLabel">{{
47
+ props.column.label
48
+ }}</span>
37
49
  <button
38
50
  v-if="props.column.sortable"
39
51
  type="button"
40
52
  :class="props.classNames.sortButton"
41
53
  @click="emit('toggleSort', props.column)"
42
54
  >
43
- <span v-if="props.sortDirection === 'asc'">Asc</span>
55
+ <component
56
+ :is="props.column.sortAscComponent"
57
+ v-if="props.sortDirection === 'asc' && props.column.sortAscComponent"
58
+ :column="props.column"
59
+ :sort-direction="props.sortDirection"
60
+ />
61
+ <component
62
+ :is="props.column.sortDescComponent"
63
+ v-else-if="
64
+ props.sortDirection === 'desc' && props.column.sortDescComponent
65
+ "
66
+ :column="props.column"
67
+ :sort-direction="props.sortDirection"
68
+ />
69
+ <component
70
+ :is="props.column.sortDefaultComponent"
71
+ v-else-if="!props.sortDirection && props.column.sortDefaultComponent"
72
+ :column="props.column"
73
+ :sort-direction="props.sortDirection"
74
+ />
75
+ <span v-else-if="props.sortDirection === 'asc'">Asc</span>
44
76
  <span v-else-if="props.sortDirection === 'desc'">Desc</span>
45
77
  <span v-else>Sort</span>
46
78
  </button>
@@ -59,8 +91,14 @@ const emit = defineEmits(["dragStart", "dragOver", "dragLeave", "drop", "dragEnd
59
91
  type="text"
60
92
  :class="props.classNames.filterInput"
61
93
  :placeholder="`Filter ${props.column.label}`"
62
- @input="emit('setFilter', props.column.key, $event.target.value)"
63
- >
94
+ @input="
95
+ emit(
96
+ 'setFilter',
97
+ props.column.key,
98
+ $event.target.value
99
+ )
100
+ "
101
+ />
64
102
 
65
103
  <div
66
104
  v-if="props.isResizeEnabled"
@@ -1,10 +1,10 @@
1
- import type { ComponentPublicInstance } from 'vue';
2
- import type { NuxtTableClassNames, NuxtTableColumn } from '../types/table.js';
1
+ import type { ComponentPublicInstance } from "vue";
2
+ import type { NuxtTableClassNames, NuxtTableColumn } from "../types/table.js";
3
3
  type __VLS_Props = {
4
4
  column: NuxtTableColumn;
5
5
  filterValue: unknown;
6
6
  columnStyle: Record<string, string | undefined>;
7
- sortDirection: 'asc' | 'desc' | null;
7
+ sortDirection: "asc" | "desc" | null;
8
8
  isDragSource: boolean;
9
9
  isDragOver: boolean;
10
10
  isDndEnabled: boolean;
@@ -1,20 +1,17 @@
1
- import type { ComponentPublicInstance } from 'vue';
2
- import type { NuxtTableColumn, TableRow, UseNuxtTableOptions } from '../types/table.js';
1
+ import type { ComponentPublicInstance } from "vue";
2
+ import type { NuxtTableColumn, TableRow, UseNuxtTableOptions } from "../types/table.js";
3
3
  export declare function useNuxtTable(options: UseNuxtTableOptions): {
4
4
  orderedColumns: import("vue").ComputedRef<NuxtTableColumn[]>;
5
5
  visibleColumns: import("vue").ComputedRef<NuxtTableColumn[]>;
6
6
  sortedRows: import("vue").ComputedRef<TableRow[]>;
7
7
  filters: import("vue").Ref<Record<string, unknown>, Record<string, unknown>>;
8
- isColumnManagerOpen: import("vue").Ref<boolean, boolean>;
9
8
  enabledColumnKeys: import("vue").Ref<string[], string[]>;
10
9
  dragSourceColumnKey: import("vue").Ref<string | null, string | null>;
11
10
  dragOverColumnKey: import("vue").Ref<string | null, string | null>;
12
11
  getSortDirection: (columnKey: string) => "asc" | "desc" | null;
13
12
  toggleSort: (column: NuxtTableColumn) => void;
14
13
  setFilter: (columnKey: string, value: unknown) => void;
15
- clearAllFilters: () => void;
16
14
  toggleColumn: (columnKey: string) => void;
17
- resetColumns: () => void;
18
15
  onHeaderDragStart: (columnKey: string) => void;
19
16
  onHeaderDragOver: (columnKey: string) => void;
20
17
  onHeaderDragLeave: (columnKey: string) => void;
@@ -1,18 +1,28 @@
1
- import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
1
+ import {
2
+ computed,
3
+ nextTick,
4
+ onBeforeUnmount,
5
+ onMounted,
6
+ ref,
7
+ watch
8
+ } from "vue";
2
9
  const MIN_COLUMN_WIDTH = 140;
3
10
  export function useNuxtTable(options) {
4
11
  const columnOrder = ref([]);
5
12
  const enabledColumnKeys = ref([]);
6
- const sortState = ref(null);
13
+ const sortState = ref(
14
+ null
15
+ );
7
16
  const filters = ref({});
8
- const isColumnManagerOpen = ref(false);
9
17
  const dragSourceColumnKey = ref(null);
10
18
  const dragOverColumnKey = ref(null);
11
19
  const hasLoadedPersistence = ref(false);
12
20
  const headerElements = ref({});
13
21
  const columnWidths = ref({});
14
22
  const activeResize = ref(null);
15
- const availableColumnKeys = computed(() => options.columns.value.map((column) => column.key));
23
+ const availableColumnKeys = computed(
24
+ () => options.columns.value.map((column) => column.key)
25
+ );
16
26
  const columnsByKey = computed(() => {
17
27
  return new Map(options.columns.value.map((column) => [column.key, column]));
18
28
  });
@@ -20,7 +30,9 @@ export function useNuxtTable(options) {
20
30
  return columnOrder.value.map((columnKey) => columnsByKey.value.get(columnKey)).filter((column) => Boolean(column));
21
31
  });
22
32
  const visibleColumns = computed(() => {
23
- return orderedColumns.value.filter((column) => enabledColumnKeys.value.includes(column.key));
33
+ return orderedColumns.value.filter(
34
+ (column) => enabledColumnKeys.value.includes(column.key)
35
+ );
24
36
  });
25
37
  const filteredRows = computed(() => {
26
38
  return options.rows.value.filter((row) => {
@@ -32,7 +44,10 @@ export function useNuxtTable(options) {
32
44
  if (column.filterFn) {
33
45
  return column.filterFn(row, filterValue, column);
34
46
  }
35
- const candidate = resolveColumnValue(row, column.filterKey ?? column.key);
47
+ const candidate = resolveColumnValue(
48
+ row,
49
+ column.filterKey ?? column.key
50
+ );
36
51
  const candidateText = String(candidate ?? "").toLowerCase();
37
52
  const filterText = String(filterValue ?? "").toLowerCase();
38
53
  return candidateText.includes(filterText);
@@ -73,9 +88,18 @@ export function useNuxtTable(options) {
73
88
  if (!hasLoadedPersistence.value || !import.meta.client) {
74
89
  return;
75
90
  }
76
- localStorage.setItem(buildStorageKey("order"), JSON.stringify(columnOrder.value));
77
- localStorage.setItem(buildStorageKey("enabledColumns"), JSON.stringify(enabledColumnKeys.value));
78
- localStorage.setItem(buildStorageKey("widths"), JSON.stringify(columnWidths.value));
91
+ localStorage.setItem(
92
+ buildStorageKey("order"),
93
+ JSON.stringify(columnOrder.value)
94
+ );
95
+ localStorage.setItem(
96
+ buildStorageKey("enabledColumns"),
97
+ JSON.stringify(enabledColumnKeys.value)
98
+ );
99
+ localStorage.setItem(
100
+ buildStorageKey("widths"),
101
+ JSON.stringify(columnWidths.value)
102
+ );
79
103
  },
80
104
  { deep: true }
81
105
  );
@@ -88,7 +112,9 @@ export function useNuxtTable(options) {
88
112
  columnOrder.value = [...currentKeys];
89
113
  } else {
90
114
  const currentKeySet = new Set(currentKeys);
91
- const keptKeys = columnOrder.value.filter((key) => currentKeySet.has(key));
115
+ const keptKeys = columnOrder.value.filter(
116
+ (key) => currentKeySet.has(key)
117
+ );
92
118
  const newKeys = currentKeys.filter((key) => !keptKeys.includes(key));
93
119
  columnOrder.value = [...keptKeys, ...newKeys];
94
120
  }
@@ -96,8 +122,12 @@ export function useNuxtTable(options) {
96
122
  enabledColumnKeys.value = [...currentKeys];
97
123
  } else {
98
124
  const currentKeySet = new Set(currentKeys);
99
- const keptEnabledKeys = enabledColumnKeys.value.filter((key) => currentKeySet.has(key));
100
- const missingEnabledKeys = currentKeys.filter((key) => !keptEnabledKeys.includes(key));
125
+ const keptEnabledKeys = enabledColumnKeys.value.filter(
126
+ (key) => currentKeySet.has(key)
127
+ );
128
+ const missingEnabledKeys = currentKeys.filter(
129
+ (key) => !keptEnabledKeys.includes(key)
130
+ );
101
131
  enabledColumnKeys.value = [...keptEnabledKeys, ...missingEnabledKeys];
102
132
  }
103
133
  const nextFilters = {};
@@ -123,20 +153,28 @@ export function useNuxtTable(options) {
123
153
  if (persistedOrder) {
124
154
  const parsedOrder = JSON.parse(persistedOrder);
125
155
  if (Array.isArray(parsedOrder)) {
126
- const validPersistedOrder = parsedOrder.filter((key) => {
127
- return typeof key === "string" && availableColumnKeys.value.includes(key);
128
- });
129
- const missingKeys = availableColumnKeys.value.filter((key) => !validPersistedOrder.includes(key));
156
+ const validPersistedOrder = parsedOrder.filter(
157
+ (key) => {
158
+ return typeof key === "string" && availableColumnKeys.value.includes(key);
159
+ }
160
+ );
161
+ const missingKeys = availableColumnKeys.value.filter(
162
+ (key) => !validPersistedOrder.includes(key)
163
+ );
130
164
  columnOrder.value = [...validPersistedOrder, ...missingKeys];
131
165
  }
132
166
  }
133
- const persistedEnabledColumns = localStorage.getItem(buildStorageKey("enabledColumns"));
167
+ const persistedEnabledColumns = localStorage.getItem(
168
+ buildStorageKey("enabledColumns")
169
+ );
134
170
  if (persistedEnabledColumns) {
135
171
  const parsedEnabledColumns = JSON.parse(persistedEnabledColumns);
136
172
  if (Array.isArray(parsedEnabledColumns)) {
137
- enabledColumnKeys.value = parsedEnabledColumns.filter((key) => {
138
- return typeof key === "string" && availableColumnKeys.value.includes(key);
139
- });
173
+ enabledColumnKeys.value = parsedEnabledColumns.filter(
174
+ (key) => {
175
+ return typeof key === "string" && availableColumnKeys.value.includes(key);
176
+ }
177
+ );
140
178
  }
141
179
  }
142
180
  const persistedWidths = localStorage.getItem(buildStorageKey("widths"));
@@ -185,27 +223,18 @@ export function useNuxtTable(options) {
185
223
  function setFilter(columnKey, value) {
186
224
  filters.value[columnKey] = value;
187
225
  }
188
- function clearAllFilters() {
189
- const nextFilters = {};
190
- for (const key of availableColumnKeys.value) {
191
- nextFilters[key] = "";
192
- }
193
- filters.value = nextFilters;
194
- }
195
226
  function toggleColumn(columnKey) {
196
227
  if (enabledColumnKeys.value.includes(columnKey)) {
197
228
  if (enabledColumnKeys.value.length === 1) {
198
229
  return;
199
230
  }
200
- enabledColumnKeys.value = enabledColumnKeys.value.filter((key) => key !== columnKey);
231
+ enabledColumnKeys.value = enabledColumnKeys.value.filter(
232
+ (key) => key !== columnKey
233
+ );
201
234
  return;
202
235
  }
203
236
  enabledColumnKeys.value = [...enabledColumnKeys.value, columnKey];
204
237
  }
205
- function resetColumns() {
206
- columnOrder.value = [...availableColumnKeys.value];
207
- enabledColumnKeys.value = [...availableColumnKeys.value];
208
- }
209
238
  function onHeaderDragStart(columnKey) {
210
239
  if (!options.enableColumnDnd.value || activeResize.value) {
211
240
  return;
@@ -299,7 +328,10 @@ export function useNuxtTable(options) {
299
328
  return;
300
329
  }
301
330
  const delta = event.clientX - activeResize.value.startX;
302
- const nextWidth = Math.max(MIN_COLUMN_WIDTH, Math.round(activeResize.value.startWidth + delta));
331
+ const nextWidth = Math.max(
332
+ MIN_COLUMN_WIDTH,
333
+ Math.round(activeResize.value.startWidth + delta)
334
+ );
303
335
  columnWidths.value = {
304
336
  ...columnWidths.value,
305
337
  [activeResize.value.columnKey]: nextWidth
@@ -393,7 +425,9 @@ export function useNuxtTable(options) {
393
425
  return value.length > 0;
394
426
  }
395
427
  if (typeof value === "object") {
396
- return Object.values(value).some((nestedValue) => isFilterActive(nestedValue));
428
+ return Object.values(value).some(
429
+ (nestedValue) => isFilterActive(nestedValue)
430
+ );
397
431
  }
398
432
  return true;
399
433
  }
@@ -430,16 +464,13 @@ export function useNuxtTable(options) {
430
464
  visibleColumns,
431
465
  sortedRows,
432
466
  filters,
433
- isColumnManagerOpen,
434
467
  enabledColumnKeys,
435
468
  dragSourceColumnKey,
436
469
  dragOverColumnKey,
437
470
  getSortDirection,
438
471
  toggleSort,
439
472
  setFilter,
440
- clearAllFilters,
441
473
  toggleColumn,
442
- resetColumns,
443
474
  onHeaderDragStart,
444
475
  onHeaderDragOver,
445
476
  onHeaderDragLeave,
@@ -1,4 +1,4 @@
1
- import type { Component, Ref } from 'vue';
1
+ import type { Component, Ref } from "vue";
2
2
  export type TableRow = Record<string, any>;
3
3
  export type ValueResolver = string | ((row: TableRow) => unknown);
4
4
  export interface NuxtTableColumn {
@@ -6,6 +6,9 @@ export interface NuxtTableColumn {
6
6
  label: string;
7
7
  sortable?: boolean;
8
8
  filterable?: boolean;
9
+ sortAscComponent?: Component;
10
+ sortDescComponent?: Component;
11
+ sortDefaultComponent?: Component;
9
12
  sortKey?: ValueResolver;
10
13
  filterKey?: ValueResolver;
11
14
  formatter?: (value: unknown, row: TableRow) => string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serhiitupilow/nuxt-table",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
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",