- The KTable can be used with default sorting functionality, allowing you to sort data on the client side without the need for server requests. There are 4 permissible data types - string,number,date and undefined. Columns declared with undefined data type are not sortable. This example demonstrates a table with default sorting enabled.
+ The KTable offers built-in sorting functionality. There are 4 permissible data types - string,number,date and undefined. Columns declared with undefined data type are not sortable. This example demonstrates a table with sorting enabled via the sortable prop.
+
+
+ Clicking the same header multiple times toggles the sort direction cyclically in the order of ascending, descending, and unsorted.
This is an example to show how slots can be used in KTable. The table currently provides slots for header and cell which can be used to customize the table header and cell content respectively.
This is an example to show how KTable can be used with custom column widths. The column widths are defined in the headers prop. The width property is used to define the overall width of the column. The minWidth defines the minimum width of column, below which the column will not shrink.
+ This is an example to show how to use the defaultSort prop to sort the table based on a particular column upon the initial load. The defaultSort attribute can be used irrespective of the sortable attribute.
+
+
+
+ The defaultSort attribute takes an object with two properties - columnId and direction. The columnId is the unique identifier of the column based on which the table should be sorted. The direction can be either asc or desc.
+
+
+ To make use of defaultSort, please ensure that the disableBuiltinSorting attribute is not set to true.
+
+ For sortable tables, you can use the disableBuiltinSorting prop to disable built-in sort function. This is useful when the table receives already sorted data, for example when sorting is done by backend or a custom sorting function outside the table. In this case, when one of the header sort buttons is clicked, the table won't sort the column itself, but only emit the changeSort event to notify the parent component to handle the sorting logic. The event contains column index of the header and the sort order in its payload.
+
+
+
+ You should not use this attribute if sortable is set to false. If sortable is set to true, then the table component will emit a changeSort event with column index of the header clicked and the sort order to notify the parent component to handle the sorting logic.
+
+
+
+
+
+
+
+
+ data() {
+ return {
+ headers: [
+ { label: 'Name', dataType: 'string', columnId: 'name' },
+ { label: 'Age', dataType: 'number', columnId: 'age' },
+ { label: 'City', dataType: 'string', columnId: 'city' },
+ ],
+ rows: [
+ ['John Doe', 28, 'New York'],
+ ['Jane Smith', 34, 'Los Angeles'],
+ ['Samuel Green', 22, 'Chicago'],
+ ['Alice Johnson', 30, 'Houston'],
+ ['Michael Brown', 45, 'Phoenix'],
+ ['Emily Davis', 27, 'Philadelphia'],
+ ]
+ };
+ },
+ methods: {
+ changeSortHandler(index, sortOrder) {
+ console.log(`changeSort event emitted with index: ${index} and sortOrder: ${sortOrder}`);
+ },
+ },
+
+
+
+
+
+
+
+
@@ -225,9 +379,9 @@
data() {
return {
headers: [
- { label: 'Name', dataType: 'string' },
- { label: 'Age', dataType: 'number' },
- { label: 'City', dataType: 'string' },
+ { label: 'Name', dataType: 'string', columnId: 'name' },
+ { label: 'Age', dataType: 'number', columnId: 'age' },
+ { label: 'City', dataType: 'string', columnId: 'city' },
],
rows: [
['John Doe', 28, 'New York'],
@@ -238,11 +392,11 @@
['Emily Davis', 27, 'Philadelphia'],
],
slotHeaders: [
- { label: 'Name', dataType: 'string' },
- { label: 'Age', dataType: 'number' },
- { label: 'City', dataType: 'string' },
- { label: 'Joined', dataType: 'date' },
- { label: 'Misc', dataType: 'undefined' },
+ { label: 'Name', dataType: 'string', columnId: 'name' },
+ { label: 'Age', dataType: 'number', columnId: 'age' },
+ { label: 'City', dataType: 'string', columnId: 'city' },
+ { label: 'Joined', dataType: 'date', columnId: 'joined' },
+ { label: 'Misc', dataType: 'undefined', columnId: 'misc' },
],
slotRows: [
['John Doe', 28, 'New York', '2022-01-15T00:00:00Z', 'N/A'],
@@ -251,11 +405,23 @@
['Alice Johnson', 30, 'Houston', '2020-07-18T00:00:00Z', 'N/A'],
],
headersWithCustomWidths: [
- { label: 'Name', dataType: 'string', minWidth: '20px', width: '2%' },
- { label: 'Age', dataType: 'number', minWidth: '100px', width: '33%' },
- { label: 'City', dataType: 'string', minWidth: '200px', width: '25%' },
- { label: 'Joined', dataType: 'date', minWidth: '150px', width: '20%' },
- { label: 'Misc', dataType: 'undefined', minWidth: '100px', width: '20%' },
+ { label: 'Name', dataType: 'string', minWidth: '20px', width: '2%', columnId: 'name' },
+ { label: 'Age', dataType: 'number', minWidth: '100px', width: '33%', columnId: 'age' },
+ { label: 'City', dataType: 'string', minWidth: '200px', width: '25%', columnId: 'city' },
+ {
+ label: 'Joined',
+ dataType: 'date',
+ minWidth: '150px',
+ width: '20%',
+ columnId: 'joined',
+ },
+ {
+ label: 'Misc',
+ dataType: 'undefined',
+ minWidth: '100px',
+ width: '20%',
+ columnId: 'misc',
+ },
],
customRows: [
['John Doe', 28, 'New York', '2022-01-15T00:00:00Z', 'N/A'],
@@ -265,6 +431,11 @@
],
};
},
+ methods: {
+ changeSortHandler(index, sortOrder) {
+ console.log(`changeSort event emitted with index: ${index} and sortOrder: ${sortOrder}`);
+ },
+ },
};
diff --git a/lib/KTable/index.vue b/lib/KTable/index.vue
index a8acb8814..6e2c6ab00 100644
--- a/lib/KTable/index.vue
+++ b/lib/KTable/index.vue
@@ -112,24 +112,22 @@
setup(props, { emit }) {
const headers = ref(props.headers);
const rows = ref(props.rows);
- const useLocalSorting = ref(props.sortable && !props.disableDefaultSorting);
+ const disableBuiltinSorting = ref(props.disableBuiltinSorting);
+
+ const defaultSort = ref({
+ index: props.headers.findIndex(h => h.columnId === props.defaultSort.columnId),
+ direction: props.defaultSort.direction,
+ });
+
const {
sortKey,
sortOrder,
sortedRows,
handleSort: localHandleSort,
getAriaSort,
- } = useSorting(headers, rows, useLocalSorting);
+ } = useSorting(headers, rows, defaultSort, disableBuiltinSorting);
- const finalRows = computed(() => {
- if (props.sortable) {
- return useLocalSorting.value ? sortedRows.value : rows.value;
- } else {
- return rows.value;
- }
- });
-
- const isTableEmpty = computed(() => finalRows.value.length === 0);
+ const isTableEmpty = computed(() => sortedRows.value.length === 0);
watch(
() => props.rows,
@@ -142,15 +140,11 @@
if (headers.value[index].dataType === DATA_TYPE_OTHERS) {
return;
}
- if (useLocalSorting.value) {
- localHandleSort(index);
- } else {
- emit(
- 'changeSort',
- index,
- sortOrder.value === SORT_ORDER_ASC ? SORT_ORDER_DESC : SORT_ORDER_ASC
- );
- }
+
+ if (props.disableBuiltinSorting && props.sortable) {
+ // Emit the event to the parent to notify that the sorting has been requested
+ emit('changeSort', index, sortOrder.value);
+ } else localHandleSort(index);
};
const getHeaderStyle = header => {
@@ -159,10 +153,11 @@
if (header.width) style.width = header.width;
return style;
};
+
return {
sortKey,
sortOrder,
- finalRows,
+ finalRows: sortedRows,
handleSort,
getAriaSort,
SORT_ORDER_ASC,
@@ -175,16 +170,22 @@
/* eslint-disable kolibri/vue-no-unused-properties */
props: {
/**
- * An array of objects `{ label, dataType, minWidth, width }`representing the headers of the table. The `dataType` can be one of `'string'`, `'number'`, `'date'`, or `'undefined'`. `label` and `dataType` are required. `minWidth` and `width` are optional.
+ * An array of objects `{ label, dataType, minWidth, width, columnId }`representing the headers of the table. The `dataType` can be one of `'string'`, `'number'`, `'date'`, or `'undefined'`. `label` and `dataType` are required. `minWidth` and `width` are optional. `columnId` is an unique identifier for the column, and can be a `number` or a `string`.
*/
headers: {
type: Array,
required: true,
validator: function(value) {
- return value.every(
- header =>
- ['label', 'dataType'].every(key => key in header) &&
- ['string', 'number', 'date', 'undefined'].includes(header.dataType)
+ const uniqueColumnIds = new Set(value.map(h => h.columnId));
+
+ return (
+ uniqueColumnIds.size == value.length &&
+ value.every(
+ header =>
+ ['label', 'dataType', 'columnId'].every(key => key in header) &&
+ ['string', 'number', 'date', 'undefined'].includes(header.dataType) &&
+ ['string', 'number'].includes(typeof header.columnId)
+ )
);
},
},
@@ -202,14 +203,6 @@
type: String,
required: true,
},
-
- /**
- * Disables the default sorting when sortable is true. Facilitates integration with externally sorted data.
- */
- disableDefaultSorting: {
- type: Boolean,
- default: false,
- },
/**
* Enables or disables sorting functionality for the table headers.
*/
@@ -231,6 +224,33 @@
type: Boolean,
default: false,
},
+ /**
+ * Indicates whether the table is to be sorted by default by any header or not. By default it is an empty object which means no default sorting is to be used. It accepts a configuration object `{ columnId, direction }`. `columnId` references a `columnId` defined for a header in `headers`. This specifies a column by which the table should be sorted when initially loaded. `direction` can be `'asc'` for ascending or `'desc'` for descending sort direction.
+ */
+ defaultSort: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ validator: function(value) {
+ if (Object.keys(value).length === 0) {
+ return true;
+ }
+
+ return (
+ ['columnId', 'direction'].every(key => key in value) &&
+ ['asc', 'desc'].includes(value.direction) &&
+ ['string', 'number'].includes(typeof value.columnId)
+ );
+ },
+ },
+ /**
+ * Disables built-in sort function. This is useful when you want to define your own sorting logic. Refer to the examples above for more details.
+ */
+ disableBuiltinSorting: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
},
data() {
return {
@@ -283,6 +303,23 @@
return colIndex => this.sortable && this.headers[colIndex].dataType !== DATA_TYPE_OTHERS;
},
},
+ watch: {
+ // Use a watcher on props to perform validation on props.
+ // This is required as we need access to multiple props simultaneously in some validations.
+ $props: {
+ immediate: true,
+ handler() {
+ if (this.defaultSort.columnId) {
+ const allHeaderColumnIds = this.headers.map(h => h.columnId);
+ if (!allHeaderColumnIds.includes(this.defaultSort.columnId)) {
+ console.error(
+ `The columnId used for default sorting is ${this.defaultSort.columnId}, but the same was not found to be defined in any headers.`
+ );
+ }
+ }
+ },
+ },
+ },
methods: {
/**
* Takes care of
diff --git a/lib/KTable/useSorting/__tests__/index.spec.js b/lib/KTable/useSorting/__tests__/index.spec.js
index bbaebe33a..12cb77d28 100644
--- a/lib/KTable/useSorting/__tests__/index.spec.js
+++ b/lib/KTable/useSorting/__tests__/index.spec.js
@@ -9,14 +9,14 @@ import useSorting, {
} from '../';
describe('useSorting', () => {
- let headers, rows, useLocalSorting;
+ let headers, rows, defaultSort, disableBuiltinSorting;
beforeEach(() => {
headers = ref([
- { label: 'Name', dataType: DATA_TYPE_STRING },
- { label: 'Age', dataType: DATA_TYPE_NUMERIC },
- { label: 'Birthdate', dataType: DATA_TYPE_DATE },
- { label: 'Other', dataType: DATA_TYPE_OTHERS },
+ { label: 'Name', dataType: DATA_TYPE_STRING, columnId: 'name' },
+ { label: 'Age', dataType: DATA_TYPE_NUMERIC, columnId: 'age' },
+ { label: 'Birthdate', dataType: DATA_TYPE_DATE, columnId: 'birthdate' },
+ { label: 'Other', dataType: DATA_TYPE_OTHERS, columnId: 'other' },
]);
rows = ref([
@@ -25,21 +25,123 @@ describe('useSorting', () => {
['Alice', 28, new Date(1992, 8, 10)],
]);
- useLocalSorting = ref(true);
+ // Disable default sorting
+ defaultSort = ref({
+ index: -1,
+ });
+
+ disableBuiltinSorting = ref(false);
});
- it('should return rows unsorted when useLocalSorting is false', () => {
- useLocalSorting.value = false;
+ describe('default sorting', () => {
+ it('should return rows unsorted by default', () => {
+ defaultSort.value = {
+ index: -1,
+ };
+
+ const { sortedRows } = useSorting(headers, rows, defaultSort, disableBuiltinSorting);
+ expect(sortedRows.value).toEqual(rows.value);
+ });
+
+ it('should sort the rows in ascending correctly for string values', () => {
+ defaultSort.value = {
+ index: 0,
+ direction: 'asc',
+ };
+
+ const { sortedRows } = useSorting(headers, rows, defaultSort, disableBuiltinSorting);
+ expect(sortedRows.value).toEqual([
+ ['Alice', 28, new Date(1992, 8, 10)],
+ ['Jane', 25, new Date(1995, 10, 20)],
+ ['John', 30, new Date(1990, 5, 15)],
+ ]);
+ });
+
+ it('should sort the rows in descending order correctly for string values', () => {
+ defaultSort.value = {
+ index: 0,
+ direction: 'desc',
+ };
+
+ const { sortedRows } = useSorting(headers, rows, defaultSort, disableBuiltinSorting);
+ expect(sortedRows.value).toEqual([
+ ['John', 30, new Date(1990, 5, 15)],
+ ['Jane', 25, new Date(1995, 10, 20)],
+ ['Alice', 28, new Date(1992, 8, 10)],
+ ]);
+ });
+
+ it('should sort the rows correctly for numeric values', () => {
+ defaultSort.value = {
+ index: 1,
+ direction: 'asc',
+ };
+
+ const { sortedRows } = useSorting(headers, rows, defaultSort, disableBuiltinSorting);
+ expect(sortedRows.value).toEqual([
+ ['Jane', 25, new Date(1995, 10, 20)],
+ ['Alice', 28, new Date(1992, 8, 10)],
+ ['John', 30, new Date(1990, 5, 15)],
+ ]);
+ });
+
+ it('should sort the rows correctly for date values', () => {
+ defaultSort.value = {
+ index: 2,
+ direction: 'asc',
+ };
+
+ const { sortedRows } = useSorting(headers, rows, defaultSort, disableBuiltinSorting);
+ expect(sortedRows.value).toEqual([
+ ['John', 30, new Date(1990, 5, 15)],
+ ['Alice', 28, new Date(1992, 8, 10)],
+ ['Jane', 25, new Date(1995, 10, 20)],
+ ]);
+ });
+ });
- const { sortedRows } = useSorting(headers, rows, useLocalSorting);
- expect(sortedRows.value).toEqual(rows.value);
+ describe('disableBuiltinSorting is set to true', () => {
+ it('should return rows unsorted when disableBuiltinSorting is true', () => {
+ disableBuiltinSorting.value = true;
+
+ const { sortedRows } = useSorting(headers, rows, defaultSort, disableBuiltinSorting);
+ expect(sortedRows.value).toEqual(rows.value);
+ });
+
+ it('should return the rows unsorted even when default sorting is enabled', () => {
+ disableBuiltinSorting.value = true;
+ defaultSort.value = {
+ index: 0,
+ direction: 'asc',
+ };
+
+ const { sortedRows } = useSorting(headers, rows, defaultSort, disableBuiltinSorting);
+ expect(sortedRows.value).toEqual(rows.value);
+ });
+
+ it('should not sort rows even when a column is clicked', () => {
+ disableBuiltinSorting.value = true;
+
+ const { handleSort, sortedRows } = useSorting(
+ headers,
+ rows,
+ defaultSort,
+ disableBuiltinSorting
+ );
+ handleSort(0); // Sort by 'Name'
+ expect(sortedRows.value).toEqual(rows.value);
+ });
});
it('should sort rows by string column in ascending order', () => {
- const { handleSort, sortedRows } = useSorting(headers, rows, useLocalSorting);
+ const { handleSort, sortedRows } = useSorting(
+ headers,
+ rows,
+ defaultSort,
+ disableBuiltinSorting
+ );
handleSort(0); // Sort by 'Name'
-
expect(sortedRows.value).toEqual([
['Alice', 28, new Date(1992, 8, 10)],
['Jane', 25, new Date(1995, 10, 20)],
@@ -48,7 +150,12 @@ describe('useSorting', () => {
});
it('should sort rows by numeric column in ascending then descending order then back to default order', () => {
- const { handleSort, sortedRows, sortOrder } = useSorting(headers, rows, useLocalSorting);
+ const { handleSort, sortedRows, sortOrder } = useSorting(
+ headers,
+ rows,
+ defaultSort,
+ disableBuiltinSorting
+ );
handleSort(1); // Sort by 'Age'
expect(sortedRows.value).toEqual([
@@ -66,8 +173,7 @@ describe('useSorting', () => {
]);
expect(sortOrder.value).toBe(SORT_ORDER_DESC);
- handleSort(1); //Sort by 'Age' again to default order
-
+ handleSort(1); // Sort by 'Age' again to default order
expect(sortedRows.value).toEqual([
['John', 30, new Date(1990, 5, 15)],
['Jane', 25, new Date(1995, 10, 20)],
@@ -77,7 +183,12 @@ describe('useSorting', () => {
});
it('should sort rows by date column in ascending order', () => {
- const { handleSort, sortedRows } = useSorting(headers, rows, useLocalSorting);
+ const { handleSort, sortedRows } = useSorting(
+ headers,
+ rows,
+ defaultSort,
+ disableBuiltinSorting
+ );
handleSort(2); // Sort by 'Birthdate'
expect(sortedRows.value).toEqual([
@@ -88,7 +199,12 @@ describe('useSorting', () => {
});
it('should not sort rows when sorting by a column with dataType "undefined"', () => {
- const { handleSort, sortedRows, sortKey } = useSorting(headers, rows, useLocalSorting);
+ const { handleSort, sortedRows, sortKey } = useSorting(
+ headers,
+ rows,
+ defaultSort,
+ disableBuiltinSorting
+ );
handleSort(3); // Attempt to sort by 'Other'
expect(sortedRows.value).toEqual(rows.value);
@@ -96,7 +212,12 @@ describe('useSorting', () => {
});
it('should return correct aria-sort attribute based on current sorting', () => {
- const { handleSort, getAriaSort } = useSorting(headers, rows, useLocalSorting);
+ const { handleSort, getAriaSort } = useSorting(
+ headers,
+ rows,
+ defaultSort,
+ disableBuiltinSorting
+ );
expect(getAriaSort(0)).toBe('none');
@@ -108,7 +229,12 @@ describe('useSorting', () => {
});
it('should reset sortKey and sortOrder when a new column is sorted', () => {
- const { handleSort, sortKey, sortOrder } = useSorting(headers, rows, useLocalSorting);
+ const { handleSort, sortKey, sortOrder } = useSorting(
+ headers,
+ rows,
+ defaultSort,
+ disableBuiltinSorting
+ );
handleSort(0); // Sort by 'Name'
expect(sortKey.value).toBe(0);
diff --git a/lib/KTable/useSorting/index.js b/lib/KTable/useSorting/index.js
index 4b819b996..a2b9c95a5 100644
--- a/lib/KTable/useSorting/index.js
+++ b/lib/KTable/useSorting/index.js
@@ -13,22 +13,36 @@ export const DATA_TYPE_OTHERS = 'undefined';
*
* @param {Ref} headers - Reactive reference to the table headers.
* @param {Ref} rows - Reactive reference to the table rows.
- * @param {Ref} useLocalSorting - Reactive reference to a boolean indicating if local sorting should be used.
+ * @param {Ref