@veristone/nuxt-v-app 0.2.2 → 0.2.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/app/components/V/A/Crud/Delete.vue +1 -0
- package/app/components/V/A/CrudTable/index.vue +486 -0
- package/app/components/V/A/Table/ActionColumn.vue +133 -0
- package/app/components/V/A/Table/Actions.vue +79 -0
- package/app/components/V/A/Table/CellRenderer.vue +198 -0
- package/app/components/V/A/Table/ColumnToggle.vue +131 -0
- package/app/components/V/A/Table/EditableCell.vue +176 -0
- package/app/components/V/A/Table/Export.vue +154 -0
- package/app/components/V/A/Table/FilterBar.vue +140 -0
- package/app/components/V/A/Table/FilterChips.vue +107 -0
- package/app/components/V/A/Table/README.md +380 -0
- package/app/components/V/A/Table/Toolbar.vue +163 -0
- package/app/components/V/A/Table/index.vue +483 -0
- package/app/composables/useDataTable.js +169 -0
- package/app/composables/useXATableColumns.ts +279 -386
- package/app/pages/playground/tables.vue +182 -553
- package/app/types/table.ts +52 -0
- package/package.json +4 -2
- package/app/components/V/A/Table.vue +0 -674
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
3
|
+
<!-- Search Input -->
|
|
4
|
+
<UInput
|
|
5
|
+
v-if="showSearch"
|
|
6
|
+
v-model="searchQuery"
|
|
7
|
+
:placeholder="searchPlaceholder"
|
|
8
|
+
icon="i-lucide-search"
|
|
9
|
+
size="sm"
|
|
10
|
+
class="w-64"
|
|
11
|
+
@update:model-value="onSearchChange"
|
|
12
|
+
/>
|
|
13
|
+
|
|
14
|
+
<!-- Filter Dropdowns -->
|
|
15
|
+
<template v-for="filter in filters" :key="filter.key">
|
|
16
|
+
<USelectMenu
|
|
17
|
+
v-if="filter.type === 'select'"
|
|
18
|
+
v-model="filterValues[filter.key]"
|
|
19
|
+
:items="filter.options || []"
|
|
20
|
+
:placeholder="filter.placeholder || filter.label"
|
|
21
|
+
size="sm"
|
|
22
|
+
class="w-40"
|
|
23
|
+
@update:model-value="onFilterChange(filter.key, $event)"
|
|
24
|
+
/>
|
|
25
|
+
|
|
26
|
+
<UInput
|
|
27
|
+
v-else-if="filter.type === 'text'"
|
|
28
|
+
v-model="filterValues[filter.key]"
|
|
29
|
+
:placeholder="filter.placeholder || filter.label"
|
|
30
|
+
size="sm"
|
|
31
|
+
class="w-40"
|
|
32
|
+
@update:model-value="onFilterChange(filter.key, $event)"
|
|
33
|
+
/>
|
|
34
|
+
|
|
35
|
+
<UInput
|
|
36
|
+
v-else-if="filter.type === 'date'"
|
|
37
|
+
v-model="filterValues[filter.key]"
|
|
38
|
+
type="date"
|
|
39
|
+
:placeholder="filter.placeholder || filter.label"
|
|
40
|
+
size="sm"
|
|
41
|
+
class="w-40"
|
|
42
|
+
@update:model-value="onFilterChange(filter.key, $event)"
|
|
43
|
+
/>
|
|
44
|
+
|
|
45
|
+
<UInput
|
|
46
|
+
v-else-if="filter.type === 'number'"
|
|
47
|
+
v-model="filterValues[filter.key]"
|
|
48
|
+
type="number"
|
|
49
|
+
:placeholder="filter.placeholder || filter.label"
|
|
50
|
+
size="sm"
|
|
51
|
+
class="w-32"
|
|
52
|
+
@update:model-value="onFilterChange(filter.key, $event)"
|
|
53
|
+
/>
|
|
54
|
+
</template>
|
|
55
|
+
|
|
56
|
+
<!-- Clear Filters Button -->
|
|
57
|
+
<UButton
|
|
58
|
+
v-if="hasActiveFilters"
|
|
59
|
+
label="Clear"
|
|
60
|
+
color="neutral"
|
|
61
|
+
variant="ghost"
|
|
62
|
+
size="sm"
|
|
63
|
+
icon="i-lucide-x"
|
|
64
|
+
@click="clearFilters"
|
|
65
|
+
/>
|
|
66
|
+
|
|
67
|
+
<!-- Additional actions slot -->
|
|
68
|
+
<slot name="actions" />
|
|
69
|
+
</div>
|
|
70
|
+
</template>
|
|
71
|
+
|
|
72
|
+
<script setup>
|
|
73
|
+
defineOptions({
|
|
74
|
+
name: 'VATableFilterBar'
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
const props = defineProps({
|
|
78
|
+
filters: {
|
|
79
|
+
type: Array,
|
|
80
|
+
default: () => []
|
|
81
|
+
},
|
|
82
|
+
showSearch: {
|
|
83
|
+
type: Boolean,
|
|
84
|
+
default: true
|
|
85
|
+
},
|
|
86
|
+
searchPlaceholder: {
|
|
87
|
+
type: String,
|
|
88
|
+
default: 'Search...'
|
|
89
|
+
},
|
|
90
|
+
debounce: {
|
|
91
|
+
type: Number,
|
|
92
|
+
default: 300
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
const emit = defineEmits(['update:search', 'update:filters', 'filter', 'clear'])
|
|
97
|
+
|
|
98
|
+
// Search state
|
|
99
|
+
const searchQuery = ref('')
|
|
100
|
+
|
|
101
|
+
// Filter values
|
|
102
|
+
const filterValues = ref({})
|
|
103
|
+
|
|
104
|
+
// Debounced search
|
|
105
|
+
let searchTimeout = null
|
|
106
|
+
|
|
107
|
+
function onSearchChange(value) {
|
|
108
|
+
if (searchTimeout) clearTimeout(searchTimeout)
|
|
109
|
+
|
|
110
|
+
searchTimeout = setTimeout(() => {
|
|
111
|
+
emit('update:search', value)
|
|
112
|
+
}, props.debounce)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function onFilterChange(key, value) {
|
|
116
|
+
emit('filter', key, value)
|
|
117
|
+
emit('update:filters', { ...filterValues.value })
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function clearFilters() {
|
|
121
|
+
searchQuery.value = ''
|
|
122
|
+
filterValues.value = {}
|
|
123
|
+
emit('update:search', '')
|
|
124
|
+
emit('update:filters', {})
|
|
125
|
+
emit('clear')
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Check if any filters are active
|
|
129
|
+
const hasActiveFilters = computed(() => {
|
|
130
|
+
if (searchQuery.value) return true
|
|
131
|
+
return Object.values(filterValues.value).some(v => v !== undefined && v !== '' && v !== null)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
// Expose for parent access
|
|
135
|
+
defineExpose({
|
|
136
|
+
searchQuery,
|
|
137
|
+
filterValues,
|
|
138
|
+
clearFilters
|
|
139
|
+
})
|
|
140
|
+
</script>
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div v-if="activeFilters.length > 0" class="flex flex-wrap items-center gap-2">
|
|
3
|
+
<span class="text-sm text-neutral-500">Filters:</span>
|
|
4
|
+
|
|
5
|
+
<TransitionGroup name="chip">
|
|
6
|
+
<div
|
|
7
|
+
v-for="filter in activeFilters"
|
|
8
|
+
:key="filter.key"
|
|
9
|
+
class="inline-flex items-center gap-1 px-2 py-1 text-sm bg-neutral-100 dark:bg-neutral-800 rounded-full"
|
|
10
|
+
>
|
|
11
|
+
<span class="text-neutral-600 dark:text-neutral-400">{{ filter.label }}:</span>
|
|
12
|
+
<span class="font-medium text-neutral-900 dark:text-white">{{ formatValue(filter.value) }}</span>
|
|
13
|
+
<button
|
|
14
|
+
type="button"
|
|
15
|
+
class="ml-1 p-0.5 rounded-full hover:bg-neutral-200 dark:hover:bg-neutral-700 transition-colors"
|
|
16
|
+
@click="removeFilter(filter.key)"
|
|
17
|
+
>
|
|
18
|
+
<UIcon name="i-lucide-x" class="w-3 h-3 text-neutral-500" />
|
|
19
|
+
</button>
|
|
20
|
+
</div>
|
|
21
|
+
</TransitionGroup>
|
|
22
|
+
|
|
23
|
+
<UButton
|
|
24
|
+
v-if="activeFilters.length > 1"
|
|
25
|
+
label="Clear all"
|
|
26
|
+
color="neutral"
|
|
27
|
+
variant="link"
|
|
28
|
+
size="xs"
|
|
29
|
+
@click="clearAll"
|
|
30
|
+
/>
|
|
31
|
+
</div>
|
|
32
|
+
</template>
|
|
33
|
+
|
|
34
|
+
<script setup>
|
|
35
|
+
defineOptions({
|
|
36
|
+
name: 'VATableFilterChips'
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const props = defineProps({
|
|
40
|
+
filters: {
|
|
41
|
+
type: Object,
|
|
42
|
+
required: true
|
|
43
|
+
},
|
|
44
|
+
labels: {
|
|
45
|
+
type: Object,
|
|
46
|
+
default: () => ({})
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const emit = defineEmits(['remove', 'clear'])
|
|
51
|
+
|
|
52
|
+
// Convert filters object to array with labels
|
|
53
|
+
const activeFilters = computed(() => {
|
|
54
|
+
return Object.entries(props.filters)
|
|
55
|
+
.filter(([_, value]) => value !== undefined && value !== '' && value !== null)
|
|
56
|
+
.map(([key, value]) => ({
|
|
57
|
+
key,
|
|
58
|
+
label: props.labels[key] || formatLabel(key),
|
|
59
|
+
value
|
|
60
|
+
}))
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
// Format camelCase/snake_case to readable label
|
|
64
|
+
function formatLabel(key) {
|
|
65
|
+
return key
|
|
66
|
+
.replace(/([A-Z])/g, ' $1')
|
|
67
|
+
.replace(/[_-]/g, ' ')
|
|
68
|
+
.replace(/^\s/, '')
|
|
69
|
+
.split(' ')
|
|
70
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
71
|
+
.join(' ')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Format value for display
|
|
75
|
+
function formatValue(value) {
|
|
76
|
+
if (value === true) return 'Yes'
|
|
77
|
+
if (value === false) return 'No'
|
|
78
|
+
if (value instanceof Date) return value.toLocaleDateString()
|
|
79
|
+
if (Array.isArray(value)) return value.join(', ')
|
|
80
|
+
return String(value)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function removeFilter(key) {
|
|
84
|
+
emit('remove', key)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function clearAll() {
|
|
88
|
+
emit('clear')
|
|
89
|
+
}
|
|
90
|
+
</script>
|
|
91
|
+
|
|
92
|
+
<style scoped>
|
|
93
|
+
.chip-enter-active,
|
|
94
|
+
.chip-leave-active {
|
|
95
|
+
transition: all 0.2s ease;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.chip-enter-from {
|
|
99
|
+
opacity: 0;
|
|
100
|
+
transform: scale(0.8);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.chip-leave-to {
|
|
104
|
+
opacity: 0;
|
|
105
|
+
transform: scale(0.8);
|
|
106
|
+
}
|
|
107
|
+
</style>
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
# VATable Component System
|
|
2
|
+
|
|
3
|
+
TanStack-powered data table components for Nuxt 4 with built-in search, sorting, filtering, pagination, selection, and preset-based cell rendering.
|
|
4
|
+
|
|
5
|
+
## Components
|
|
6
|
+
|
|
7
|
+
### `<VATable>` — Main Table
|
|
8
|
+
|
|
9
|
+
The primary table component. Wraps Nuxt UI's `<UTable>` with a toolbar, filter chips, pagination footer, and automatic column processing.
|
|
10
|
+
|
|
11
|
+
```vue
|
|
12
|
+
<VATable
|
|
13
|
+
name="Users"
|
|
14
|
+
:data="users"
|
|
15
|
+
:columns="columns"
|
|
16
|
+
:loading="loading"
|
|
17
|
+
:filters="filterDefs"
|
|
18
|
+
:selectable="true"
|
|
19
|
+
:on-refresh="refresh"
|
|
20
|
+
:on-row-click="handleClick"
|
|
21
|
+
default-sort="name"
|
|
22
|
+
:default-sort-desc="false"
|
|
23
|
+
:initial-page-limit="20"
|
|
24
|
+
/>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
#### Props
|
|
28
|
+
|
|
29
|
+
| Prop | Type | Default | Description |
|
|
30
|
+
|------|------|---------|-------------|
|
|
31
|
+
| `name` | `string` | — | Table title shown in the card header |
|
|
32
|
+
| `data` | `unknown[]` | **required** | Array of row data |
|
|
33
|
+
| `columns` | `TableColumn[]` | **required** | TanStack column definitions |
|
|
34
|
+
| `loading` | `boolean` | `false` | Shows loading skeleton on initial load |
|
|
35
|
+
| `initialPageLimit` | `number` | `10` | Rows per page |
|
|
36
|
+
| `selectable` | `boolean` | `false` | Enable row selection checkboxes |
|
|
37
|
+
| `filters` | `FilterDefinition[]` | — | Filter dropdown definitions |
|
|
38
|
+
| `onRefresh` | `() => void \| Promise<void>` | — | Shows refresh button; called on click |
|
|
39
|
+
| `onRowClick` | `(row: unknown) => void` | — | Called when a row is clicked |
|
|
40
|
+
| `defaultSort` | `string` | — | Column accessorKey to sort by on mount |
|
|
41
|
+
| `defaultSortDesc` | `boolean` | `false` | Sort descending by default |
|
|
42
|
+
|
|
43
|
+
#### v-model Bindings
|
|
44
|
+
|
|
45
|
+
| Model | Type | Description |
|
|
46
|
+
|-------|------|-------------|
|
|
47
|
+
| `sorting` | `SortingState` | TanStack sorting state |
|
|
48
|
+
| `filterValues` | `Record<string, unknown>` | Active filter values |
|
|
49
|
+
|
|
50
|
+
#### Slots
|
|
51
|
+
|
|
52
|
+
| Slot | Props | Description |
|
|
53
|
+
|------|-------|-------------|
|
|
54
|
+
| `header-right` | — | Content placed right of the table name |
|
|
55
|
+
| `bulk-actions` | `{ selected, count, clear }` | Shown when rows are selected |
|
|
56
|
+
| `actions-cell` | `{ row }` | Custom actions column (auto-added) |
|
|
57
|
+
| `[column]-cell` | `{ row }` | Custom cell for a specific column |
|
|
58
|
+
| `cell-[column]` | `{ row }` | Alias for above (auto-mapped) |
|
|
59
|
+
|
|
60
|
+
#### Exposed
|
|
61
|
+
|
|
62
|
+
| Property | Type | Description |
|
|
63
|
+
|----------|------|-------------|
|
|
64
|
+
| `selectedRows` | `computed<any[]>` | Currently selected row data |
|
|
65
|
+
| `rowSelection` | `ref<Record<string, boolean>>` | Selection state |
|
|
66
|
+
| `clearSelection` | `() => void` | Clears all selections |
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
### `<VACrudTable>` — CRUD-Integrated Table
|
|
71
|
+
|
|
72
|
+
Full CRUD table that integrates with `useXCrud` composable. Provides toolbar, search, column toggle, export, pagination, and delete confirmation.
|
|
73
|
+
|
|
74
|
+
```vue
|
|
75
|
+
<VACrudTable
|
|
76
|
+
endpoint="/api/users"
|
|
77
|
+
:columns="columns"
|
|
78
|
+
:config="{
|
|
79
|
+
toolbar: { search: true, export: true },
|
|
80
|
+
actions: { create: true, view: true, edit: true, delete: true },
|
|
81
|
+
pagination: { enabled: true, pageSize: 20 }
|
|
82
|
+
}"
|
|
83
|
+
@create="openCreate"
|
|
84
|
+
@view="openView"
|
|
85
|
+
@edit="openEdit"
|
|
86
|
+
@delete="onDeleted"
|
|
87
|
+
/>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
#### Props
|
|
91
|
+
|
|
92
|
+
| Prop | Type | Default | Description |
|
|
93
|
+
|------|------|---------|-------------|
|
|
94
|
+
| `endpoint` | `string` | — | API endpoint for useXCrud |
|
|
95
|
+
| `columns` | `Array` | **required** | Column definitions |
|
|
96
|
+
| `rowKey` | `string` | `'id'` | Row identifier key |
|
|
97
|
+
| `crud` | `Object` | — | Inject existing crud instance |
|
|
98
|
+
| `config` | `Object` | see below | Grouped configuration |
|
|
99
|
+
| `emptyState` | `Object` | `{ title: 'No data' }` | Empty state config |
|
|
100
|
+
| `initialFilters` | `Object` | `{}` | Initial filter values |
|
|
101
|
+
|
|
102
|
+
**Config structure:**
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
{
|
|
106
|
+
toolbar: {
|
|
107
|
+
search: true,
|
|
108
|
+
searchPlaceholder: 'Search...',
|
|
109
|
+
columns: true, // show column toggle
|
|
110
|
+
export: true,
|
|
111
|
+
exportFilename: 'export'
|
|
112
|
+
},
|
|
113
|
+
actions: {
|
|
114
|
+
create: true,
|
|
115
|
+
createLabel: 'Create',
|
|
116
|
+
view: true,
|
|
117
|
+
edit: true,
|
|
118
|
+
delete: true,
|
|
119
|
+
viewRoute: '/users/:id', // auto-navigate
|
|
120
|
+
editRoute: '/users/:id/edit'
|
|
121
|
+
},
|
|
122
|
+
selection: { enabled: false },
|
|
123
|
+
pagination: { enabled: true, pageSize: 20 }
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
#### Events
|
|
128
|
+
|
|
129
|
+
| Event | Payload | Description |
|
|
130
|
+
|-------|---------|-------------|
|
|
131
|
+
| `create` | — | Create button clicked |
|
|
132
|
+
| `view` | `row` | View action (if no viewRoute) |
|
|
133
|
+
| `edit` | `row` | Edit action (if no editRoute) |
|
|
134
|
+
| `delete` | `row` | After successful delete |
|
|
135
|
+
| `select` | `rows[]` | Selection changed |
|
|
136
|
+
| `row-click` | `row` | Row clicked |
|
|
137
|
+
|
|
138
|
+
#### Slots
|
|
139
|
+
|
|
140
|
+
| Slot | Description |
|
|
141
|
+
|------|-------------|
|
|
142
|
+
| `toolbar-left` | Left side of toolbar |
|
|
143
|
+
| `toolbar-right` | Right side of toolbar |
|
|
144
|
+
| `toolbar-actions` | After the create button |
|
|
145
|
+
| `row-actions` | Override row action buttons |
|
|
146
|
+
| `extra-actions` | Additional action buttons per row |
|
|
147
|
+
| `empty` | Empty state content |
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Sub-Components
|
|
152
|
+
|
|
153
|
+
### `<VATableCellRenderer>`
|
|
154
|
+
|
|
155
|
+
Renders cell content based on `column.meta.preset`. Used automatically by VATable when columns have presets.
|
|
156
|
+
|
|
157
|
+
**Presets:**
|
|
158
|
+
|
|
159
|
+
| Preset | Description | Meta Options |
|
|
160
|
+
|--------|-------------|--------------|
|
|
161
|
+
| `text` | Plain text (default) | — |
|
|
162
|
+
| `email` | Clickable mailto link | — |
|
|
163
|
+
| `link` | External link with icon | — |
|
|
164
|
+
| `badge` | Colored status badge | `colorMap` |
|
|
165
|
+
| `boolean` | Check/X circle icon | — |
|
|
166
|
+
| `avatar` | Avatar image + optional name | `size`, `showName` |
|
|
167
|
+
| `date` | Formatted date | `format: 'date' \| 'time' \| 'datetime' \| 'relative'` |
|
|
168
|
+
| `currency` | Formatted currency | `currency`, `locale` |
|
|
169
|
+
| `number` | Formatted number | `decimals`, `locale` |
|
|
170
|
+
|
|
171
|
+
### `<VATableActions>`
|
|
172
|
+
|
|
173
|
+
Row action buttons with optional route navigation.
|
|
174
|
+
|
|
175
|
+
```vue
|
|
176
|
+
<VATableActions
|
|
177
|
+
:row="row"
|
|
178
|
+
:show-view="true"
|
|
179
|
+
:show-edit="true"
|
|
180
|
+
:show-delete="true"
|
|
181
|
+
view-route="/users/:id"
|
|
182
|
+
@view="onView"
|
|
183
|
+
@edit="onEdit"
|
|
184
|
+
@delete="onDelete"
|
|
185
|
+
/>
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### `<VATableActionColumn>`
|
|
189
|
+
|
|
190
|
+
Smart action column with primary actions + overflow dropdown menu.
|
|
191
|
+
|
|
192
|
+
```vue
|
|
193
|
+
<VATableActionColumn
|
|
194
|
+
:row="row"
|
|
195
|
+
:show-view="true"
|
|
196
|
+
:show-edit="true"
|
|
197
|
+
:show-delete="true"
|
|
198
|
+
:max-visible="2"
|
|
199
|
+
:actions="[
|
|
200
|
+
{ name: 'archive', label: 'Archive', icon: 'i-lucide-archive' }
|
|
201
|
+
]"
|
|
202
|
+
/>
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### `<VATableToolbar>`
|
|
206
|
+
|
|
207
|
+
Standalone toolbar with search, filter chips, column toggle, and export. Used by VACrudTable.
|
|
208
|
+
|
|
209
|
+
### `<VATableFilterBar>`
|
|
210
|
+
|
|
211
|
+
Filter input row with support for `select`, `text`, `date`, and `number` filter types.
|
|
212
|
+
|
|
213
|
+
### `<VATableFilterChips>`
|
|
214
|
+
|
|
215
|
+
Displays active filters as animated chips with remove/clear buttons.
|
|
216
|
+
|
|
217
|
+
### `<VATableColumnToggle>`
|
|
218
|
+
|
|
219
|
+
Popover with per-column visibility checkboxes, show all, and reset.
|
|
220
|
+
|
|
221
|
+
### `<VATableExport>`
|
|
222
|
+
|
|
223
|
+
Dropdown button for CSV/JSON export with nested path and custom formatter support.
|
|
224
|
+
|
|
225
|
+
### `<VATableEditableCell>`
|
|
226
|
+
|
|
227
|
+
Inline editing triggered by double-click. Supports text, email, number, textarea, select, and boolean input types.
|
|
228
|
+
|
|
229
|
+
```vue
|
|
230
|
+
<VATableEditableCell
|
|
231
|
+
:value="row.name"
|
|
232
|
+
type="text"
|
|
233
|
+
:editable="true"
|
|
234
|
+
@save="(newVal, oldVal) => updateField(row.id, 'name', newVal)"
|
|
235
|
+
/>
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## Composables
|
|
241
|
+
|
|
242
|
+
### `useXATableColumns()`
|
|
243
|
+
|
|
244
|
+
Preset-based column factory. Returns column definitions with `meta.preset` that CellRenderer auto-renders.
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
const { presets } = useXATableColumns()
|
|
248
|
+
|
|
249
|
+
const columns = [
|
|
250
|
+
presets.text('name', 'Name'),
|
|
251
|
+
presets.email('email'),
|
|
252
|
+
presets.badge('status', 'Status', {
|
|
253
|
+
colorMap: { active: 'success', pending: 'warning', inactive: 'neutral' }
|
|
254
|
+
}),
|
|
255
|
+
presets.date('createdAt', 'Created', { format: 'relative' }),
|
|
256
|
+
presets.currency('price', 'Price', { currency: 'USD' }),
|
|
257
|
+
presets.number('quantity', 'Qty', { decimals: 0 }),
|
|
258
|
+
presets.boolean('isActive', 'Active'),
|
|
259
|
+
presets.avatar('avatar', 'Photo', { size: 'sm', showName: true }),
|
|
260
|
+
presets.link('website', 'Website'),
|
|
261
|
+
presets.editable('notes', 'Notes', { inputType: 'textarea' }),
|
|
262
|
+
presets.actions({ showView: true, showEdit: true, showDelete: true }),
|
|
263
|
+
presets.custom('custom', 'Custom Column'),
|
|
264
|
+
]
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### `useDataTable(options?)`
|
|
268
|
+
|
|
269
|
+
State management composable for server-side table patterns. Manages pagination, sorting, filtering, selection, and builds API query params.
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
const {
|
|
273
|
+
// Pagination
|
|
274
|
+
page, pageSize, total, totalPages, startIndex, endIndex,
|
|
275
|
+
|
|
276
|
+
// Sorting
|
|
277
|
+
sortKey, sortDirection, toggleSort, setSort, clearSort,
|
|
278
|
+
|
|
279
|
+
// Filtering
|
|
280
|
+
filters, searchQuery, activeFilters, setFilter, removeFilter, clearFilters,
|
|
281
|
+
|
|
282
|
+
// Selection
|
|
283
|
+
selectedRows, selectRow, deselectRow, toggleRowSelection,
|
|
284
|
+
selectAll, deselectAll, isRowSelected,
|
|
285
|
+
|
|
286
|
+
// API query
|
|
287
|
+
queryParams, // { page, limit, sort, search, ...filters }
|
|
288
|
+
|
|
289
|
+
// Reset
|
|
290
|
+
reset,
|
|
291
|
+
} = useDataTable({
|
|
292
|
+
defaultPageSize: 20,
|
|
293
|
+
defaultSortKey: 'createdAt',
|
|
294
|
+
defaultSortDirection: 'desc',
|
|
295
|
+
})
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## Quick Start
|
|
301
|
+
|
|
302
|
+
### Basic table
|
|
303
|
+
|
|
304
|
+
```vue
|
|
305
|
+
<script setup>
|
|
306
|
+
const { data, pending } = await useFetch('/api/users')
|
|
307
|
+
|
|
308
|
+
const columns = [
|
|
309
|
+
{ accessorKey: 'name', header: 'Name' },
|
|
310
|
+
{ accessorKey: 'email', header: 'Email' },
|
|
311
|
+
{ accessorKey: 'status', header: 'Status' },
|
|
312
|
+
]
|
|
313
|
+
</script>
|
|
314
|
+
|
|
315
|
+
<template>
|
|
316
|
+
<VATable name="Users" :data="data" :columns="columns" :loading="pending" />
|
|
317
|
+
</template>
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Preset columns with actions
|
|
321
|
+
|
|
322
|
+
```vue
|
|
323
|
+
<script setup>
|
|
324
|
+
const { presets } = useXATableColumns()
|
|
325
|
+
|
|
326
|
+
const columns = [
|
|
327
|
+
presets.text('name', 'Name'),
|
|
328
|
+
presets.email('email'),
|
|
329
|
+
presets.badge('status', 'Status'),
|
|
330
|
+
presets.date('createdAt', 'Created', { format: 'relative' }),
|
|
331
|
+
]
|
|
332
|
+
|
|
333
|
+
const { data } = await useFetch('/api/users')
|
|
334
|
+
</script>
|
|
335
|
+
|
|
336
|
+
<template>
|
|
337
|
+
<VATable name="Users" :data="data" :columns="columns">
|
|
338
|
+
<template #actions-cell="{ row }">
|
|
339
|
+
<VATableActions
|
|
340
|
+
:row="row.original"
|
|
341
|
+
view-route="/users/:id"
|
|
342
|
+
@edit="openEdit"
|
|
343
|
+
@delete="confirmDelete"
|
|
344
|
+
/>
|
|
345
|
+
</template>
|
|
346
|
+
</VATable>
|
|
347
|
+
</template>
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### CRUD table (auto-fetches data)
|
|
351
|
+
|
|
352
|
+
```vue
|
|
353
|
+
<script setup>
|
|
354
|
+
const columns = [
|
|
355
|
+
{ accessorKey: 'name', header: 'Name' },
|
|
356
|
+
{ accessorKey: 'email', header: 'Email' },
|
|
357
|
+
{ accessorKey: 'status', header: 'Status', meta: { preset: 'badge' } },
|
|
358
|
+
{ accessorKey: 'createdAt', header: 'Created' },
|
|
359
|
+
]
|
|
360
|
+
</script>
|
|
361
|
+
|
|
362
|
+
<template>
|
|
363
|
+
<VACrudTable
|
|
364
|
+
endpoint="/api/users"
|
|
365
|
+
:columns="columns"
|
|
366
|
+
:config="{
|
|
367
|
+
actions: { viewRoute: '/users/:id', editRoute: '/users/:id/edit' }
|
|
368
|
+
}"
|
|
369
|
+
@create="navigateTo('/users/new')"
|
|
370
|
+
/>
|
|
371
|
+
</template>
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
## Date Auto-Detection
|
|
377
|
+
|
|
378
|
+
VATable automatically detects date columns by accessorKey and applies relative time formatting. Recognized patterns:
|
|
379
|
+
|
|
380
|
+
`createdAt`, `updatedAt`, `deletedAt`, `publishedAt`, `startedAt`, `endedAt`, `dueDate`, `due_at`, `expiresAt`, `completedAt`, `created_at`, `updated_at`
|