@kikiloaw/simple-table 1.0.1 β†’ 1.0.2

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
@@ -212,6 +212,14 @@ const columns = [
212
212
  label: 'Email'
213
213
  },
214
214
 
215
+ // Auto-numbering (row numbers instead of data)
216
+ {
217
+ key: 'id',
218
+ label: '#',
219
+ autonumber: true,
220
+ width: '80px'
221
+ },
222
+
215
223
  // Sticky actions column (always visible)
216
224
  {
217
225
  key: 'actions',
@@ -233,6 +241,50 @@ Use custom sort keys when:
233
241
 
234
242
  ## 🎨 Features
235
243
 
244
+ ### Auto-Numbering
245
+
246
+ **Display sequential row numbers instead of actual data:**
247
+
248
+ ```vue
249
+ const columns = [
250
+ {
251
+ key: 'id',
252
+ label: '#',
253
+ autonumber: true,
254
+ width: '80px'
255
+ },
256
+ // ... other columns
257
+ ]
258
+ ```
259
+
260
+ **Features:**
261
+ - βœ… Displays 1, 2, 3, 4... for each data row
262
+ - βœ… Skips group headers (only counts data rows)
263
+ - βœ… Pagination-aware: Page 2 shows 11, 12, 13... (with 10 per page)
264
+ - βœ… Overrides actual column data
265
+ - βœ… Works with both server-side and client-side modes
266
+
267
+ **Example:**
268
+ ```
269
+ β”Œβ”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
270
+ β”‚ # β”‚ Name β”‚ Status β”‚
271
+ β”œβ”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
272
+ β”‚ ACTIVE USERS β”‚ ← Header (not counted)
273
+ β”‚ 1 β”‚ John Doe β”‚ Active β”‚
274
+ β”‚ 2 β”‚ Jane Smith β”‚ Active β”‚
275
+ β”œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
276
+ β”‚ INACTIVE USERS β”‚ ← Header (not counted)
277
+ β”‚ 3 β”‚ Bob Johnson β”‚ Inactiveβ”‚
278
+ ```
279
+
280
+ **Perfect for:**
281
+ - Sequential numbering regardless of actual IDs
282
+ - User-friendly row references
283
+ - Tables with group headers
284
+ - Paginated lists with continuous numbering
285
+
286
+ ---
287
+
236
288
  ### Custom Sort Keys
237
289
 
238
290
  **Problem:** You want to display `department.name` but sort by `department_id`.
@@ -456,6 +508,248 @@ const downloadCSV = (content, filename) => {
456
508
  - βœ… Integration with third-party libraries
457
509
 
458
510
 
511
+ ---
512
+
513
+ ### Group Headers
514
+
515
+ **Organize your table data with full-width group headers:**
516
+
517
+ ```vue
518
+ <script setup>
519
+ const addGroupHeaders = (rows) => {
520
+ // Sort by category first
521
+ const sorted = [...rows].sort((a, b) =>
522
+ (a.category || '').localeCompare(b.category || '')
523
+ )
524
+
525
+ const result = []
526
+ let currentCategory = null
527
+
528
+ sorted.forEach(row => {
529
+ const category = row.category || 'Uncategorized'
530
+
531
+ // When category changes, add a header row
532
+ if (category !== currentCategory) {
533
+ result.push({
534
+ _isGroupHeader: true, // Special flag
535
+ _groupTitle: category, // Header text
536
+ // Add empty values for all columns
537
+ ...Object.fromEntries(
538
+ Object.keys(row).map(key => [key, ''])
539
+ )
540
+ })
541
+ currentCategory = category
542
+ }
543
+
544
+ result.push(row)
545
+ })
546
+
547
+ return result
548
+ }
549
+ </script>
550
+
551
+ <template>
552
+ <SimpleTable
553
+ :columns="columns"
554
+ :before-render="addGroupHeaders"
555
+ odd-row-color="bg-gray-50"
556
+ even-row-color="bg-white"
557
+ fetch-url="/api/data"
558
+ />
559
+ </template>
560
+ ```
561
+
562
+ **Result:**
563
+ ```
564
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
565
+ β”‚ CATEGORY A β”‚ ← Full-width header
566
+ β”œβ”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
567
+ β”‚ 1 β”‚ Item 1 β”‚ $100 β”‚ Active β”‚
568
+ β”‚ 2 β”‚ Item 2 β”‚ $200 β”‚ Active β”‚
569
+ β”œβ”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
570
+ β”‚ CATEGORY B β”‚ ← Full-width header
571
+ β”œβ”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
572
+ β”‚ 3 β”‚ Item 3 β”‚ $150 β”‚ Inactive β”‚
573
+ ```
574
+
575
+ **How It Works:**
576
+ 1. Use `beforeRender` callback to transform data
577
+ 2. Insert rows with `_isGroupHeader: true` flag
578
+ 3. Component renders these as full-width cells with `colspan`
579
+ 4. Striping continues correctly across all rows
580
+
581
+ **Grouping Examples:**
582
+
583
+ ```vue
584
+ // Group by first letter
585
+ const groupByLetter = (rows) => {
586
+ const sorted = [...rows].sort((a, b) =>
587
+ a.name.localeCompare(b.name)
588
+ )
589
+
590
+ const result = []
591
+ let currentLetter = null
592
+
593
+ sorted.forEach(row => {
594
+ const letter = row.name.charAt(0).toUpperCase()
595
+
596
+ if (letter !== currentLetter) {
597
+ result.push({
598
+ _isGroupHeader: true,
599
+ _groupTitle: letter,
600
+ })
601
+ currentLetter = letter
602
+ }
603
+
604
+ result.push(row)
605
+ })
606
+
607
+ return result
608
+ }
609
+
610
+ // Group by date range
611
+ const groupByDate = (rows) => {
612
+ const sorted = [...rows].sort((a, b) =>
613
+ new Date(b.created_at) - new Date(a.created_at)
614
+ )
615
+
616
+ const result = []
617
+ let currentMonth = null
618
+
619
+ sorted.forEach(row => {
620
+ const month = new Date(row.created_at).toLocaleDateString('en', {
621
+ year: 'numeric',
622
+ month: 'long'
623
+ })
624
+
625
+ if (month !== currentMonth) {
626
+ result.push({
627
+ _isGroupHeader: true,
628
+ _groupTitle: month,
629
+ })
630
+ currentMonth = month
631
+ }
632
+
633
+ result.push(row)
634
+ })
635
+
636
+ return result
637
+ }
638
+
639
+ // Group by status
640
+ const groupByStatus = (rows) => {
641
+ const active = rows.filter(r => r.is_active)
642
+ const inactive = rows.filter(r => !r.is_active)
643
+
644
+ return [
645
+ { _isGroupHeader: true, _groupTitle: 'Active Items' },
646
+ ...active,
647
+ { _isGroupHeader: true, _groupTitle: 'Inactive Items' },
648
+ ...inactive
649
+ ]
650
+ }
651
+ ```
652
+
653
+ **Styling Group Headers:**
654
+
655
+ The component applies these classes to group header rows:
656
+ - `border-b border-gray-200` - Bottom border
657
+ - Font styling via the inner div
658
+ - Row striping colors (odd-row-color / even-row-color)
659
+
660
+ You can customize by overriding row colors:
661
+ ```vue
662
+ <SimpleTable
663
+ :before-render="addGroupHeaders"
664
+ odd-row-color="bg-blue-50"
665
+ even-row-color="bg-white"
666
+ />
667
+ ```
668
+
669
+ ---
670
+
671
+ ### Data Transformation (beforeRender)
672
+
673
+ **Transform data before it's rendered in the table:**
674
+
675
+ ```vue
676
+ <script setup>
677
+ import dayjs from 'dayjs'
678
+
679
+ const transformData = (rows) => {
680
+ return rows.map(row => ({
681
+ ...row,
682
+ // Add computed properties
683
+ full_name: `${row.first_name} ${row.last_name}`,
684
+
685
+ // Format dates
686
+ created_at_formatted: dayjs(row.created_at).format('MMM D, YYYY'),
687
+
688
+ // Add status badge classes
689
+ status_class: row.status === 'active' ? 'text-green-600' : 'text-red-600',
690
+
691
+ // Transform arrays
692
+ tags_joined: row.tags?.join(', ') || 'No tags',
693
+
694
+ // Add custom logic
695
+ is_urgent: row.priority > 8,
696
+ days_since_created: dayjs().diff(dayjs(row.created_at), 'days')
697
+ }))
698
+ }
699
+ </script>
700
+
701
+ <template>
702
+ <SimpleTable
703
+ :columns="columns"
704
+ fetch-url="/api/users"
705
+ :before-render="transformData"
706
+ />
707
+ </template>
708
+ ```
709
+
710
+ **When to Use:**
711
+ - βœ… Format dates, numbers, or currencies
712
+ - βœ… Combine multiple fields into one
713
+ - βœ… Add computed properties
714
+ - βœ… Transform nested objects to flat properties
715
+ - βœ… Add CSS classes based on data
716
+ - βœ… Filter unwanted rows (return modified array)
717
+
718
+ **Example - Adding Full Names:**
719
+ ```vue
720
+ <script setup>
721
+ const columns = [
722
+ { key: 'full_name', label: 'Name' }, // Not in API response
723
+ { key: 'email', label: 'Email' },
724
+ { key: 'created_at_formatted', label: 'Joined' }
725
+ ]
726
+
727
+ const beforeRender = (rows) => {
728
+ return rows.map(row => ({
729
+ ...row,
730
+ full_name: `${row.first_name} ${row.last_name}`,
731
+ created_at_formatted: new Date(row.created_at).toLocaleDateString()
732
+ }))
733
+ }
734
+ </script>
735
+
736
+ <template>
737
+ <SimpleTable
738
+ :columns="columns"
739
+ :before-render="beforeRender"
740
+ fetch-url="/api/users"
741
+ />
742
+ </template>
743
+ ```
744
+
745
+ **Execution Order:**
746
+ 1. Data fetched from API
747
+ 2. Filtering & Searching (client-side only)
748
+ 3. Sorting (client-side only)
749
+ 4. Pagination (client-side only)
750
+ 5. **`beforeRender` called** ← Your transformation here
751
+ 6. Rows rendered in table
752
+
459
753
  ---
460
754
 
461
755
  ### Custom Cell Rendering
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kikiloaw/simple-table",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "A lightweight, dependency-light DataTable component for Vue 3 with Tailwind CSS",
5
5
  "main": "src/index.js",
6
6
  "module": "src/index.js",
@@ -26,6 +26,7 @@ interface Props {
26
26
  class?: string
27
27
  fixed?: boolean // 'left' or 'right' could be added later, assuming 'right' for actions usually
28
28
  width?: string
29
+ autonumber?: boolean // If true, display auto-incremented row numbers instead of data
29
30
  }[]
30
31
  mode?: 'auto' | 'server' | 'client'
31
32
  protocol?: 'laravel' | 'datatables' // API request/response format
@@ -34,6 +35,9 @@ interface Props {
34
35
  pageSizes?: any[] // number[] or { label: string, value: number }[]
35
36
  fetchUrl?: string
36
37
 
38
+ // Callbacks
39
+ beforeRender?: (rows: any[]) => any[] // Transform data before rendering
40
+
37
41
  // Cache Props
38
42
  enableCache?: boolean // If true, cache responses by page/search/sort to avoid redundant requests
39
43
 
@@ -189,6 +193,7 @@ const serverMeta = computed(() => {
189
193
  return {
190
194
  current_page: meta.current_page ?? 1,
191
195
  last_page: meta.last_page ?? 1,
196
+ per_page: meta.per_page ?? currentPerPage.value,
192
197
  from: meta.from ?? 0,
193
198
  to: meta.to ?? 0,
194
199
  total: meta.total ?? 0,
@@ -198,39 +203,52 @@ const serverMeta = computed(() => {
198
203
 
199
204
  // -- Computed: Data Normalization --
200
205
  const tableData = computed(() => {
206
+ let result: any[] = []
207
+
201
208
  if (isServerSide.value) {
202
209
  const d = internalData.value as any
203
- return d.data || []
204
- }
205
-
206
- // Client Side Processing
207
- let items = [...(internalData.value as any[])]
210
+ result = d.data || []
211
+ } else {
212
+ // Client Side Processing
213
+ let items = [...(internalData.value as any[])]
208
214
 
209
- // 1. Filter
210
- if (searchQuery.value) {
211
- const lowerQuery = searchQuery.value.toLowerCase()
212
- items = items.filter((item) =>
213
- Object.values(item).some((val) =>
214
- String(val).toLowerCase().includes(lowerQuery)
215
+ // 1. Filter
216
+ if (searchQuery.value) {
217
+ const lowerQuery = searchQuery.value.toLowerCase()
218
+ items = items.filter((item) =>
219
+ Object.values(item).some((val) =>
220
+ String(val).toLowerCase().includes(lowerQuery)
221
+ )
215
222
  )
216
- )
217
- }
223
+ }
218
224
 
219
- // 2. Sort
220
- if (sortColumn.value) {
221
- items.sort((a, b) => {
222
- const valA = a[sortColumn.value]
223
- const valB = b[sortColumn.value]
224
- if (valA === valB) return 0
225
- const comparison = valA > valB ? 1 : -1
226
- return sortDirection.value === 'asc' ? comparison : -comparison
227
- })
228
- }
225
+ // 2. Sort
226
+ if (sortColumn.value) {
227
+ items.sort((a, b) => {
228
+ const valA = a[sortColumn.value]
229
+ const valB = b[sortColumn.value]
230
+ if (valA === valB) return 0
231
+ const comparison = valA > valB ? 1 : -1
232
+ return sortDirection.value === 'asc' ? comparison : -comparison
233
+ })
234
+ }
229
235
 
230
- // 3. Paginate
231
- const start = (currentPage.value - 1) * currentPerPage.value
232
- const end = start + currentPerPage.value
233
- return items.slice(start, end)
236
+ // 3. Paginate
237
+ const start = (currentPage.value - 1) * currentPerPage.value
238
+ const end = start + currentPerPage.value
239
+ result = items.slice(start, end)
240
+ }
241
+
242
+ // Apply beforeRender callback if provided
243
+ if (props.beforeRender && typeof props.beforeRender === 'function') {
244
+ const transformed = props.beforeRender(result)
245
+ // Ensure callback returns an array
246
+ if (Array.isArray(transformed)) {
247
+ result = transformed
248
+ }
249
+ }
250
+
251
+ return result
234
252
  })
235
253
 
236
254
  const totalPages = computed(() => {
@@ -532,6 +550,38 @@ function handlePageSizeChange(size: any) {
532
550
  fetchData()
533
551
  }
534
552
 
553
+ // Get row class with simple alternating stripes (all rows)
554
+ function getRowClass(row: any, idx: number) {
555
+ // Alternate: index 0 = white, index 1 = gray, index 2 = white, etc.
556
+ const isOdd = idx % 2 === 0 // Changed: even index = odd color (white)
557
+ return [
558
+ { [props.oddRowColor]: isOdd, [props.evenRowColor]: !isOdd },
559
+ row._isGroupHeader ? '' : props.hoverColor // No hover on headers
560
+ ]
561
+ }
562
+
563
+ // Get row number for auto-numbering (excluding group headers)
564
+ function getRowNumber(idx: number): number {
565
+ // Count only data rows before this index
566
+ let dataRowCount = 0
567
+ for (let i = 0; i <= idx; i++) {
568
+ if (!tableData.value[i]?._isGroupHeader) {
569
+ dataRowCount++
570
+ }
571
+ }
572
+
573
+ // Add offset for pagination
574
+ if (isServerSide.value) {
575
+ const currentPage = serverMeta.value?.current_page || 1
576
+ const perPage = serverMeta.value?.per_page || currentPerPage.value
577
+ const offset = (currentPage - 1) * perPage
578
+ return offset + dataRowCount
579
+ }
580
+
581
+ // Client-side: just return the count
582
+ return dataRowCount
583
+ }
584
+
535
585
  function handlePageChange(page: number) {
536
586
  if (page < 1 || page > totalPages.value) return
537
587
 
@@ -706,21 +756,38 @@ function getCellStyle(col: any) {
706
756
  v-for="(row, idx) in tableData"
707
757
  :key="idx"
708
758
  class="group"
709
- :class="[{ [evenRowColor]: Number(idx) % 2 === 0, [oddRowColor]: Number(idx) % 2 !== 0 }, hoverColor]"
759
+ :class="getRowClass(row, idx)"
710
760
  >
711
- <TableCell
712
- v-for="(col, cIdx) in columns"
713
- :key="col.key"
714
- :class="getCellClass(col, cIdx, columns.length, Number(idx))"
715
- :style="getCellStyle(col)"
716
- >
717
- <!-- Scoped Slot for custom cell rendering -->
718
- <div>
719
- <slot :name="`cell-${col.key}`" :row="row">
720
- {{ row[col.key] }}
721
- </slot>
722
- </div>
723
- </TableCell>
761
+ <!-- Group Header Row: Single cell spanning all columns -->
762
+ <template v-if="row._isGroupHeader">
763
+ <TableCell :colspan="columns.length" class="border-b border-gray-200">
764
+ <div class="px-2 py-2 font-semibold text-gray-700 text-sm uppercase tracking-wide">
765
+ {{ row._groupTitle }}
766
+ </div>
767
+ </TableCell>
768
+ </template>
769
+
770
+ <!-- Regular Data Row: Individual cells -->
771
+ <template v-else>
772
+ <TableCell
773
+ v-for="(col, cIdx) in columns"
774
+ :key="col.key"
775
+ :class="getCellClass(col, cIdx, columns.length, idx)"
776
+ :style="getCellStyle(col)"
777
+ >
778
+ <!-- Auto-numbering or custom cell rendering -->
779
+ <div>
780
+ <template v-if="col.autonumber">
781
+ {{ getRowNumber(idx) }}
782
+ </template>
783
+ <template v-else>
784
+ <slot :name="`cell-${col.key}`" :row="row">
785
+ {{ row[col.key] }}
786
+ </slot>
787
+ </template>
788
+ </div>
789
+ </TableCell>
790
+ </template>
724
791
  </TableRow>
725
792
  </template>
726
793
  <TableRow v-else>