WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Commit a8f54b6

Browse files
committed
feat: enhance table sorting functionality with per-column control and default sort handling
1 parent 3a66c2c commit a8f54b6

File tree

2 files changed

+96
-60
lines changed

2 files changed

+96
-60
lines changed

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

Lines changed: 85 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -22,57 +22,6 @@ 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-
7625
<Button @click="doSmth"
7726
:loader="false" class="w-full">
7827
Your button text
@@ -1024,6 +973,89 @@ async function loadPageData(data) {
1024973
```
1025974
> 👆 The page size is used as the limit for pagination.
1026975
976+
### Sorting
977+
978+
Table supports column sorting out of the box.
979+
980+
- By default, columns are NOT sortable. Enable sorting per column with `sortable: true`.
981+
- Clicking a sortable header cycles sorting in a tri‑state order:
982+
- none → ascending → descending → none
983+
- When it returns to "none", the sorting is cleared.
984+
985+
Basic example (client-side sorting when `data` is an array):
986+
```html
987+
<Table
988+
:columns="[
989+
{ label: 'Name', fieldName: 'name', sortable: true },
990+
{ label: 'Age', fieldName: 'age', sortable: true },
991+
// disable sort for a specific column
992+
{ label: 'Country', fieldName: 'country', sortable: false },
993+
]"
994+
:data="[
995+
{ name: 'John', age: 30, country: 'US' },
996+
{ name: 'Rick', age: 25, country: 'CA' },
997+
{ name: 'Alice', age: 35, country: 'BR' },
998+
{ name: 'Colin', age: 40, country: 'AU' },
999+
]"
1000+
:pageSize="3"
1001+
/>
1002+
```
1003+
1004+
You can also predefine a default sort:
1005+
1006+
```html
1007+
<Table
1008+
:columns="[
1009+
{ label: 'Name', fieldName: 'name', sortable: true },
1010+
{ label: 'Age', fieldName: 'age', sortable: true },
1011+
{ label: 'Country', fieldName: 'country' },
1012+
]"
1013+
:data="rows"
1014+
defaultSortField="age"
1015+
defaultSortDirection="desc"
1016+
/>
1017+
```
1018+
1019+
Notes:
1020+
- Client-side sorting supports nested field paths using dot-notation, e.g. `user.name`.
1021+
- When a column is not currently sorted, a subtle double-arrow icon is shown; arrows switch up/down for ascending/descending.
1022+
1023+
#### Nested field path sorting
1024+
1025+
You can sort by nested properties of objects in rows using dot-notation in `fieldName`.
1026+
1027+
Example:
1028+
1029+
```html
1030+
<Table
1031+
:columns="[
1032+
{ label: 'User Name', fieldName: 'user.name', sortable: true },
1033+
{ label: 'User Email', fieldName: 'user.email', sortable: true },
1034+
{ label: 'City', fieldName: 'user.address.city', sortable: true },
1035+
{ label: 'Country', fieldName: 'user.address.country' },
1036+
]"
1037+
:data="[
1038+
{ user: { name: 'Alice', email: '[email protected]', address: { city: 'Berlin', country: 'DE' } } },
1039+
{ user: { name: 'Bob', email: '[email protected]', address: { city: 'Paris', country: 'FR' } } },
1040+
{ user: { name: 'Carlos', email: '[email protected]', address: { city: 'Madrid', country: 'ES' } } },
1041+
{ user: { name: 'Dana', email: '[email protected]', address: { city: 'Rome', country: 'IT' } } },
1042+
{ user: { name: 'Eve', email: '[email protected]', address: { city: null, country: 'US' } } },
1043+
]"
1044+
:pageSize="3"
1045+
/>
1046+
```
1047+
1048+
Behavior details:
1049+
- The path is split on dots and resolved step by step: `user.address.city`.
1050+
- Missing or `null` nested values are pushed to the bottom for ascending order (and top for descending) because `null/undefined` are treated as greater than defined values in asc ordering.
1051+
- Works the same for client-side sorting and for server-side loaders: when using an async loader the same `sortField` (e.g. `user.address.city`) is passed so you can implement equivalent ordering on the backend.
1052+
- Date objects at nested paths are detected and compared chronologically.
1053+
- Numeric comparison is stable for mixed numeric strings via Intl.Collator with numeric option.
1054+
1055+
Edge cases to consider in your own data:
1056+
- Deeply missing branches like `user.profile.settings.locale` simply result in `undefined` and will follow the null ordering logic above.
1057+
- Arrays are not traversed; if you need array-specific sorting you should pre-normalize data into scalar fields before passing to the table.
1058+
10271059
### Server-side sorting
10281060

10291061
When you provide an async function to `data`, the table will pass the current sort along with pagination params.
@@ -1049,9 +1081,8 @@ async function loadPageData({ offset, limit, sortField, sortDirection }) {
10491081
if (sortField) url.searchParams.set('sortField', sortField);
10501082
if (sortField && sortDirection) url.searchParams.set('sortDirection', sortDirection);
10511083

1052-
const res = await fetch(url.toString(), { credentials: 'include' });
1053-
const json = await res.json();
1054-
return { data: json.data, total: json.total };
1084+
const { data, total } = callAdminForthApi('getProducts', {limit, offset, sortField, sortDirection});
1085+
return { data, total };
10551086
}
10561087

10571088
<Table

adminforth/spa/src/afcl/Table.vue

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -166,13 +166,11 @@
166166
evenHighlights?: boolean,
167167
pageSize?: number,
168168
isLoading?: boolean,
169-
sortable?: boolean, // enable/disable sorting globally
170169
defaultSortField?: string,
171170
defaultSortDirection?: 'asc' | 'desc',
172171
}>(), {
173172
evenHighlights: true,
174173
pageSize: 5,
175-
sortable: false,
176174
}
177175
);
178176
@@ -189,6 +187,13 @@
189187
const currentSortDirection = ref<'asc' | 'desc'>(props.defaultSortDirection ?? 'asc');
190188
191189
onMounted(() => {
190+
// If defaultSortField points to a non-sortable column, ignore it
191+
if (currentSortField.value) {
192+
const col = props.columns?.find(c => c.fieldName === currentSortField.value);
193+
if (!col || !isColumnSortable(col)) {
194+
currentSortField.value = undefined;
195+
}
196+
}
192197
refresh();
193198
});
194199
@@ -206,7 +211,6 @@
206211
});
207212
208213
watch([() => currentSortField.value, () => currentSortDirection.value], () => {
209-
if (!props.sortable) return;
210214
if (currentPage.value !== 1) currentPage.value = 1;
211215
refresh();
212216
emit('update:sortField', currentSortField.value);
@@ -296,7 +300,8 @@
296300
}
297301
298302
function isColumnSortable(col:{fieldName:string; sortable?:boolean}) {
299-
return props.sortable === true && col.sortable === true;
303+
// Sorting is controlled per column; default is NOT sortable. Enable with `sortable: true`.
304+
return col.sortable === true;
300305
}
301306
302307
function onHeaderClick(col:{fieldName:string; sortable?:boolean}) {
@@ -321,11 +326,11 @@ function getAriaSort(col:{fieldName:string; sortable?:boolean}) {
321326
const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });
322327
323328
function sortArrayData(data:any[], sortField?:string, dir:'asc'|'desc'='asc') {
324-
if (!props.sortable || !sortField) return data;
329+
if (!sortField) return data;
325330
// Helper function to get nested properties by path
326331
const getByPath = (o:any, p:string) => p.split('.').reduce((a:any,k)=>a?.[k], o);
327332
return [...data].sort((a,b) => {
328-
let av = getByPath(a, sortField), bv = getByPath(b, sortField);
333+
const av = getByPath(a, sortField), bv = getByPath(b, sortField);
329334
// Handle null/undefined values
330335
if (av == null && bv == null) return 0;
331336
// Handle null/undefined values

0 commit comments

Comments
 (0)