feat(data-table): multiple column sorting (#1035)

* feat(data-table): multiple sorter

* fix(data-table): cell color while sorting

* feat(data-table): multiple sort finished

* feat: add test

* feat(data-table): sort uncontrolled test

* feat(data-table): finish test

* feat(data-table): add controlled mutiple sort

* chore(data-table): clean code

* optimization

* optimization

* optimization

* optimization

* optimization

Co-authored-by: Jiwen Bai <56228105@qq.com>
Co-authored-by: 07akioni <07akioni2@gmail.com>
This commit is contained in:
Mr.Bai 2021-10-06 00:37:21 +08:00 committed by GitHub
parent f19822bdd1
commit 8fcf0558f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 881 additions and 153 deletions

View File

@ -0,0 +1,130 @@
# 受控的多列排序
如果列对象的 sortOrder 属性被设为 'ascend'、'descend' 或者 false表格的排序将为受控状态。
```html
<n-space vertical :size="12">
<n-data-table
ref="table"
:columns="columns"
:data="data"
:pagination="pagination"
@update:sorter="handleUpdateSorter"
/>
</n-space>
```
```js
import { ref, computed } from 'vue'
const data = [
{
key: 0,
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park',
chinese: 98,
math: 60,
english: 70
},
{
key: 1,
name: 'Jim Green',
age: 42,
address: 'London No. 1 Lake Park',
chinese: 98,
math: 66,
english: 89
},
{
key: 2,
name: 'Joe Black',
age: 32,
address: 'Sidney No. 1 Lake Park',
chinese: 98,
math: 66,
english: 89
},
{
key: 3,
name: 'Jim Red',
age: 32,
address: 'London No. 2 Lake Park',
chinese: 88,
math: 99,
english: 89
}
]
export default {
data () {
return {
data: data,
pagination: { pageSize: 5 }
}
},
setup () {
const sortStatesRef = ref([])
const sortKeyMapOrderRef = computed(() =>
sortStatesRef.value.reduce((result, { columnKey, order }) => {
result[columnKey] = order
return result
}, {})
)
const paginationRef = ref({ pageSize: 5 })
const columnsRef = computed(() => [
{
title: 'Name',
key: 'name'
},
{
title: 'Age',
key: 'age',
sortOrder: sortKeyMapOrderRef.value.age || false,
sorter (rowA, rowB) {
return rowA.age - rowB.age
}
},
{
title: 'Chinese Score',
key: 'chinese',
sortOrder: sortKeyMapOrderRef.value.chinese || false,
sorter: {
compare: (a, b) => a.chinese - b.chinese,
multiple: 3
}
},
{
title: 'Math Score',
key: 'math',
sortOrder: sortKeyMapOrderRef.value.math || false,
sorter: {
compare: (a, b) => a.math - b.math,
multiple: 2
}
},
{
title: 'English Score',
sortOrder: sortKeyMapOrderRef.value.english || false,
key: 'english',
sorter: {
compare: (a, b) => a.english - b.english,
multiple: 1
}
}
])
function handleUpdateSorter (sorters) {
console.log(sorters)
sortStatesRef.value = [].concat(sorters)
}
return {
columns: columnsRef,
handleUpdateSorter,
data,
pagination: paginationRef
}
}
}
```

View File

@ -19,12 +19,14 @@ size
row-props row-props
merge-cell merge-cell
filter-and-sorter filter-and-sorter
multiple-sorter
select select
custom-select custom-select
group-header group-header
controlled-page controlled-page
controlled-filter controlled-filter
controlled-sorter controlled-sorter
controlled-multiple-sorter
fixed-header fixed-header
fixed-header-column fixed-header-column
summary summary

View File

@ -0,0 +1,143 @@
# 多列排序
如果仅想使用多列排序的 UI`sorter` 不传 `compare` 函数即可。
```html
<n-space vertical :size="12">
<n-space>
<n-button @click="sortName">Sort By Name (Ascend)</n-button>
<n-button @click="filterAddress">Filter Address (London)</n-button>
<n-button @click="clearFilters">Clear Filters</n-button>
<n-button @click="clearSorter">Clear Sorter</n-button>
</n-space>
<n-data-table
ref="table"
:columns="columns"
:data="data"
:pagination="pagination"
/>
</n-space>
```
```js
const columns = [
{
title: 'Name',
key: 'name'
},
{
title: 'Age',
key: 'age',
sorter: (row1, row2) => row1.age - row2.age
},
{
title: 'Chinese Score',
key: 'chinese',
defaultSortOrder: false,
sorter: {
compare: (a, b) => a.chinese - b.chinese,
multiple: 3
}
},
{
title: 'Math Score',
defaultSortOrder: false,
key: 'math',
sorter: {
compare: (a, b) => a.math - b.math,
multiple: 2
}
},
{
title: 'English Score',
defaultSortOrder: false,
key: 'english',
sorter: {
compare: (a, b) => a.english - b.english,
multiple: 1
}
},
{
title: 'Address',
key: 'address',
filterOptions: [
{
label: 'London',
value: 'London'
},
{
label: 'New York',
value: 'New York'
}
],
filter (value, row) {
return ~row.address.indexOf(value)
}
}
]
const data = [
{
key: 0,
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park',
chinese: 98,
math: 60,
english: 70
},
{
key: 1,
name: 'Jim Green',
age: 42,
address: 'London No. 1 Lake Park',
chinese: 98,
math: 66,
english: 89
},
{
key: 2,
name: 'Joe Black',
age: 32,
address: 'Sidney No. 1 Lake Park',
chinese: 98,
math: 66,
english: 89
},
{
key: 3,
name: 'Jim Red',
age: 32,
address: 'London No. 2 Lake Park',
chinese: 88,
math: 99,
english: 89
}
]
export default {
data () {
return {
data: data,
columns,
pagination: { pageSize: 5 }
}
},
methods: {
filterAddress () {
this.$refs.table.filter({
address: ['London']
})
},
sortName () {
this.$refs.table.sort('name', 'ascend')
},
clearFilters () {
this.$refs.table.filter(null)
},
clearSorter () {
this.$refs.table.sort(null)
}
}
}
```

View File

@ -19,15 +19,19 @@ export default defineComponent({
const { mergedSortStateRef, mergedClsPrefixRef } = inject( const { mergedSortStateRef, mergedClsPrefixRef } = inject(
dataTableInjectionKey dataTableInjectionKey
)! )!
const sortStateRef = mergedSortStateRef const sortStateRef = computed(() =>
mergedSortStateRef.value.find(
(state) => state.columnKey === props.column.key
)
)
const activeRef = computed(() => { const activeRef = computed(() => {
const { value } = sortStateRef return sortStateRef.value !== undefined
if (value) return value.columnKey === props.column.key
return false
}) })
const mergedSortOrderRef = computed(() => { const mergedSortOrderRef = computed(() => {
const { value } = sortStateRef if (sortStateRef.value && activeRef.value) {
if (value) return activeRef.value ? value.order : false return sortStateRef.value.order
}
return false return false
}) })
const mergedRenderSorterRef = computed(() => { const mergedRenderSorterRef = computed(() => {

View File

@ -22,7 +22,7 @@ import {
MainTableBodyRef, MainTableBodyRef,
TmNode TmNode
} from '../interface' } from '../interface'
import { createRowClassName, getColKey } from '../utils' import { createRowClassName, getColKey, isColumnSorting } from '../utils'
import Cell from './Cell' import Cell from './Cell'
import ExpandTrigger from './ExpandTrigger' import ExpandTrigger from './ExpandTrigger'
import RenderSafeCheckbox from './BodyCheckbox' import RenderSafeCheckbox from './BodyCheckbox'
@ -384,10 +384,6 @@ export default defineComponent({
paginatedData.forEach((tmNode, rowIndex) => { paginatedData.forEach((tmNode, rowIndex) => {
rowIndexToKey[rowIndex] = tmNode.key rowIndexToKey[rowIndex] = tmNode.key
}) })
const sorterKey =
!!mergedSortState &&
mergedSortState.order &&
mergedSortState.columnKey
let mergedData: RowRenderInfo[] let mergedData: RowRenderInfo[]
@ -503,7 +499,7 @@ export default defineComponent({
isSummary && `${mergedClsPrefix}-data-table-td--summary`, isSummary && `${mergedClsPrefix}-data-table-td--summary`,
((hoverKey !== null && ((hoverKey !== null &&
cordKey[rowIndex][colIndex].includes(hoverKey)) || cordKey[rowIndex][colIndex].includes(hoverKey)) ||
(sorterKey !== false && sorterKey === colKey)) && isColumnSorting(column, mergedSortState)) &&
`${mergedClsPrefix}-data-table-td--hover`, `${mergedClsPrefix}-data-table-td--hover`,
column.fixed && column.fixed &&
`${mergedClsPrefix}-data-table-td--fixed-${column.fixed}`, `${mergedClsPrefix}-data-table-td--fixed-${column.fixed}`,

View File

@ -9,7 +9,8 @@ import {
isColumnSortable, isColumnSortable,
isColumnFilterable, isColumnFilterable,
createNextSorter, createNextSorter,
getColKey getColKey,
isColumnSorting
} from '../utils' } from '../utils'
import { import {
TableExpandColumn, TableExpandColumn,
@ -72,7 +73,10 @@ export default defineComponent({
): void { ): void {
if (happensIn(e, 'dataTableFilter')) return if (happensIn(e, 'dataTableFilter')) return
if (!isColumnSortable(column)) return if (!isColumnSortable(column)) return
const activeSorter = mergedSortStateRef.value const activeSorter =
mergedSortStateRef.value.find(
(state) => state.columnKey === column.key
) || null
const nextSorter = createNextSorter(column, activeSorter) const nextSorter = createNextSorter(column, activeSorter)
doUpdateSorter(nextSorter) doUpdateSorter(nextSorter)
} }
@ -109,7 +113,6 @@ export default defineComponent({
currentPage, currentPage,
allRowsChecked, allRowsChecked,
someRowsChecked, someRowsChecked,
mergedSortState,
rows, rows,
cols, cols,
mergedTheme, mergedTheme,
@ -118,6 +121,7 @@ export default defineComponent({
discrete, discrete,
mergedTableLayout, mergedTableLayout,
headerCheckboxDisabled, headerCheckboxDisabled,
mergedSortState,
handleColHeaderClick, handleColHeaderClick,
handleCheckboxUpdateChecked handleCheckboxUpdateChecked
} = this } = this
@ -151,8 +155,7 @@ export default defineComponent({
`${mergedClsPrefix}-data-table-th--fixed-${column.fixed}`, `${mergedClsPrefix}-data-table-th--fixed-${column.fixed}`,
{ {
[`${mergedClsPrefix}-data-table-th--hover`]: [`${mergedClsPrefix}-data-table-th--hover`]:
mergedSortState?.order && isColumnSorting(column, mergedSortState),
mergedSortState.columnKey === key,
[`${mergedClsPrefix}-data-table-th--filterable`]: [`${mergedClsPrefix}-data-table-th--filterable`]:
isColumnFilterable(column), isColumnFilterable(column),
[`${mergedClsPrefix}-data-table-th--sortable`]: [`${mergedClsPrefix}-data-table-th--sortable`]:

View File

@ -34,8 +34,13 @@ export type CreateRowProps<T = InternalRowData> = (
row: T, row: T,
index: number index: number
) => HTMLAttributes ) => HTMLAttributes
export type CompareFn<T = InternalRowData> = (row1: T, row2: T) => number
export type Sorter<T = InternalRowData> = CompareFn<T> | SorterMultiple<T>
export interface SorterMultiple<T = InternalRowData> {
multiple: number
compare?: CompareFn<T> | 'default'
}
export type Sorter<T = InternalRowData> = (row1: T, row2: T) => number
export type Filter<T = InternalRowData> = ( export type Filter<T = InternalRowData> = (
filterOptionValue: FilterOptionValue, filterOptionValue: FilterOptionValue,
row: T row: T
@ -174,7 +179,7 @@ export interface DataTableInjection {
mergedCurrentPageRef: Ref<number> mergedCurrentPageRef: Ref<number>
someRowsCheckedRef: Ref<boolean> someRowsCheckedRef: Ref<boolean>
allRowsCheckedRef: Ref<boolean> allRowsCheckedRef: Ref<boolean>
mergedSortStateRef: Ref<SortState | null> mergedSortStateRef: Ref<SortState[]>
mergedFilterStateRef: Ref<FilterState> mergedFilterStateRef: Ref<FilterState>
loadingRef: Ref<boolean> loadingRef: Ref<boolean>
rowClassNameRef: Ref<string | CreateRowClassName | undefined> rowClassNameRef: Ref<string | CreateRowClassName | undefined>
@ -231,7 +236,7 @@ export type RenderFilterMenu = (actions: { hide: () => void }) => VNodeChild
export type OnUpdateExpandedRowKeys = (keys: RowKey[]) => void export type OnUpdateExpandedRowKeys = (keys: RowKey[]) => void
export type OnUpdateCheckedRowKeys = (keys: RowKey[]) => void export type OnUpdateCheckedRowKeys = (keys: RowKey[]) => void
export type OnUpdateSorter = (sortState: SortState | null) => void export type OnUpdateSorter = (sortState: SortState | SortState[] | null) => void
export type OnUpdateFilters = ( export type OnUpdateFilters = (
filterState: FilterState, filterState: FilterState,
sourceColumn?: TableBaseColumn sourceColumn?: TableBaseColumn

View File

@ -0,0 +1,250 @@
import { computed, ref, ComputedRef } from 'vue'
import {
ColumnKey,
InternalRowData,
SortOrder,
SortState,
TmNode,
TableBaseColumn,
TableExpandColumn,
TableSelectionColumn,
CompareFn
} from './interface'
import { getFlagOfOrder } from './utils'
import { call } from '../../_utils'
import type { DataTableSetupProps } from './DataTable'
function getMultiplePriority ({
sorter
}: {
sorter: TableBaseColumn['sorter']
}): number | false {
if (typeof sorter === 'object' && typeof sorter.multiple === 'number') {
return sorter.multiple
}
return false
}
function getSortFunction (
sorter: TableBaseColumn['sorter'],
columnKey: ColumnKey
): CompareFn | false {
if (
columnKey &&
(sorter === undefined ||
sorter === 'default' ||
(typeof sorter === 'object' && sorter.compare === 'default'))
) {
return getDefaultSorterFn(columnKey)
}
if (typeof sorter === 'function') {
return sorter
}
if (
sorter &&
typeof sorter === 'object' &&
sorter.compare &&
sorter.compare !== 'default'
) {
return sorter.compare
}
return false
}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
function getDefaultSorterFn (columnKey: ColumnKey) {
return (row1: InternalRowData, row2: InternalRowData) => {
const value1 = row1[columnKey]
const value2 = row2[columnKey]
if (typeof value1 === 'number' && typeof value2 === 'number') {
return value1 - value2
} else if (typeof value1 === 'string' && typeof value2 === 'string') {
return value1.localeCompare(value2)
}
return 0
}
}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function useSorter (
props: DataTableSetupProps,
{
dataRelatedColsRef,
filteredDataRef
}: {
dataRelatedColsRef: ComputedRef<
Array<TableSelectionColumn | TableBaseColumn | TableExpandColumn>
>
filteredDataRef: ComputedRef<TmNode[]>
}
) {
const uncontrolledSortStateRef = ref<SortState[]>([])
const mergedSortStateRef = computed(() => {
// If one of the columns's sort order is false or 'ascend' or 'descend',
// the table's controll functionality should work in controlled manner.
const columnsWithControlledSortOrder = dataRelatedColsRef.value.filter(
(column) =>
column.type !== 'selection' &&
column.sorter !== undefined &&
(column.sortOrder === 'ascend' ||
column.sortOrder === 'descend' ||
column.sortOrder === false)
)
// if multiple columns are controlled sortable, then we need to find columns with active sortOrder
const columnToSort: TableBaseColumn[] | undefined = (
columnsWithControlledSortOrder as TableBaseColumn[]
).filter((col: TableBaseColumn) => col.sortOrder !== false)
if (columnToSort.length) {
return columnToSort.map((column) => {
return {
columnKey: column.key,
// column to sort has controlled sorter
// sorter && sort order won't be undefined
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
order: column.sortOrder!,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
sorter: column.sorter!
}
})
}
if (columnsWithControlledSortOrder.length) return []
return uncontrolledSortStateRef.value
})
const sortedDataRef = computed<TmNode[]>(() => {
const activeSorters = mergedSortStateRef.value.slice().sort((a, b) => {
const item1Priority = getMultiplePriority(a) || 0
const item2Priority = getMultiplePriority(b) || 0
return item2Priority - item1Priority
})
if (activeSorters.length) {
const filteredData = filteredDataRef.value.slice()
return filteredData.sort((tmNode1, tmNode2) => {
let compareResult = 0
activeSorters.some((sorterState) => {
const { columnKey, sorter, order } = sorterState
const compareFn = getSortFunction(sorter, columnKey)
if (compareFn && order) {
compareResult = compareFn(tmNode1.rawNode, tmNode2.rawNode)
if (compareResult !== 0) {
compareResult = compareResult * getFlagOfOrder(order)
return true
}
}
return false
})
return compareResult
})
}
return filteredDataRef.value
})
dataRelatedColsRef.value.forEach((column) => {
if (column.sorter !== undefined) {
addSortSate({
columnKey: column.key,
sorter: column.sorter,
order: column.defaultSortOrder ?? false
})
}
})
function getUpdatedSorterState (
sortState: SortState | null
): SortState | null | SortState[] {
let currentSortState = mergedSortStateRef.value.slice()
// Multiple sorter
if (
sortState &&
getMultiplePriority({ sorter: sortState.sorter }) !== false
) {
// clear column is not multiple sort
currentSortState = currentSortState.filter(
(sortState) =>
getMultiplePriority({ sorter: sortState.sorter }) !== false
)
updateSortInSortStates(currentSortState, sortState)
return currentSortState
} else if (sortState) {
// single sorter
return sortState
}
// no sorter
return null
}
function doUpdateSorter (sortState: SortState | null): void {
const {
'onUpdate:sorter': _onUpdateSorter,
onUpdateSorter,
onSorterChange
} = props
const updateSorterState: SortState | SortState[] | null =
getUpdatedSorterState(sortState)
if (_onUpdateSorter) call(_onUpdateSorter, updateSorterState)
if (onUpdateSorter) call(onUpdateSorter, updateSorterState)
if (onSorterChange) call(onSorterChange, updateSorterState)
if (Array.isArray(updateSorterState)) {
uncontrolledSortStateRef.value = updateSorterState
} else if (updateSorterState) {
uncontrolledSortStateRef.value = [updateSorterState]
} else {
uncontrolledSortStateRef.value = []
}
}
function sort (columnKey: ColumnKey, order: SortOrder = 'ascend'): void {
if (!columnKey) {
clearSorter()
} else {
const columnToSort = dataRelatedColsRef.value.find(
(column) =>
column.type !== 'selection' &&
column.type !== 'expand' &&
column.key === columnKey
)
if (!columnToSort || !columnToSort.sorter) return
const sorter = columnToSort.sorter
doUpdateSorter({
columnKey,
sorter,
order: order
})
}
}
function clearSorter (): void {
doUpdateSorter(null)
}
function updateSortInSortStates (
sortStates: SortState[],
sortState: SortState
): void {
const index = sortStates.findIndex(
(state) => sortState?.columnKey && state.columnKey === sortState.columnKey
)
if (index !== undefined && index >= 0) {
sortStates[index] = sortState
} else {
sortStates.push(sortState)
}
}
function addSortSate (sortState: SortState): void {
updateSortInSortStates(uncontrolledSortStateRef.value, sortState)
}
return {
clearSorter,
sort,
sortedDataRef,
mergedSortStateRef,
uncontrolledSortStateRef,
doUpdateSorter
}
}

View File

@ -8,7 +8,6 @@ import {
FilterOptionValue, FilterOptionValue,
FilterState, FilterState,
SortOrder, SortOrder,
SortState,
TableBaseColumn, TableBaseColumn,
TableSelectionColumn, TableSelectionColumn,
InternalRowData, InternalRowData,
@ -16,10 +15,10 @@ import {
TableExpandColumn, TableExpandColumn,
RowKey RowKey
} from './interface' } from './interface'
import { createShallowClonedObject, getFlagOfOrder } from './utils' import { createShallowClonedObject } from './utils'
import { PaginationProps } from '../../pagination/src/Pagination' import { PaginationProps } from '../../pagination/src/Pagination'
import { call, warn } from '../../_utils' import { call, warn } from '../../_utils'
import { useSorter } from './use-sorter'
// useTableData combines filter, sorter and pagination // useTableData combines filter, sorter and pagination
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
@ -70,92 +69,9 @@ export function useTableData (
}) })
const uncontrolledFilterStateRef = ref<FilterState>({}) const uncontrolledFilterStateRef = ref<FilterState>({})
const uncontrolledSortStateRef = ref<SortState | null>(null)
const uncontrolledCurrentPageRef = ref(1) const uncontrolledCurrentPageRef = ref(1)
const uncontrolledPageSizeRef = ref(10) const uncontrolledPageSizeRef = ref(10)
dataRelatedColsRef.value.forEach((column) => {
if (column.sorter !== undefined) {
uncontrolledSortStateRef.value = {
columnKey: column.key,
sorter: column.sorter,
order: column.defaultSortOrder ?? false
}
}
if (column.filter) {
const defaultFilterOptionValues = column.defaultFilterOptionValues
if (column.filterMultiple) {
uncontrolledFilterStateRef.value[column.key] =
defaultFilterOptionValues || []
} else if (defaultFilterOptionValues !== undefined) {
// this branch is for compatibility, someone may use `values` in single filter mode
uncontrolledFilterStateRef.value[column.key] =
defaultFilterOptionValues === null ? [] : defaultFilterOptionValues
} else {
uncontrolledFilterStateRef.value[column.key] =
column.defaultFilterOptionValue ?? null
}
}
})
const controlledCurrentPageRef = computed(() => {
const { pagination } = props
if (pagination === false) return undefined
return pagination.page
})
const controlledPageSizeRef = computed(() => {
const { pagination } = props
if (pagination === false) return undefined
return pagination.pageSize
})
const mergedCurrentPageRef = useMergedState(
controlledCurrentPageRef,
uncontrolledCurrentPageRef
)
const mergedPageSizeRef = useMergedState(
controlledPageSizeRef,
uncontrolledPageSizeRef
)
const mergedPageCountRef = computed(() => {
const { pagination } = props
if (pagination) {
const { pageCount } = pagination
if (pageCount !== undefined) return pageCount
}
return undefined
})
const mergedSortStateRef = computed<SortState | null>(() => {
// If one of the columns's sort order is false or 'ascend' or 'descend',
// the table's controll functionality should work in controlled manner.
const columnsWithControlledSortOrder = dataRelatedColsRef.value.filter(
(column) =>
column.type !== 'selection' &&
column.sorter !== undefined &&
(column.sortOrder === 'ascend' ||
column.sortOrder === 'descend' ||
column.sortOrder === false)
)
// if multiple column is controlled sortable, then we need to find a column with active sortOrder
const columnToSort: TableBaseColumn | undefined = (
columnsWithControlledSortOrder as TableBaseColumn[]
).filter((col: TableBaseColumn) => col.sortOrder !== false)[0]
if (columnToSort) {
return {
columnKey: columnToSort.key,
// column to sort has controlled sorter
// sorter && sort order won't be undefined
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
order: columnToSort.sortOrder!,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
sorter: columnToSort.sorter!
}
}
if (columnsWithControlledSortOrder.length) return null
return uncontrolledSortStateRef.value
})
const mergedFilterStateRef = computed<FilterState>(() => { const mergedFilterStateRef = computed<FilterState>(() => {
const columnsWithControlledFilter = dataRelatedColsRef.value.filter( const columnsWithControlledFilter = dataRelatedColsRef.value.filter(
(column) => { (column) => {
@ -242,41 +158,56 @@ export function useTableData (
: [] : []
}) })
const sortedDataRef = computed<TmNode[]>(() => { const { sortedDataRef, doUpdateSorter, mergedSortStateRef } = useSorter(
const activeSorter = mergedSortStateRef.value props,
if (activeSorter) { {
// When async, mergedSortState.sorter should be true dataRelatedColsRef,
// and we sort nothing, just return the filtered data filteredDataRef
if (activeSorter.sorter === true || activeSorter.sorter === false) {
return filteredDataRef.value
}
const filteredData = filteredDataRef.value.slice(0)
const columnKey = activeSorter.columnKey
// 1 for asc
// -1 for desc
const order = activeSorter.order
const sorter =
activeSorter.sorter === undefined || activeSorter.sorter === 'default'
? (row1: InternalRowData, row2: InternalRowData) => {
const value1 = row1[columnKey]
const value2 = row2[columnKey]
if (typeof value1 === 'number' && typeof value2 === 'number') {
return value1 - value2
} else if (
typeof value1 === 'string' &&
typeof value2 === 'string'
) {
return value1.localeCompare(value2)
}
return 0
}
: activeSorter.sorter
return filteredData.sort(
(tmNode1, tmNode2) =>
getFlagOfOrder(order) * sorter(tmNode1.rawNode, tmNode2.rawNode)
)
} }
return filteredDataRef.value )
dataRelatedColsRef.value.forEach((column) => {
if (column.filter) {
const defaultFilterOptionValues = column.defaultFilterOptionValues
if (column.filterMultiple) {
uncontrolledFilterStateRef.value[column.key] =
defaultFilterOptionValues || []
} else if (defaultFilterOptionValues !== undefined) {
// this branch is for compatibility, someone may use `values` in single filter mode
uncontrolledFilterStateRef.value[column.key] =
defaultFilterOptionValues === null ? [] : defaultFilterOptionValues
} else {
uncontrolledFilterStateRef.value[column.key] =
column.defaultFilterOptionValue ?? null
}
}
})
const controlledCurrentPageRef = computed(() => {
const { pagination } = props
if (pagination === false) return undefined
return pagination.page
})
const controlledPageSizeRef = computed(() => {
const { pagination } = props
if (pagination === false) return undefined
return pagination.pageSize
})
const mergedCurrentPageRef = useMergedState(
controlledCurrentPageRef,
uncontrolledCurrentPageRef
)
const mergedPageSizeRef = useMergedState(
controlledPageSizeRef,
uncontrolledPageSizeRef
)
const mergedPageCountRef = computed(() => {
const { pagination } = props
if (pagination) {
const { pageCount } = pagination
if (pageCount !== undefined) return pageCount
}
return undefined
}) })
const paginatedDataRef = computed<TmNode[]>(() => { const paginatedDataRef = computed<TmNode[]>(() => {
@ -365,17 +296,7 @@ export function useTableData (
if (_onUpdatePageSize) call(_onUpdatePageSize, pageSize) if (_onUpdatePageSize) call(_onUpdatePageSize, pageSize)
uncontrolledPageSizeRef.value = pageSize uncontrolledPageSizeRef.value = pageSize
} }
function doUpdateSorter (sortState: SortState | null): void {
const {
'onUpdate:sorter': _onUpdateSorter,
onUpdateSorter,
onSorterChange
} = props
if (_onUpdateSorter) call(_onUpdateSorter, sortState)
if (onUpdateSorter) call(onUpdateSorter, sortState)
if (onSorterChange) call(onSorterChange, sortState)
uncontrolledSortStateRef.value = sortState
}
function doUpdateFilters ( function doUpdateFilters (
filters: FilterState, filters: FilterState,
sourceColumn?: TableBaseColumn sourceColumn?: TableBaseColumn
@ -397,6 +318,7 @@ export function useTableData (
if (!columnKey) { if (!columnKey) {
clearSorter() clearSorter()
} else { } else {
// TODO:
const columnToSort = dataRelatedColsRef.value.find( const columnToSort = dataRelatedColsRef.value.find(
(column) => (column) =>
column.type !== 'selection' && column.type !== 'selection' &&

View File

@ -108,3 +108,15 @@ export function createNextSorter (
} }
} }
} }
export function isColumnSorting (
column: TableColumn,
mergedSortState: SortState[]
): boolean {
return (
mergedSortState.find(
(state) =>
state.columnKey === (column as TableBaseColumn).key && state.order
) !== undefined
)
}

View File

@ -1,6 +1,6 @@
import { h, HTMLAttributes, nextTick } from 'vue' import { h, HTMLAttributes, nextTick, ref } from 'vue'
import { mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import { NDataTable } from '../index' import { DataTableInst, NDataTable } from '../index'
import type { DataTableColumns } from '../index' import type { DataTableColumns } from '../index'
import { NButton, NButtonGroup } from '../../button' import { NButton, NButtonGroup } from '../../button'
@ -319,6 +319,267 @@ describe('n-data-table', () => {
expect(wrapper.find('tbody .n-data-table-tr').classes()).toContain('0-test') expect(wrapper.find('tbody .n-data-table-tr').classes()).toContain('0-test')
}) })
describe('should work with multiple sorter', () => {
interface UserData {
name: string
age: number
address: string
chinese: number
math: number
english: number
}
const columns: DataTableColumns<UserData> = [
{
title: 'Name',
key: 'name'
},
{
title: () => h('span', { id: 'age-title' }, 'Age'),
className: 'age-col',
key: 'age',
sorter: (a, b) => a.age - b.age
},
{
title: () => <span id="chinese-title">Chinese Score</span>,
key: 'chinese',
defaultSortOrder: false,
className: 'chinese-col',
sorter: {
compare: (a, b) => a.chinese - b.chinese,
multiple: 3
}
},
{
title: () => <span id="math-title">Math Score</span>,
defaultSortOrder: false,
className: 'math-col',
key: 'math',
sorter: {
compare: (a, b) => a.math - b.math,
multiple: 2
}
},
{
title: () => <span id="english-title">English Score</span>,
className: 'english-col',
defaultSortOrder: false,
key: 'english',
sorter: {
compare: (a, b) => a.english - b.english,
multiple: 1
}
},
{
title: 'Address',
key: 'address',
filterOptions: [
{
label: 'London',
value: 'London'
},
{
label: 'New York',
value: 'New York'
},
{
label: 'Sidney',
value: 'Sidney'
}
],
filter (value: any, row) {
return row.address.includes(value)
}
}
]
const data = [
{
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park',
chinese: 98,
math: 60,
english: 70
},
{
name: 'Jim Green',
age: 42,
address: 'London No. 1 Lake Park',
chinese: 98,
math: 66,
english: 89
},
{
name: 'Joe Black',
age: 32,
address: 'Sidney No. 1 Lake Park',
chinese: 98,
math: 66,
english: 89
},
{
name: 'Jim Red',
age: 32,
address: 'London No. 2 Lake Park',
chinese: 88,
math: 99,
english: 89
}
]
const tableRef = ref<DataTableInst | null>(null)
const wrapper = mount(
() => <NDataTable ref={tableRef} columns={columns} data={data} />,
{
attachTo: document.body
}
)
const checkIsMatched = async (
colClassName: string,
target: number[]
): Promise<boolean> => {
const cols = await wrapper.findAll(colClassName)
const colNums = cols.slice(1).map((item) => Number(item.text()))
const matchResult = String(colNums) === String(target)
if (!matchResult) {
console.log(colClassName, String(colNums), String(target))
}
return String(colNums) === String(target)
}
const checkScoreIsMatched = async (
targets: [number[], number[], number[]]
): Promise<boolean> => {
const matchResult =
(await checkIsMatched('.chinese-col', targets[0])) &&
(await checkIsMatched('.math-col', targets[1])) &&
(await checkIsMatched('.english-col', targets[2]))
return matchResult
}
const chineseDom: HTMLElement | null =
document.querySelector('#chinese-title')
const mathDom: HTMLElement | null = document.querySelector('#math-title')
const englishDom: HTMLElement | null =
document.querySelector('#english-title')
const ageDom: HTMLElement | null = document.querySelector('#age-title')
it('chinese: descend, math: false, english: false', async () => {
await chineseDom?.click()
expect(
await checkScoreIsMatched([
[98, 98, 98, 88],
[60, 66, 66, 99],
[70, 89, 89, 89]
])
).toEqual(true)
})
it('chinese: descend, math: descend, english: false', async () => {
await mathDom?.click()
expect(
await checkScoreIsMatched([
[98, 98, 98, 88],
[66, 66, 60, 99],
[89, 89, 70, 89]
])
).toEqual(true)
})
it('chinese: descend, math: descend, english: descend', async () => {
await englishDom?.click()
expect(
await checkScoreIsMatched([
[98, 98, 98, 88],
[66, 66, 60, 99],
[89, 89, 70, 89]
])
).toEqual(true)
})
it('chinese: ascend, math: descend, english: descend', async () => {
await chineseDom?.click()
expect(
await checkScoreIsMatched([
[88, 98, 98, 98],
[99, 66, 66, 60],
[89, 89, 89, 70]
])
).toEqual(true)
})
it('chinese: false, math: descend, english: descend', async () => {
await chineseDom?.click()
expect(
await checkScoreIsMatched([
[88, 98, 98, 98],
[99, 66, 66, 60],
[89, 89, 89, 70]
])
).toEqual(true)
})
it('chinese: false, math: ascend, english: descend', async () => {
await mathDom?.click()
expect(
await checkScoreIsMatched([
[98, 98, 98, 88],
[60, 66, 66, 99],
[70, 89, 89, 89]
])
).toEqual(true)
})
it('chinese: false, math: false, english: descend', async () => {
await mathDom?.click()
expect(
await checkScoreIsMatched([
[98, 98, 88, 98],
[66, 66, 99, 60],
[89, 89, 89, 70]
])
).toEqual(true)
})
it('chinese: descend, math: false, english: descend', async () => {
await chineseDom?.click()
expect(
await checkScoreIsMatched([
[98, 98, 98, 88],
[66, 66, 60, 99],
[89, 89, 70, 89]
])
).toEqual(true)
})
it('filter: Sidney and New York', async () => {
if (tableRef.value) {
tableRef.value.filter({
address: ['Sidney', 'New York']
})
}
await nextTick()
const result = await checkScoreIsMatched([
[98, 98],
[66, 60],
[89, 70]
])
expect(result).toEqual(true)
if (tableRef.value) {
tableRef.value.filter(null)
}
})
it('age: descend', async () => {
await ageDom?.click()
const result =
(await checkIsMatched('.age-col', [42, 32, 32, 32])) &&
(await checkScoreIsMatched([
[98, 98, 98, 88],
[66, 60, 66, 99],
[89, 70, 89, 89]
]))
expect(result).toEqual(true)
})
})
it('should work with `indent` prop', async () => { it('should work with `indent` prop', async () => {
const columns = [ const columns = [
{ {