Skip to content

Commit 3a66c2c

Browse files
committed
feat: update table component to support configurable sortable columns and enhance sorting logic
1 parent b963cde commit 3a66c2c

File tree

2 files changed

+114
-5
lines changed

2 files changed

+114
-5
lines changed

adminforth/documentation/docs/tutorial/03-Customization/15-afcl.md

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,57 @@ import { Button } from '@/afcl'
2222
```
2323

2424
```html
25+
26+
### Sorting
27+
28+
Table supports column sorting out of the box.
29+
30+
- Sorting is enabled globally by default. You can disable it entirely with `:sortable="false"`.
31+
- Per column, sorting can be disabled with `sortable: false` inside the column definition.
32+
- Clicking a sortable header cycles sorting in a tri‑state order:
33+
- none → ascending → descending → none
34+
- When it returns to "none", the sorting is cleared.
35+
36+
Basic example (client-side sorting when `data` is an array):
37+
38+
```html
39+
<Table
40+
:columns="[
41+
{ label: 'Name', fieldName: 'name' },
42+
{ label: 'Age', fieldName: 'age' },
43+
// disable sort for a specific column
44+
{ label: 'Country', fieldName: 'country', sortable: false },
45+
]"
46+
:data="[
47+
{ name: 'John', age: 30, country: 'US' },
48+
{ name: 'Rick', age: 25, country: 'CA' },
49+
{ name: 'Alice', age: 35, country: 'BR' },
50+
{ name: 'Colin', age: 40, country: 'AU' },
51+
]"
52+
:sortable="true"
53+
:pageSize="3"
54+
/>
55+
```
56+
57+
You can also predefine a default sort:
58+
59+
```html
60+
<Table
61+
:columns="[
62+
{ label: 'Name', fieldName: 'name' },
63+
{ label: 'Age', fieldName: 'age' },
64+
{ label: 'Country', fieldName: 'country' },
65+
]"
66+
:data="rows"
67+
defaultSortField="age"
68+
defaultSortDirection="desc"
69+
/>
70+
```
71+
72+
Notes:
73+
- Client-side sorting supports nested field paths using dot-notation, e.g. `user.name`.
74+
- When a column is not currently sorted, a subtle double-arrow icon is shown; arrows switch up/down for ascending/descending.
75+
2576
<Button @click="doSmth"
2677
:loader="false" class="w-full">
2778
Your button text
@@ -973,6 +1024,59 @@ async function loadPageData(data) {
9731024
```
9741025
> 👆 The page size is used as the limit for pagination.
9751026
1027+
### Server-side sorting
1028+
1029+
When you provide an async function to `data`, the table will pass the current sort along with pagination params.
1030+
1031+
Signature of the loader receives:
1032+
1033+
```ts
1034+
type LoaderArgs = {
1035+
offset: number;
1036+
limit: number;
1037+
sortField?: string; // undefined when unsorted
1038+
sortDirection?: 'asc' | 'desc'; // only when sortField is set
1039+
}
1040+
```
1041+
1042+
Example using `fetch`:
1043+
1044+
```ts
1045+
async function loadPageData({ offset, limit, sortField, sortDirection }) {
1046+
const url = new URL('/api/products', window.location.origin);
1047+
url.searchParams.set('offset', String(offset));
1048+
url.searchParams.set('limit', String(limit));
1049+
if (sortField) url.searchParams.set('sortField', sortField);
1050+
if (sortField && sortDirection) url.searchParams.set('sortDirection', sortDirection);
1051+
1052+
const res = await fetch(url.toString(), { credentials: 'include' });
1053+
const json = await res.json();
1054+
return { data: json.data, total: json.total };
1055+
}
1056+
1057+
<Table
1058+
:columns="[
1059+
{ label: 'ID', fieldName: 'id' },
1060+
{ label: 'Title', fieldName: 'title' },
1061+
{ label: 'Price', fieldName: 'price' },
1062+
]"
1063+
:data="loadPageData"
1064+
:pageSize="10"
1065+
/>
1066+
```
1067+
1068+
Events you can listen to:
1069+
1070+
```html
1071+
<Table
1072+
:columns="columns"
1073+
:data="loadPageData"
1074+
@update:sortField="(f) => currentSortField = f"
1075+
@update:sortDirection="(d) => currentSortDirection = d"
1076+
@sort-change="({ field, direction }) => console.log('sort changed', field, direction)"
1077+
/>
1078+
```
1079+
9761080
### Table loading states
9771081

9781082
For tables where you load data externally and pass them to `data` prop as array (including case with front-end pagination) you might want to show skeleton loaders in table externaly using `isLoading` props.

adminforth/spa/src/afcl/Table.vue

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@
172172
}>(), {
173173
evenHighlights: true,
174174
pageSize: 5,
175-
sortable: true,
175+
sortable: false,
176176
}
177177
);
178178
@@ -206,7 +206,7 @@
206206
});
207207
208208
watch([() => currentSortField.value, () => currentSortDirection.value], () => {
209-
// reset to first page on sort change
209+
if (!props.sortable) return;
210210
if (currentPage.value !== 1) currentPage.value = 1;
211211
refresh();
212212
emit('update:sortField', currentSortField.value);
@@ -296,7 +296,7 @@
296296
}
297297
298298
function isColumnSortable(col:{fieldName:string; sortable?:boolean}) {
299-
return !!props.sortable && col.sortable !== false;
299+
return props.sortable === true && col.sortable === true;
300300
}
301301
302302
function onHeaderClick(col:{fieldName:string; sortable?:boolean}) {
@@ -322,12 +322,17 @@ const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'bas
322322
323323
function sortArrayData(data:any[], sortField?:string, dir:'asc'|'desc'='asc') {
324324
if (!props.sortable || !sortField) return data;
325-
const get = (o:any, p:string) => p.split('.').reduce((a:any,k)=>a?.[k], o);
325+
// Helper function to get nested properties by path
326+
const getByPath = (o:any, p:string) => p.split('.').reduce((a:any,k)=>a?.[k], o);
326327
return [...data].sort((a,b) => {
327-
let av = get(a, sortField), bv = get(b, sortField);
328+
let av = getByPath(a, sortField), bv = getByPath(b, sortField);
329+
// Handle null/undefined values
328330
if (av == null && bv == null) return 0;
331+
// Handle null/undefined values
329332
if (av == null) return 1; if (bv == null) return -1;
333+
// Data types
330334
if (av instanceof Date && bv instanceof Date) return dir === 'asc' ? av.getTime() - bv.getTime() : bv.getTime() - av.getTime();
335+
// Strings and numbers
331336
if (typeof av === 'number' && typeof bv === 'number') return dir === 'asc' ? av - bv : bv - av;
332337
const cmp = collator.compare(String(av), String(bv));
333338
return dir === 'asc' ? cmp : -cmp;

0 commit comments

Comments
 (0)