@mikestools/usetable 0.0.3 → 0.0.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.
@@ -1,474 +1,382 @@
1
- <template>
2
- <section id="basicTable">
3
- <h2 class="h4 mb-3">
4
- <IconTable class="me-2" />
5
- Product Inventory Dashboard
6
- </h2>
7
- <p class="text-muted mb-4">
8
- A comprehensive example demonstrating useTable's core features: sorting, filtering, selection,
9
- inline editing, aggregation, and undo/redo — all reactive and type-safe.
10
- </p>
11
-
12
- <!-- Toolbar -->
13
- <div class="card shadow-sm mb-3">
14
- <div class="card-body py-2">
15
- <div class="row g-2 align-items-center">
16
- <!-- Search -->
17
- <div class="col-lg-3">
18
- <div class="input-group input-group-sm">
19
- <span class="input-group-text">
20
- <IconSearch />
21
- </span>
22
- <input
23
- v-model="searchQuery"
24
- type="text"
25
- class="form-control"
26
- placeholder="Search products..."
27
- @input="handleSearch"
28
- >
29
- <button
30
- v-if="searchQuery"
31
- class="btn btn-outline-secondary"
32
- type="button"
33
- @click="clearSearch"
34
- >
35
- <IconX />
36
- </button>
37
- </div>
38
- </div>
39
-
40
- <!-- Category Filter -->
41
- <div class="col-lg-2">
42
- <select
43
- v-model="categoryFilter"
44
- class="form-select form-select-sm"
45
- @change="handleCategoryFilter"
46
- >
47
- <option value="">
48
- All Categories
49
- </option>
50
- <option
51
- v-for="cat in categories"
52
- :key="cat"
53
- :value="cat"
54
- >
55
- {{ cat }}
56
- </option>
57
- </select>
58
- </div>
59
-
60
- <!-- Actions -->
61
- <div class="col-lg-7">
62
- <div class="d-flex gap-2 justify-content-end flex-wrap">
63
- <button
64
- class="btn btn-primary btn-sm"
65
- @click="addProduct"
66
- >
67
- <IconPlus class="me-1" />
68
- Add Product
69
- </button>
70
- <button
71
- class="btn btn-outline-danger btn-sm"
72
- :disabled="selectedCount === 0"
73
- @click="deleteSelected"
74
- >
75
- <IconTrash class="me-1" />
76
- Delete ({{ selectedCount }})
77
- </button>
78
- <div class="btn-group">
79
- <button
80
- class="btn btn-outline-secondary btn-sm"
81
- :disabled="!canUndo"
82
- title="Undo (Ctrl+Z)"
83
- @click="handleUndo"
84
- >
85
- <IconArrowCounterclockwise />
86
- </button>
87
- <button
88
- class="btn btn-outline-secondary btn-sm"
89
- :disabled="!canRedo"
90
- title="Redo (Ctrl+Y)"
91
- @click="handleRedo"
92
- >
93
- <IconArrowClockwise />
94
- </button>
95
- </div>
96
- <button
97
- class="btn btn-outline-secondary btn-sm"
98
- @click="resetTable"
99
- >
100
- <IconArrowRepeat class="me-1" />
101
- Reset
102
- </button>
103
- </div>
104
- </div>
105
- </div>
106
- </div>
107
- </div>
108
-
109
- <!-- Table -->
110
- <div class="card shadow-sm mb-3">
111
- <div class="table-responsive">
112
- <table
113
- ref="tableRef"
114
- class="table table-hover table-striped mb-0 align-middle"
115
- >
116
- <!-- Table structure built via useTable -->
117
- </table>
118
- </div>
119
- </div>
120
-
121
- <!-- Footer Stats -->
122
- <div class="card shadow-sm mb-4">
123
- <div class="card-body py-2">
124
- <div class="row text-center">
125
- <div class="col">
126
- <div class="small text-muted">
127
- Products
128
- </div>
129
- <div class="fw-bold">
130
- {{ rowCount }}
131
- </div>
132
- </div>
133
- <div class="col">
134
- <div class="small text-muted">
135
- Total Stock
136
- </div>
137
- <div class="fw-bold">
138
- {{ totalStock.toLocaleString() }}
139
- </div>
140
- </div>
141
- <div class="col">
142
- <div class="small text-muted">
143
- Total Value
144
- </div>
145
- <div class="fw-bold text-success">
146
- ${{ totalValue.toLocaleString() }}
147
- </div>
148
- </div>
149
- <div class="col">
150
- <div class="small text-muted">
151
- Avg Price
152
- </div>
153
- <div class="fw-bold">
154
- ${{ avgPrice.toFixed(2) }}
155
- </div>
156
- </div>
157
- <div class="col">
158
- <div class="small text-muted">
159
- Low Stock
160
- </div>
161
- <div
162
- class="fw-bold"
163
- :class="lowStockCount > 0 ? 'text-warning' : 'text-success'"
164
- >
165
- {{ lowStockCount }}
166
- </div>
167
- </div>
168
- </div>
169
- </div>
170
- </div>
171
-
172
- <!-- Instructions -->
173
- <div class="alert alert-info mb-4">
174
- <strong>Try it:</strong>
175
- <ul class="mb-0 small">
176
- <li><strong>Sort:</strong> Click column headers</li>
177
- <li><strong>Select:</strong> Click rows (Shift+click for range, Ctrl+click for multi)</li>
178
- <li><strong>Edit:</strong> Double-click any cell to edit inline</li>
179
- <li><strong>Search:</strong> Filter products in real-time</li>
180
- <li><strong>Undo/Redo:</strong> Ctrl+Z / Ctrl+Y or toolbar buttons</li>
181
- </ul>
182
- </div>
183
-
184
- <!-- Code Example -->
185
- <CodeBlock :code="codeExample" />
186
- </section>
187
- </template>
188
-
189
- <script setup lang="ts">
190
- import { ref, onMounted, onUnmounted, watch } from 'vue'
191
- import { useTable } from '@mikestools/usetable'
192
-
193
- import CodeBlock from '@mikestools/usetools/components/CodeBlock.vue'
194
- import IconTable from 'bootstrap-icons/icons/table.svg?component'
195
- import IconSearch from 'bootstrap-icons/icons/search.svg?component'
196
- import IconX from 'bootstrap-icons/icons/x-lg.svg?component'
197
- import IconPlus from 'bootstrap-icons/icons/plus-lg.svg?component'
198
- import IconTrash from 'bootstrap-icons/icons/trash.svg?component'
199
- import IconArrowCounterclockwise from 'bootstrap-icons/icons/arrow-counterclockwise.svg?component'
200
- import IconArrowClockwise from 'bootstrap-icons/icons/arrow-clockwise.svg?component'
201
- import IconArrowRepeat from 'bootstrap-icons/icons/arrow-repeat.svg?component'
202
-
203
- const tableRef = ref<HTMLTableElement>()
204
-
205
- // Reactive state
206
- const rowCount = ref(0)
207
- const selectedCount = ref(0)
208
- const canUndo = ref(false)
209
- const canRedo = ref(false)
210
- const searchQuery = ref('')
211
- const categoryFilter = ref('')
212
-
213
- // Aggregation computed values
214
- const totalStock = ref(0)
215
- const totalValue = ref(0)
216
- const avgPrice = ref(0)
217
- const lowStockCount = ref(0)
218
-
219
- // Sample data
220
- const categories = ['Electronics', 'Accessories', 'Audio', 'Storage', 'Display']
221
-
222
- const sampleProducts = [
223
- ['PRD-001', 'MacBook Pro 14"', 'Electronics', 1999.99, 25, 'Active'],
224
- ['PRD-002', 'Magic Keyboard', 'Accessories', 99.99, 150, 'Active'],
225
- ['PRD-003', 'AirPods Pro', 'Audio', 249.99, 75, 'Active'],
226
- ['PRD-004', 'USB-C Hub', 'Accessories', 79.99, 200, 'Active'],
227
- ['PRD-005', 'Studio Display', 'Display', 1599.99, 12, 'Low Stock'],
228
- ['PRD-006', 'Magic Mouse', 'Accessories', 79.99, 180, 'Active'],
229
- ['PRD-007', 'HomePod Mini', 'Audio', 99.99, 90, 'Active'],
230
- ['PRD-008', 'iPad Pro 12.9"', 'Electronics', 1099.99, 35, 'Active'],
231
- ['PRD-009', 'External SSD 1TB', 'Storage', 149.99, 8, 'Low Stock'],
232
- ['PRD-010', 'Thunderbolt Cable', 'Accessories', 49.99, 300, 'Active'],
233
- ]
234
-
235
- let productCounter = 10
236
- let table: ReturnType<typeof useTable> | undefined
237
- let cleanupEditing: (() => void) | undefined
238
- let cleanupKeyboard: (() => void) | undefined
239
-
240
- onMounted(() => {
241
- const element = tableRef.value
242
- if (!element) return
243
-
244
- table = useTable(ref(element))
245
-
246
- // Set up table structure
247
- table.setCaption('Product Inventory')
248
- table.setHeaders(['SKU', 'Product Name', 'Category', 'Price ($)', 'Stock', 'Status'])
249
- table.setData(sampleProducts)
250
-
251
- // Add footer with totals
252
- updateFooter()
253
-
254
- // Enable features
255
- cleanupEditing = table.enableEditing()
256
- cleanupKeyboard = table.enableKeyboardNavigation()
257
-
258
- // Set row key function for identification (using SKU)
259
- table.setRowKeyFunction((row) => String(row[0]))
260
-
261
- // Watch for changes
262
- table.onDataChange(() => {
263
- updateStats()
264
- updateFooter()
265
- })
266
-
267
- table.onRowSelectionChange((selected) => {
268
- selectedCount.value = selected.size
269
- })
270
-
271
- table.onSortChange(() => {
272
- updateStats()
273
- })
274
-
275
- // Initial stats
276
- updateStats()
277
-
278
- // Add keyboard shortcuts
279
- document.addEventListener('keydown', handleKeyDown)
280
- })
281
-
282
- onUnmounted(() => {
283
- cleanupEditing?.()
284
- cleanupKeyboard?.()
285
- document.removeEventListener('keydown', handleKeyDown)
286
- })
287
-
288
- function handleKeyDown(event: KeyboardEvent) {
289
- if (event.ctrlKey || event.metaKey) {
290
- if (event.key === 'z') {
291
- event.preventDefault()
292
- handleUndo()
293
- } else if (event.key === 'y') {
294
- event.preventDefault()
295
- handleRedo()
296
- }
297
- }
298
- }
299
-
300
- function updateStats() {
301
- if (!table) return
302
-
303
- rowCount.value = table.rowCount.value
304
- canUndo.value = table.canUndo()
305
- canRedo.value = table.canRedo()
306
-
307
- // Aggregations
308
- totalStock.value = table.sum(4)
309
- const priceSum = table.sum(3)
310
- avgPrice.value = rowCount.value > 0 ? priceSum / rowCount.value : 0
311
- totalValue.value = calculateTotalValue()
312
- lowStockCount.value = table.count(5, (val) => val === 'Low Stock')
313
- }
314
-
315
- function calculateTotalValue(): number {
316
- if (!table) return 0
317
- const data = table.data.value
318
- return data.reduce((sum, row) => {
319
- const price = Number(row[3]) || 0
320
- const stock = Number(row[4]) || 0
321
- return sum + price * stock
322
- }, 0)
323
- }
324
-
325
- function updateFooter() {
326
- if (!table) return
327
- const priceSum = table.sum(3)
328
- const stockSum = table.sum(4)
329
- table.setFooter(['', 'TOTALS', '', `$${priceSum.toFixed(2)}`, stockSum.toString(), ''])
330
- }
331
-
332
- function addProduct() {
333
- if (!table) return
334
- productCounter++
335
- const newProduct = [
336
- `PRD-${String(productCounter).padStart(3, '0')}`,
337
- `New Product ${productCounter}`,
338
- categories[Math.floor(Math.random() * categories.length)],
339
- (Math.random() * 500 + 50).toFixed(2),
340
- Math.floor(Math.random() * 100 + 10),
341
- 'Active',
342
- ]
343
- table.addRow(newProduct)
344
- updateStats()
345
- }
346
-
347
- function deleteSelected() {
348
- if (!table) return
349
- const selected = Array.from(table.selectedRows.value).sort((a, b) => b - a)
350
- table.transaction(() => {
351
- for (const index of selected) {
352
- table!.removeRow(index)
353
- }
354
- })
355
- table.clearSelection()
356
- updateStats()
357
- }
358
-
359
- function handleUndo() {
360
- if (!table) return
361
- table.undo()
362
- updateStats()
363
- }
364
-
365
- function handleRedo() {
366
- if (!table) return
367
- table.redo()
368
- updateStats()
369
- }
370
-
371
- function handleSearch() {
372
- if (!table) return
373
- if (searchQuery.value) {
374
- table.filterRows((row) => {
375
- const query = searchQuery.value.toLowerCase()
376
- return row.some((cell) => String(cell).toLowerCase().includes(query))
377
- })
378
- } else {
379
- table.clearFilters()
380
- }
381
- updateStats()
382
- }
383
-
384
- function clearSearch() {
385
- searchQuery.value = ''
386
- handleSearch()
387
- }
388
-
389
- function handleCategoryFilter() {
390
- if (!table) return
391
- if (categoryFilter.value) {
392
- table.filterColumnByValue(2, categoryFilter.value)
393
- } else {
394
- table.clearFilters()
395
- }
396
- updateStats()
397
- }
398
-
399
- function resetTable() {
400
- if (!table) return
401
- table.setData(sampleProducts)
402
- table.clearSelection()
403
- table.clearFilters()
404
- searchQuery.value = ''
405
- categoryFilter.value = ''
406
- productCounter = 10
407
- updateStats()
408
- }
409
-
410
- // Watch for filter changes to clear search when category changes
411
- watch(categoryFilter, () => {
412
- if (categoryFilter.value && searchQuery.value) {
413
- searchQuery.value = ''
414
- }
415
- })
416
-
417
- const codeExample = `import { ref, onMounted, onUnmounted } from 'vue'
418
- import { useTable } from '@mikestools/usetable'
419
-
420
- const tableRef = ref<HTMLTableElement>()
421
- let table: ReturnType<typeof useTable>
422
-
423
- onMounted(() => {
424
- table = useTable(ref(tableRef.value!))
425
-
426
- // Set up structure
427
- table.setHeaders(['SKU', 'Product', 'Category', 'Price', 'Stock', 'Status'])
428
- table.setData([
429
- ['PRD-001', 'MacBook Pro', 'Electronics', 1999.99, 25, 'Active'],
430
- ['PRD-002', 'Magic Keyboard', 'Accessories', 99.99, 150, 'Active'],
431
- ])
432
-
433
- // Enable inline editing (double-click to edit)
434
- const cleanupEdit = table.enableEditing()
435
-
436
- // Enable keyboard navigation (arrow keys)
437
- const cleanupNav = table.enableKeyboardNavigation()
438
-
439
- // Set row key for identification
440
- table.setRowKeyFunction((row) => String(row[0]))
441
-
442
- // React to changes
443
- table.onDataChange((data) => {
444
- console.log('Data changed:', data)
445
- // Save to database, localStorage, etc.
446
- })
447
-
448
- // Aggregations
449
- const totalStock = table.sum(4) // Sum of Stock column
450
- const avgPrice = table.average(3) // Average of Price column
451
- const lowStock = table.count(5, (v) => v === 'Low Stock')
452
-
453
- // Sorting
454
- table.sortColumnAscending(1) // Sort by Product name
455
-
456
- // Filtering
457
- table.filterColumnByValue(2, 'Electronics') // Show only Electronics
458
-
459
- // Selection
460
- table.selectRow(0)
461
- table.selectRowRange(0, 2) // Select rows 0-2
462
-
463
- // Undo/Redo
464
- table.setCell(0, 4, 30) // Change stock
465
- table.undo() // Revert change
466
-
467
- // Transactions (atomic operations)
468
- table.transaction(() => {
469
- table.addRow(['PRD-003', 'New', 'Audio', 199.99, 50, 'Active'])
470
- table.setCell(0, 4, 20)
471
- // If any fails, all changes roll back
472
- })
473
- })`
474
- </script>
1
+ <template>
2
+ <section id="basicTable">
3
+ <h2 class="h4 mb-3">
4
+ <IconTable class="me-2" />
5
+ Basic Table Operations
6
+ </h2>
7
+ <p class="text-muted mb-4">
8
+ A comprehensive product inventory example showing core table operations:
9
+ headers, rows, cells, and reactive data binding.
10
+ </p>
11
+
12
+ <!-- Toolbar -->
13
+ <div class="card shadow-sm mb-3">
14
+ <div class="card-body py-2">
15
+ <div class="d-flex gap-2 flex-wrap align-items-center">
16
+ <button
17
+ class="btn btn-success btn-sm"
18
+ @click="addProduct"
19
+ >
20
+ <IconPlus class="me-1" />
21
+ Add Product
22
+ </button>
23
+ <button
24
+ class="btn btn-outline-warning btn-sm"
25
+ :disabled="rowCount === 0"
26
+ @click="updateRandomPrice"
27
+ >
28
+ <IconPencil class="me-1" />
29
+ Update Random Price
30
+ </button>
31
+ <button
32
+ class="btn btn-outline-danger btn-sm"
33
+ :disabled="rowCount === 0"
34
+ @click="removeLastRow"
35
+ >
36
+ <IconTrash class="me-1" />
37
+ Remove Last
38
+ </button>
39
+ <button
40
+ class="btn btn-outline-secondary btn-sm"
41
+ @click="resetTable"
42
+ >
43
+ <IconArrowRepeat class="me-1" />
44
+ Reset
45
+ </button>
46
+ <div class="vr mx-2" />
47
+ <div class="form-check form-switch mb-0">
48
+ <input
49
+ id="showFooter"
50
+ v-model="showFooter"
51
+ class="form-check-input"
52
+ type="checkbox"
53
+ @change="toggleFooter"
54
+ >
55
+ <label
56
+ class="form-check-label small"
57
+ for="showFooter"
58
+ >
59
+ Show Totals
60
+ </label>
61
+ </div>
62
+ </div>
63
+ </div>
64
+ </div>
65
+
66
+ <!-- Table -->
67
+ <div class="card shadow-sm mb-3">
68
+ <div class="table-responsive">
69
+ <table
70
+ ref="tableRef"
71
+ class="table table-hover table-striped mb-0 align-middle"
72
+ >
73
+ <!-- Table structure built via useTable -->
74
+ </table>
75
+ </div>
76
+ </div>
77
+
78
+ <!-- Stats Panel -->
79
+ <div class="row g-3 mb-4">
80
+ <div class="col-md-3">
81
+ <div class="card bg-primary text-white">
82
+ <div class="card-body py-2 text-center">
83
+ <div class="small opacity-75">
84
+ Total Products
85
+ </div>
86
+ <div class="h4 mb-0">
87
+ {{ rowCount }}
88
+ </div>
89
+ </div>
90
+ </div>
91
+ </div>
92
+ <div class="col-md-3">
93
+ <div class="card bg-success text-white">
94
+ <div class="card-body py-2 text-center">
95
+ <div class="small opacity-75">
96
+ Total Value
97
+ </div>
98
+ <div class="h4 mb-0">
99
+ ${{ totalValue.toLocaleString('en-US', { minimumFractionDigits: 2 }) }}
100
+ </div>
101
+ </div>
102
+ </div>
103
+ </div>
104
+ <div class="col-md-3">
105
+ <div class="card bg-info text-white">
106
+ <div class="card-body py-2 text-center">
107
+ <div class="small opacity-75">
108
+ Total Stock
109
+ </div>
110
+ <div class="h4 mb-0">
111
+ {{ totalStock }} units
112
+ </div>
113
+ </div>
114
+ </div>
115
+ </div>
116
+ <div class="col-md-3">
117
+ <div class="card bg-warning text-dark">
118
+ <div class="card-body py-2 text-center">
119
+ <div class="small opacity-75">
120
+ Columns
121
+ </div>
122
+ <div class="h4 mb-0">
123
+ {{ columnCount }}
124
+ </div>
125
+ </div>
126
+ </div>
127
+ </div>
128
+ </div>
129
+
130
+ <!-- Instructions -->
131
+ <div class="alert alert-info mb-4">
132
+ <h6 class="alert-heading">
133
+ <IconInfoCircle class="me-1" />
134
+ What this demonstrates
135
+ </h6>
136
+ <ul class="mb-0 small">
137
+ <li><strong>Reactive Data:</strong> Stats automatically update when data changes</li>
138
+ <li><strong>Row Operations:</strong> Add, update, and remove rows dynamically</li>
139
+ <li><strong>Cell Operations:</strong> Update individual cell values</li>
140
+ <li><strong>Footer:</strong> Optional summary row with totals</li>
141
+ <li><strong>Computed Values:</strong> Calculate totals from table data</li>
142
+ </ul>
143
+ </div>
144
+
145
+ <!-- Code Example -->
146
+ <CodeBlock :code="codeExample" />
147
+ </section>
148
+ </template>
149
+
150
+ <script setup lang="ts">
151
+ import { ref, onMounted, watch } from 'vue'
152
+ import { useTable, type UseTableReturn } from '@mikestools/usetable'
153
+
154
+ import CodeBlock from '@mikestools/usetools/components/CodeBlock.vue'
155
+ import IconTable from 'bootstrap-icons/icons/table.svg?component'
156
+ import IconPlus from 'bootstrap-icons/icons/plus-lg.svg?component'
157
+ import IconPencil from 'bootstrap-icons/icons/pencil.svg?component'
158
+ import IconTrash from 'bootstrap-icons/icons/trash.svg?component'
159
+ import IconArrowRepeat from 'bootstrap-icons/icons/arrow-repeat.svg?component'
160
+ import IconInfoCircle from 'bootstrap-icons/icons/info-circle.svg?component'
161
+
162
+ const tableRef = ref<HTMLTableElement>()
163
+ const showFooter = ref(true)
164
+
165
+ // Tracked state - updated via onDataChange callback
166
+ const rowCount = ref(0)
167
+ const columnCount = ref(0)
168
+ const totalValue = ref(0)
169
+ const totalStock = ref(0)
170
+
171
+ // Table instance stored for method access
172
+ let table: UseTableReturn | undefined
173
+
174
+ // Sample product data
175
+ const sampleProducts: (string | number)[][] = [
176
+ ['PRD-001', 'MacBook Pro 14"', 'Electronics', 1999.99, 25],
177
+ ['PRD-002', 'Magic Keyboard', 'Accessories', 99.99, 150],
178
+ ['PRD-003', 'AirPods Pro 2', 'Audio', 249.99, 75],
179
+ ['PRD-004', 'USB-C Hub 7-in-1', 'Accessories', 79.99, 200],
180
+ ['PRD-005', 'Studio Display 27"', 'Displays', 1599.99, 12],
181
+ ]
182
+
183
+ const categories = ['Electronics', 'Accessories', 'Audio', 'Displays', 'Storage', 'Peripherals']
184
+ const productNames = ['Wireless Mouse', 'Webcam HD', 'External SSD', 'Monitor Stand', 'Desk Lamp', 'Cable Kit']
185
+
186
+ let productCounter = 5
187
+
188
+ /**
189
+ * Update all tracked stats from the table
190
+ */
191
+ function updateStats() {
192
+ if (!table) return
193
+
194
+ rowCount.value = table.rowCount.value
195
+ columnCount.value = table.columnCount.value
196
+
197
+ // Calculate totals from table data
198
+ let valueSum = 0
199
+ let stockSum = 0
200
+
201
+ for (const row of table.data.value) {
202
+ const price = Number(row[3]) || 0
203
+ const stock = Number(row[4]) || 0
204
+ valueSum += price * stock
205
+ stockSum += stock
206
+ }
207
+
208
+ totalValue.value = valueSum
209
+ totalStock.value = stockSum
210
+ }
211
+
212
+ /**
213
+ * Update footer with current totals
214
+ */
215
+ function updateFooter() {
216
+ if (!table) return
217
+
218
+ table.setFooter([
219
+ '',
220
+ `${rowCount.value} Products`,
221
+ '',
222
+ `$${totalValue.value.toLocaleString('en-US', { minimumFractionDigits: 2 })}`,
223
+ `${totalStock.value} units`
224
+ ])
225
+
226
+ // Style the footer
227
+ const element = tableRef.value
228
+ if (element) {
229
+ const tfoot = element.tFoot
230
+ if (tfoot) {
231
+ tfoot.classList.add('table-dark')
232
+ const row = tfoot.rows[0]
233
+ if (row) {
234
+ row.classList.add('fw-bold')
235
+ }
236
+ }
237
+ }
238
+ }
239
+
240
+ onMounted(() => {
241
+ const element = tableRef.value
242
+ if (!element) return
243
+
244
+ // Create table instance
245
+ table = useTable(ref(element), sampleProducts, {
246
+ headers: ['SKU', 'Product Name', 'Category', 'Price ($)', 'Stock'],
247
+ caption: 'Product Inventory',
248
+ })
249
+
250
+ // Initial stats calculation
251
+ updateStats()
252
+
253
+ // Set initial footer if enabled
254
+ if (showFooter.value) {
255
+ updateFooter()
256
+ }
257
+
258
+ // Subscribe to data changes to keep stats in sync
259
+ table.onDataChange(() => {
260
+ updateStats()
261
+ if (showFooter.value) {
262
+ updateFooter()
263
+ }
264
+ })
265
+ })
266
+
267
+ // Watch for footer toggle
268
+ watch(showFooter, (show) => {
269
+ if (show) {
270
+ updateFooter()
271
+ } else {
272
+ table?.clearFooter()
273
+ }
274
+ })
275
+
276
+ function addProduct() {
277
+ if (!table) return
278
+ productCounter++
279
+
280
+ const category = categories[Math.floor(Math.random() * categories.length)]
281
+ const name = productNames[Math.floor(Math.random() * productNames.length)]
282
+ const price = (Math.random() * 300 + 29.99).toFixed(2)
283
+ const stock = Math.floor(Math.random() * 100 + 5)
284
+
285
+ table.addRow([
286
+ `PRD-${String(productCounter).padStart(3, '0')}`,
287
+ name,
288
+ category,
289
+ price,
290
+ stock
291
+ ])
292
+ }
293
+
294
+ function updateRandomPrice() {
295
+ if (!table || rowCount.value === 0) return
296
+
297
+ const randomRow = Math.floor(Math.random() * rowCount.value)
298
+ const currentPrice = Number(table.getCell(randomRow, 3)) || 0
299
+ const change = (Math.random() - 0.5) * 100 // -50 to +50
300
+ const newPrice = Math.max(9.99, currentPrice + change).toFixed(2)
301
+
302
+ table.setCell(randomRow, 3, newPrice)
303
+
304
+ // Flash the row to show the change
305
+ const element = tableRef.value
306
+ if (element) {
307
+ const tbody = element.tBodies[0]
308
+ if (tbody) {
309
+ const row = tbody.rows[randomRow]
310
+ if (row) {
311
+ row.classList.add('table-warning')
312
+ setTimeout(() => row.classList.remove('table-warning'), 500)
313
+ }
314
+ }
315
+ }
316
+ }
317
+
318
+ function removeLastRow() {
319
+ if (!table || rowCount.value === 0) return
320
+ table.removeRow(rowCount.value - 1)
321
+ }
322
+
323
+ function resetTable() {
324
+ if (!table) return
325
+ table.setData(sampleProducts)
326
+ productCounter = 5
327
+ }
328
+
329
+ function toggleFooter() {
330
+ // Handled by watcher
331
+ }
332
+
333
+ const codeExample = `import { ref, onMounted } from 'vue'
334
+ import { useTable, type CellValue } from '@mikestools/usetable'
335
+
336
+ const tableRef = ref<HTMLTableElement>()
337
+
338
+ // Tracked stats - updated via callback
339
+ const rowCount = ref(0)
340
+ const totalValue = ref(0)
341
+
342
+ // Table instance
343
+ let table
344
+
345
+ onMounted(() => {
346
+ // Initialize table with data and options
347
+ table = useTable(ref(tableRef.value!), [
348
+ ['PRD-001', 'MacBook Pro', 'Electronics', 1999.99, 25],
349
+ ['PRD-002', 'Magic Keyboard', 'Accessories', 99.99, 150],
350
+ ], {
351
+ headers: ['SKU', 'Product', 'Category', 'Price', 'Stock'],
352
+ caption: 'Product Inventory'
353
+ })
354
+
355
+ // Calculate stats
356
+ function updateStats() {
357
+ rowCount.value = table.rowCount.value
358
+ totalValue.value = table.data.value.reduce((sum, row) => {
359
+ return sum + (Number(row[3]) * Number(row[4]))
360
+ }, 0)
361
+ }
362
+
363
+ // Initial calculation
364
+ updateStats()
365
+
366
+ // Subscribe to data changes
367
+ table.onDataChange(() => updateStats())
368
+
369
+ // Add a new row
370
+ table.addRow(['PRD-003', 'AirPods', 'Audio', 249.99, 75])
371
+
372
+ // Update a cell value
373
+ table.setCell(0, 3, 1899.99)
374
+
375
+ // Remove a row
376
+ table.removeRow(1)
377
+
378
+ // Set footer with totals
379
+ table.setFooter(['', 'Total', '', '', totalStock.value])
380
+ })`
381
+ </script>
382
+