feat(data-table): expandable rows

This commit is contained in:
07akioni 2021-03-26 13:47:53 +08:00
parent 02adb504a8
commit 0756513259
16 changed files with 600 additions and 169 deletions

View File

@ -1,5 +1,11 @@
# CHANGELOG
## Pending
### Feats
- `n-data-table` supports expanding rows.
## 2.1.3
### Fixes

View File

@ -1,5 +1,11 @@
# CHANGELOG
## Pending
### Feats
- `n-data-table` 支持行展开
## 2.1.3
### Fixes

View File

@ -0,0 +1,110 @@
# Expand Rows
```html
<n-data-table :columns="columns" :data="data" :pagination="pagination" />
```
```js
import { h, defineComponent } from 'vue'
import { NTag, NButton, useMessage } from 'naive-ui'
const createColumns = ({ sendMail }) => {
return [
{
type: 'expand',
expandable: (_, index) => index !== 1,
renderExpand: (rowData) => {
return `${rowData.name} is a good guy.`
}
},
{
title: 'Name',
key: 'name'
},
{
title: 'Age',
key: 'age'
},
{
title: 'Address',
key: 'address'
},
{
title: 'Tags',
key: 'tags',
render (row) {
const tags = row.tags.map((tagKey) => {
return h(
NTag,
{
style: {
marginRight: '6px'
},
type: 'info'
},
{
default: () => tagKey
}
)
})
return tags
}
},
{
title: 'Action',
key: 'actions',
render (row) {
return h(
NButton,
{
size: 'small',
onClick: () => sendMail(row)
},
{ default: () => 'Send Email' }
)
}
}
]
}
const createData = () => [
{
key: 0,
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park',
tags: ['nice', 'developer']
},
{
key: 1,
name: 'Jim Green',
age: 42,
address: 'London No. 1 Lake Park',
tags: ['loser']
},
{
key: 2,
name: 'Joe Black',
age: 32,
address: 'Sidney No. 1 Lake Park',
tags: ['cool', 'teacher']
}
]
export default defineComponent({
setup () {
const message = useMessage()
return {
data: createData(),
columns: createColumns({
sendMail (rowData) {
message.info('send mail to ' + rowData.name)
}
}),
pagination: {
pageSize: 10
}
}
}
})
```

View File

@ -26,6 +26,7 @@ fixed-header
fixed-header-column
ellipsis
ellipsis-tooltip
expand
render-header
custom-style
ajax-usage
@ -47,7 +48,7 @@ custom-filter-menu
| min-height | `number \| string` | `undefined` | The min-height of the table. |
| pagination | `false \| Object` | `false` | See [Pagination props](n-pagination#Props) |
| paging | `boolean` | `true` | If data-table do automatic paging. You may set it to `false` in async usage. |
| row-class-name | `string \| (rowData: Object, index : number) => string \| Object` | `undefined` | |
| row-class-name | `string \| (rowData: Object, rowIndex : number) => string \| Object` | `undefined` | |
| row-key | `(rowData: Object) => number \| string` | `undefined` | Generate the key of the row by row data (if you don't want to set the key) |
| scroll-x | `number \| string` | `undefined` | If columns are horizontal fixed, scroll-x need to be set |
| single-column | `boolean` | `false` | |
@ -84,8 +85,9 @@ These methods can help you control table in an uncontrolled manner. However, it'
| defaultFilterOptionValue | `string \| number \| null` | `null` | The default active filter option value in uncontrolled manner. (works when not using multiple filters) |
| defaultFilterOptionValues | `Array<string \| number>` | `[]` | The default active filter option values in uncontrolled manner. (works when there are multiple filters) |
| defaultSortOrder | `'descend' \| 'ascend' \| false` | `false` | The default sort order of the table in uncontrolled manner |
| disabled | `(rowData: Object, index: number) => boolean` | `() => false` | |
| disabled | `(rowData: Object, rowIndex: number) => boolean` | `() => false` | |
| ellipsis | `boolean \| EllipsisProps` | `false` | |
| expandable | `(rowData: Object, rowIndex: number) => boolean` | `undefined` | Whethe the row is expandable. Only works when `type` is `'expand'`. |
| filter | `boolean \| (optionValue: string \| number, rowData: Object) => boolean \| 'default'` | `false` | The filter of the column. If set to `true`, it will only display filter button on the column, which can be used in async status. |
| filterMode | `'and' \| 'or'` | `'or'` | |
| filterMultiple | `boolean` | `true` | |
@ -93,13 +95,14 @@ These methods can help you control table in an uncontrolled manner. However, it'
| filterOptionValues | `Array<string \| number> \| null` | `undefined` | The active filter option values in controlled manner. If not set, the filter of the column works in an uncontrolled manner. (works when there are multiple filters) |
| filterOptions | `Array<{ label: string, value: string \| number}>` | `undefined` | |
| fixed | `'left \| 'right' \| false` | `false` | |
| key | `string \| number` | **required** | Unique key of this column, **required** when table's row-key is not set. |
| render | `(rowData: Object) => VNodeChild` | `undefined` | Render function of column row cell. |
| key | `string \| number` | `undefined` | Unique key of this column, **required** when table's row-key is not set. |
| render | `(rowData: Object, rowIndex: number) => VNodeChild` | `undefined` | Render function of column row cell. |
| renderExpand | `(rowData: Object, rowIndex: number) => VNodeChild` | `undefined` | Render function of the expand area. Only works when `type` is `'expand'`. |
| renderFilterMenu | `() => VNodeChild` | `undefined` | Render function of column filter menu. |
| rowSpan | `(rowData: Object, rowIndex: number) => number` | `undefined` | |
| sortOrder | `'descend' \| 'ascend' \| false` | `undefined` | The controlled sort order of the column. If multiple columns' sortOrder is set, the first one will affect. |
| sorter | `boolean \| function \| 'default'` | `false` | The sorter of the column. If set `'default'`, it will use a basic builtin compare function. If set to `true`, it will only display sort icon on the column, which can be used in async status. Otherwise it works like `Array.sort`'s compare function. |
| title | `string \| (() => VNodeChild)` | `undefined` | Can be a render function |
| titleRowSpan | `number` | `undefined` | |
| type | `'default' \| 'selection'` | `default` | |
| type | `'selection' \| 'expand'` | `undefined` | |
| width | `number \| string` | `undefined` | Width of the column, **required** when fixed |

View File

@ -0,0 +1,110 @@
# 可展开
```html
<n-data-table :columns="columns" :data="data" :pagination="pagination" />
```
```js
import { h, defineComponent } from 'vue'
import { NTag, NButton, useMessage } from 'naive-ui'
const createColumns = ({ sendMail }) => {
return [
{
type: 'expand',
expandable: (_, index) => index !== 1,
renderExpand: (rowData) => {
return `${rowData.name} is a good guy.`
}
},
{
title: 'Name',
key: 'name'
},
{
title: 'Age',
key: 'age'
},
{
title: 'Address',
key: 'address'
},
{
title: 'Tags',
key: 'tags',
render (row) {
const tags = row.tags.map((tagKey) => {
return h(
NTag,
{
style: {
marginRight: '6px'
},
type: 'info'
},
{
default: () => tagKey
}
)
})
return tags
}
},
{
title: 'Action',
key: 'actions',
render (row) {
return h(
NButton,
{
size: 'small',
onClick: () => sendMail(row)
},
{ default: () => 'Send Email' }
)
}
}
]
}
const createData = () => [
{
key: 0,
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park',
tags: ['nice', 'developer']
},
{
key: 1,
name: 'Jim Green',
age: 42,
address: 'London No. 1 Lake Park',
tags: ['loser']
},
{
key: 2,
name: 'Joe Black',
age: 32,
address: 'Sidney No. 1 Lake Park',
tags: ['cool', 'teacher']
}
]
export default defineComponent({
setup () {
const message = useMessage()
return {
data: createData(),
columns: createColumns({
sendMail (rowData) {
message.info('send mail to ' + rowData.name)
}
}),
pagination: {
pageSize: 10
}
}
}
})
```

View File

@ -26,6 +26,7 @@ fixed-header
fixed-header-column
ellipsis
ellipsis-tooltip
expand
render-header
custom-style
ajax-usage
@ -84,8 +85,9 @@ custom-filter-menu
| defaultFilterOptionValue | `string \| number \| null` | `null` | 非受控状态下默认的过滤器选项值(过滤器单选时生效) |
| defaultFilterOptionValues | `Array<string \| number>` | `[]` | 非受控状态下默认的过滤器选项值(过滤器多选时生效) |
| defaultSortOrder | `'descend' \| 'ascend' \| false` | `false` | 非受控状态下表格默认的排序方式 |
| disabled | `(rowData: Object, index: number) => boolean` | `undefined` | |
| disabled | `(rowData: Object, rowIndex: number) => boolean` | `undefined` | |
| ellipsis | `boolean \| EllipsisProps` | `false` | |
| expandable | `(rowData: Object, rowIndex: number) => boolean` | `undefined` | 行是否可展开,仅在 `type``'expand'` 时生效 |
| filter | `boolean \| (optionValue: string \| number, rowData: Object) => boolean \| 'default'` | `undefined` | 这一列的过滤方法。如果设为 `true`,表格将只会在这列展示一个排序图标,在异步的时候可能有用。 |
| filterMode | `'and' \| 'or'` | `'or'` | |
| filterMultiple | `boolean` | `true` | |
@ -93,13 +95,14 @@ custom-filter-menu
| filterOptionValues | `Array<string \| number> \| null` | `undefined` | 受控状态下,当前激活的过滤器选项值数组。如果不做设定,这一列的过滤行为将是非受控的(过滤器多选时生效) |
| filterOptions | `Array<{ label: string, value: string \| number}>` | `undefined` | |
| fixed | `'left \| 'right' \| false` | `false` | |
| key | `string \| number` | **必须** | 这一列的 key在表格未设定 row-key 的时候是**必须**的。 |
| render | `(rowData: Object) => VNodeChild` | `undefined` | 渲染函数,渲染这一列的每一行的单元格 |
| key | `string \| number` | `undefined` | 这一列的 key在表格未设定 row-key 的时候是**必须**的。 |
| render | `(rowData: Object, rowIndex: number) => VNodeChild` | `undefined` | 渲染函数,渲染这一列的每一行的单元格 |
| renderExpand | `(rowData: Object, rowIndex: number) => VNodeChild` | `undefined` | 展开区域的渲染函数,仅在 `type``'expand'` 的时候生效 |
| renderFilterMenu | `() => VNodeChild` | `undefined` | 渲染函数,渲染这一列的过滤器菜单 |
| rowSpan | `(rowData: Object, rowIndex: number) => number` | `undefined` | |
| sortOrder | `'descend' \| 'ascend' \| false` | `undefined` | 受控状态下表格的排序方式。如果多列都设定了有效值,那么只有第一个会生效 |
| sorter | `boolean \| function \| 'default'` | `undefined` | 这一列的排序方法。如果设为 `'default'` 表格将会使用一个内置的排序函数;如果设为 `true`,表格将只会在这列展示一个排序图标,在异步的时候可能有用。其他情况下它工作的方式类似 `Array.sort` 的对比函数 |
| title | `string \| (() => VNodeChild)` | `undefined` | 可以是渲染函数 |
| titleRowSpan | `number` | `undefined` | |
| type | `'selection'` | `undefined` | |
| type | `'selection' \| 'expand'` | `undefined` | |
| width | `number \| string` | `undefined` | 列的宽度,在列固定时是**必需**的 |

View File

@ -33,10 +33,12 @@ import {
OnUpdateFilters,
MainTableRef,
DataTableInjection,
DataTableRef
DataTableRef,
OnUpdateExpandedRowKeys
} from './interface'
import style from './styles/index.cssr'
import { useGroupHeader } from './use-group-header'
import { useExpand } from './use-expand'
export const dataTableProps = {
...(useTheme.props as ThemeProps<DataTableTheme>),
@ -90,6 +92,11 @@ export const dataTableProps = {
default: 'medium'
},
remote: Boolean,
defaultExpandedRowKeys: {
type: Array as PropType<RowKey[]>,
default: []
},
expandedRowKeys: Array as PropType<RowKey[]>,
// eslint-disable-next-line vue/prop-name-casing
'onUpdate:page': [Function, Array] as PropType<
PaginationProps['onUpdate:page']
@ -108,6 +115,12 @@ export const dataTableProps = {
'onUpdate:checkedRowKeys': [Function, Array] as PropType<
MaybeArray<OnUpdateCheckedRowKeys>
>,
'onUpdate:expandedRowKeys': [Function, Array] as PropType<
MaybeArray<OnUpdateExpandedRowKeys>
>,
onUpdateExpandedRowKeys: [Function, Array] as PropType<
MaybeArray<OnUpdateExpandedRowKeys>
>,
// deprecated
onPageChange: {
type: [Function, Array] as PropType<PaginationProps['onUpdate:page']>,
@ -233,6 +246,11 @@ export default defineComponent({
paginatedDataRef,
treeMateRef
})
const {
mergedExpandedRowKeys,
renderExpand,
doUpdateExpandedRowKeys
} = useExpand(props)
const {
handleTableBodyScroll,
handleTableHeaderScroll,
@ -272,6 +290,7 @@ export default defineComponent({
loading: toRef(props, 'loading'),
rowClassName: toRef(props, 'rowClassName'),
mergedCheckedRowKeys,
mergedExpandedRowKeys,
locale,
rowKey: toRef(props, 'rowKey'),
filterMenuCssVars: computed(() => {
@ -285,6 +304,7 @@ export default defineComponent({
'--action-divider-color': actionDividerColor
} as CSSProperties
}),
renderExpand,
deriveActiveRightFixedColumn,
deriveActiveLeftFixedColumn,
doUpdateFilters,
@ -292,6 +312,7 @@ export default defineComponent({
doUpdateCheckedRowKeys,
doCheckAll,
doUncheckAll,
doUpdateExpandedRowKeys,
handleTableHeaderScroll,
handleTableBodyScroll
})

View File

@ -3,9 +3,10 @@ import { pxfy } from 'seemly'
import { NCheckbox } from '../../../checkbox'
import { NScrollbar, ScrollbarRef } from '../../../scrollbar'
import { formatLength } from '../../../_utils'
import { DataTableInjection, TmNode } from '../interface'
import { DataTableInjection, RowKey, TmNode } from '../interface'
import { createRowClassName } from '../utils'
import Cell from './Cell'
import ExpandTrigger from './ExpandTrigger'
export default defineComponent({
name: 'DataTableBody',
@ -38,12 +39,24 @@ export default defineComponent({
function handleScroll (event: Event): void {
NDataTable.handleTableBodyScroll(event)
}
function handleUpdateExpanded (key: RowKey): void {
const { mergedExpandedRowKeys, doUpdateExpandedRowKeys } = NDataTable
const index = mergedExpandedRowKeys.indexOf(key)
const nextExpandedKeys = Array.from(mergedExpandedRowKeys)
if (~index) {
nextExpandedKeys.splice(index, 1)
} else {
nextExpandedKeys.push(key)
}
doUpdateExpandedRowKeys(nextExpandedKeys)
}
return {
NDataTable,
scrollbarInstRef,
getScrollContainer,
handleScroll,
handleCheckboxUpdateChecked
handleCheckboxUpdateChecked,
handleUpdateExpanded
}
},
render () {
@ -65,10 +78,167 @@ export default defineComponent({
>
{{
default: () => {
let hasExpandedRows = false
const cordToPass: Record<number, number[]> = {}
const { cols, paginatedData } = NDataTable
const {
cols,
paginatedData,
mergedTheme,
fixedColumnLeftMap,
fixedColumnRightMap,
currentPage,
mergedCheckedRowKeys,
rowClassName,
leftActiveFixedColKey,
rightActiveFixedColKey,
renderExpand,
mergedExpandedRowKeys
} = NDataTable
const { length: colCount } = cols
const { length: rowCount } = paginatedData
const { handleCheckboxUpdateChecked, handleUpdateExpanded } = this
const rows = paginatedData.map((tmNode, rowIndex) => {
const { rawNode: rowData, key: rowKey } = tmNode
const expanded =
renderExpand && mergedExpandedRowKeys.includes(rowKey)
const row = (
<tr
key={rowKey}
class={[
'n-data-table-tr',
createRowClassName(rowData, rowIndex, rowClassName)
]}
>
{cols.map((col, colIndex) => {
if (rowIndex in cordToPass) {
const cordOfRowToPass = cordToPass[rowIndex]
const indexInCordOfRowToPass = cordOfRowToPass.indexOf(
colIndex
)
if (~indexInCordOfRowToPass) {
cordOfRowToPass.splice(indexInCordOfRowToPass, 1)
return null
}
}
const { key: colKey, column } = col
const { rowSpan, colSpan } = column
const mergedColSpan = colSpan
? colSpan(rowData, rowIndex)
: 1
const mergedRowSpan = rowSpan
? rowSpan(rowData, rowIndex)
: 1
const isLastCol = colIndex + mergedColSpan === colCount
const isLastRow = rowIndex + mergedRowSpan === rowCount
if (mergedColSpan > 1 || mergedRowSpan > 1) {
for (
let i = rowIndex;
i < rowIndex + mergedRowSpan;
++i
) {
for (
let j = colIndex;
j < colIndex + mergedColSpan;
++j
) {
if (i === rowIndex && j === colIndex) continue
if (!(i in cordToPass)) {
cordToPass[i] = [j]
} else {
cordToPass[i].push(j)
}
}
}
}
return (
<td
key={colKey}
style={{
textAlign: column.align || undefined,
left: pxfy(fixedColumnLeftMap[colKey]),
right: pxfy(fixedColumnRightMap[colKey])
}}
colspan={mergedColSpan}
rowspan={mergedRowSpan}
class={[
'n-data-table-td',
column.className,
column.fixed &&
`n-data-table-td--fixed-${column.fixed}`,
column.align &&
`n-data-table-td--${column.align}-align`,
{
'n-data-table-td--ellipsis':
column.ellipsis === true ||
// don't add ellpisis class if tooltip exists
(column.ellipsis && !column.ellipsis.tooltip),
'n-data-table-td--shadow-after':
leftActiveFixedColKey === colKey,
'n-data-table-td--shadow-before':
rightActiveFixedColKey === colKey,
'n-data-table-td--selection':
column.type === 'selection',
'n-data-table-td--expand': column.type === 'expand',
'n-data-table-td--last-col': isLastCol,
'n-data-table-td--last-row': isLastRow && !expanded
}
]}
>
{column.type === 'selection' ? (
<NCheckbox
key={currentPage}
disabled={column.disabled?.(rowData)}
checked={mergedCheckedRowKeys.includes(rowKey)}
onUpdateChecked={(checked) =>
handleCheckboxUpdateChecked(tmNode, checked)
}
/>
) : column.type === 'expand' ? (
!column.expandable ||
column.expandable?.(rowData, rowIndex) ? (
<ExpandTrigger
expanded={expanded}
onClick={() => handleUpdateExpanded(rowKey)}
/>
) : null
) : (
<Cell
index={rowIndex}
row={rowData}
column={column}
mergedTheme={mergedTheme}
/>
)}
</td>
)
})}
</tr>
)
if (expanded && renderExpand) {
if (!hasExpandedRows) {
hasExpandedRows = true
}
return [
row,
<tr class="n-data-table-tr" key={`${rowKey}__expand`}>
<td
class={[
'n-data-table-td',
'n-data-table-td--last-col',
{
'n-data-table-td--last-row': rowIndex + 1 === rowCount
}
]}
colspan={colCount}
>
{renderExpand(rowData, rowIndex)}
</td>
</tr>
]
}
return row
})
return (
<table ref="body" class="n-data-table-table">
<colgroup>
@ -77,130 +247,7 @@ export default defineComponent({
))}
</colgroup>
<tbody ref="tbody" class="n-data-table-tbody">
{paginatedData.map((tmNode, rowIndex) => {
const { rawNode: row } = tmNode
const { handleCheckboxUpdateChecked } = this
const {
mergedTheme,
cols,
fixedColumnLeftMap,
fixedColumnRightMap,
currentPage,
mergedCheckedRowKeys,
rowClassName,
leftActiveFixedColKey,
rightActiveFixedColKey
} = NDataTable
return (
<tr
key={tmNode.key}
class={[
'n-data-table-tr',
createRowClassName(row, rowIndex, rowClassName)
]}
>
{cols.map((col, colIndex) => {
if (rowIndex in cordToPass) {
const cordOfRowToPass = cordToPass[rowIndex]
const indexInCordOfRowToPass = cordOfRowToPass.indexOf(
colIndex
)
if (~indexInCordOfRowToPass) {
cordOfRowToPass.splice(indexInCordOfRowToPass, 1)
return null
}
}
const { key, column } = col
const { rowSpan, colSpan } = column
const mergedColSpan = colSpan
? colSpan(row, rowIndex)
: 1
const mergedRowSpan = rowSpan
? rowSpan(row, rowIndex)
: 1
const isLastCol =
colIndex + mergedColSpan === colCount
const isLastRow =
rowIndex + mergedRowSpan === rowCount
if (mergedColSpan > 1 || mergedRowSpan > 1) {
for (
let i = rowIndex;
i < rowIndex + mergedRowSpan;
++i
) {
for (
let j = colIndex;
j < colIndex + mergedColSpan;
++j
) {
if (i === rowIndex && j === colIndex) continue
if (!(i in cordToPass)) {
cordToPass[i] = [j]
} else {
cordToPass[i].push(j)
}
}
}
}
return (
<td
key={key}
style={{
textAlign: column.align || undefined,
left: pxfy(fixedColumnLeftMap[key]),
right: pxfy(fixedColumnRightMap[key])
}}
colspan={mergedColSpan}
rowspan={mergedRowSpan}
class={[
'n-data-table-td',
column.className,
column.fixed &&
`n-data-table-td--fixed-${column.fixed}`,
column.align &&
`n-data-table-td--${column.align}-align`,
{
'n-data-table-td--ellipsis':
column.ellipsis === true ||
// don't add ellpisis class if tooltip exists
(column.ellipsis &&
!column.ellipsis.tooltip),
'n-data-table-td--shadow-after':
leftActiveFixedColKey === key,
'n-data-table-td--shadow-before':
rightActiveFixedColKey === key,
'n-data-table-td--selection':
column.type === 'selection',
'n-data-table-td--last-col': isLastCol,
'n-data-table-td--last-row': isLastRow
}
]}
>
{column.type === 'selection' ? (
<NCheckbox
key={currentPage}
disabled={column.disabled?.(row)}
checked={mergedCheckedRowKeys.includes(
tmNode.key
)}
onUpdateChecked={(checked) =>
handleCheckboxUpdateChecked(tmNode, checked)
}
/>
) : (
<Cell
index={rowIndex}
row={row}
column={column}
mergedTheme={mergedTheme}
/>
)}
</td>
)
})}
</tr>
)
})}
{hasExpandedRows ? rows.flat() : rows}
</tbody>
</table>
)

View File

@ -0,0 +1,33 @@
import { h, defineComponent, PropType, CSSProperties } from 'vue'
import { ChevronRightIcon } from '../../../_internal/icons'
import { NBaseIcon } from '../../../_internal'
export default defineComponent({
name: 'DataTableExpandTrigger',
props: {
expanded: Boolean,
onClick: {
type: Function as PropType<() => void>,
required: true
}
},
render () {
const style: CSSProperties = {
cursor: 'pointer',
fontSize: '16px'
}
return (
<NBaseIcon onClick={this.onClick} style={style}>
{{
default: () => {
return (
<ChevronRightIcon
style={this.expanded ? 'transform: rotate(90deg);' : undefined}
/>
)
}
}}
</NBaseIcon>
)
}
})

View File

@ -13,14 +13,17 @@ import {
} from '../utils'
import {
DataTableInjection,
ExpandColInfo,
SelectionColInfo,
TableColumnGroup,
TableColumnInfo
} from '../interface'
function renderTitle (column: TableColumnInfo | TableColumnGroup): VNodeChild {
function renderTitle (
column: ExpandColInfo | TableColumnInfo | TableColumnGroup
): VNodeChild {
return typeof column.title === 'function'
? column.title(column)
? column.title(column as any)
: column.title
}
@ -131,10 +134,15 @@ export default defineComponent({
},
column.className
]}
onClick={(e) => {
onClick={
column.type !== 'selection' &&
handleColHeaderClick(e, column)
}}
column.type !== 'expand' &&
!('children' in column)
? (e) => {
handleColHeaderClick(e, column)
}
: undefined
}
>
{column.type === 'selection' ? (
<NCheckbox
@ -162,10 +170,8 @@ export default defineComponent({
default: () => renderTitle(column)
}}
</NEllipsis>
) : typeof column.title === 'function' ? (
column.title(column)
) : (
column.title
renderTitle(column)
)}
{isColumnSortable(column) ? (
<SortButton column={column as TableColumnInfo} />

View File

@ -50,8 +50,14 @@ export type TableColumnTitle =
| string
| ((column: TableColumnInfo) => VNodeChild)
export type ExpandColTitle = string | ((column: ExpandColInfo) => VNodeChild)
export type TableColumnGroupTitle =
| string
| ((column: TableColumnGroup) => VNodeChild)
export type TableColumnGroup = {
title?: TableColumnTitle
title?: TableColumnGroupTitle
type?: never
key: ColumnKey
children: TableColumnInfo[]
@ -81,12 +87,12 @@ export type TableColumnInfo = {
defaultFilterOptionValue?: FilterOptionValue | null
filterMultiple?: boolean
render?: (data: TableNode, index: number) => VNodeChild
render?: (rowData: TableNode, rowIndex: number) => VNodeChild
renderFilterMenu?: FilterMenuRender
renderSorter?: SorterRender
renderFilter?: FilterRender
colSpan?: (data: TableNode, index: number) => number
rowSpan?: (data: TableNode, index: number) => number
colSpan?: (rowData: TableNode, rowIndex: number) => number
rowSpan?: (rowData: TableNode, rowIndex: number) => number
} & CommonColInfo
export type SelectionColInfo = {
@ -103,7 +109,20 @@ export type SelectionColInfo = {
rowSpan?: never
} & CommonColInfo
export type TableColumn = TableColumnGroup | TableColumnInfo | SelectionColInfo
export type RenderExpand = (row: TableNode, index: number) => VNodeChild
export type Expandable = (row: TableNode, index: number) => boolean
export interface ExpandColInfo extends Omit<SelectionColInfo, 'type'> {
type: 'expand'
title?: ExpandColTitle
renderExpand: RenderExpand
expandable?: Expandable
}
export type TableColumn =
| TableColumnGroup
| TableColumnInfo
| SelectionColInfo
| ExpandColInfo
export type TableColumns = TableColumn[]
export interface DataTableInjection {
@ -129,6 +148,9 @@ export interface DataTableInjection {
mergedCheckedRowKeys: RowKey[]
locale: NLocale['DataTable']
filterMenuCssVars: CSSProperties
mergedExpandedRowKeys: RowKey[]
renderExpand: undefined | RenderExpand
doUpdateExpandedRowKeys: (keys: RowKey[]) => void
rowKey: CreateRowKey | undefined
doUpdateFilters: (
filters: FilterState,
@ -161,6 +183,7 @@ export type SorterRender = (props: { order: SortOrder | false }) => VNodeChild
export type FilterMenuRender = () => VNodeChild
export type OnUpdateExpandedRowKeys = (keys: RowKey[]) => void
export type OnUpdateCheckedRowKeys = (keys: RowKey[]) => void
export type OnUpdateSorter = (sortState: SortState | null) => void
export type OnUpdateFilters = (

View File

@ -246,7 +246,7 @@ export default c([
overflow: hidden;
white-space: nowrap;
`),
cM('selection', `
cM('selection, expand', `
text-align: center;
padding: 0;
line-height: 0;

View File

@ -0,0 +1,46 @@
import { toRef, ref } from 'vue'
import { useMemo, useMergedState } from 'vooks'
import type { DataTableProps } from './DataTable'
import { RowKey } from './interface'
import { call, warn } from '../../_utils'
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function useExpand (props: DataTableProps) {
const renderExpandRef = useMemo(() => {
for (const col of props.columns) {
if (col.type === 'expand') {
if (__DEV__ && !col.renderExpand) {
warn(
'data-table',
'column with type `expand` has no `renderExpand` prop.'
)
}
return col.renderExpand
}
}
})
const uncontrolledExpandedRowKeysRef = ref(props.defaultExpandedRowKeys)
const controlledExpandedRowKeysRef = toRef(props, 'expandedRowKeys')
const mergedExpandedRowKeys = useMergedState(
controlledExpandedRowKeysRef,
uncontrolledExpandedRowKeysRef
)
function doUpdateExpandedRowKeys (expandedKeys: RowKey[]): void {
const {
onUpdateExpandedRowKeys,
'onUpdate:expandedRowKeys': _onUpdateExpandedRowKeys
} = props
if (onUpdateExpandedRowKeys) {
call(onUpdateExpandedRowKeys, expandedKeys)
}
if (_onUpdateExpandedRowKeys) {
call(_onUpdateExpandedRowKeys, expandedKeys)
}
uncontrolledExpandedRowKeysRef.value = expandedKeys
}
return {
mergedExpandedRowKeys: mergedExpandedRowKeys,
renderExpand: renderExpandRef,
doUpdateExpandedRowKeys
}
}

View File

@ -1,6 +1,7 @@
import { CSSProperties, ComputedRef, computed } from 'vue'
import { DataTableProps } from './DataTable'
import type {
ExpandColInfo,
SelectionColInfo,
TableColumn,
TableColumnInfo,
@ -17,7 +18,7 @@ export interface RowItem {
export interface ColItem {
key: string | number
style: CSSProperties
column: SelectionColInfo | TableColumnInfo
column: SelectionColInfo | ExpandColInfo | TableColumnInfo
}
type RowItemMap = WeakMap<TableColumn, RowItem>
@ -26,11 +27,13 @@ function getRowsAndCols (
): {
rows: RowItem[][]
cols: ColItem[]
dataRelatedCols: Array<SelectionColInfo | TableColumnInfo>
dataRelatedCols: Array<SelectionColInfo | TableColumnInfo | ExpandColInfo>
} {
const rows: RowItem[][] = []
const cols: ColItem[] = []
const dataRelatedCols: Array<SelectionColInfo | TableColumnInfo> = []
const dataRelatedCols: Array<
SelectionColInfo | TableColumnInfo | ExpandColInfo
> = []
const rowItemMap: RowItemMap = new WeakMap()
let maxDepth = -1
let totalRowSpan = 0
@ -116,7 +119,9 @@ export function useGroupHeader (
): {
rows: ComputedRef<RowItem[][]>
cols: ComputedRef<ColItem[]>
dataRelatedCols: ComputedRef<Array<SelectionColInfo | TableColumnInfo>>
dataRelatedCols: ComputedRef<
Array<SelectionColInfo | TableColumnInfo | ExpandColInfo>
>
} {
const rowsAndCols = computed(() => getRowsAndCols(props.columns))
return {

View File

@ -12,7 +12,8 @@ import type {
TableColumnInfo,
SelectionColInfo,
TableNode,
TmNode
TmNode,
ExpandColInfo
} from './interface'
import { createShallowClonedObject, getFlagOfOrder } from './utils'
import { PaginationProps } from '../../pagination/src/Pagination'
@ -26,7 +27,9 @@ export function useTableData (
{
dataRelatedCols
}: {
dataRelatedCols: ComputedRef<Array<SelectionColInfo | TableColumnInfo>>
dataRelatedCols: ComputedRef<
Array<SelectionColInfo | TableColumnInfo | ExpandColInfo>
>
}
) {
const treeMateRef = computed(() =>
@ -138,7 +141,7 @@ export function useTableData (
)
const controlledFilterState: FilterState = {}
columnsWithControlledFilter.forEach((column) => {
if (column.type === 'selection') return
if (column.type === 'selection' || column.type === 'expand') return
controlledFilterState[column.key] =
column.filterOptionValues || column.filterOptionValue || null
})
@ -161,7 +164,13 @@ export function useTableData (
} = treeMateRef
const columnEntries: Array<[ColumnKey, TableColumnInfo]> = []
columns.forEach((column) => {
if (column.type === 'selection') return
if (
column.type === 'selection' ||
column.type === 'expand' ||
'children' in column
) {
return
}
columnEntries.push([column.key, column])
})
return data
@ -326,7 +335,10 @@ export function useTableData (
clearSorter()
} else {
const columnToSort = dataRelatedCols.value.find(
(column) => column.type !== 'selection' && column.key === columnKey
(column) =>
column.type !== 'selection' &&
column.type !== 'expand' &&
column.key === columnKey
)
if (!columnToSort || !columnToSort.sorter) return
const sorter = columnToSort.sorter

View File

@ -8,23 +8,23 @@ import type {
SortState,
CreateRowClassName,
SelectionColInfo,
TableColumnGroup,
TableColumn
TableColumn,
ExpandColInfo
} from './interface'
export const selectionColWidth = 40
export const expandColWidth = 40
export function getColWidth (
col: TableColumnInfo | SelectionColInfo
): number | undefined {
export function getColWidth (col: TableColumn): number | undefined {
if (col.type === 'selection') return selectionColWidth
if (col.type === 'expand') return expandColWidth
if ('children' in col) return undefined
return col.width
}
export function getColKey (
col: TableColumnInfo | SelectionColInfo | TableColumnGroup
): string | number {
export function getColKey (col: TableColumn): string | number {
if (col.type === 'selection') return '__n_selection__'
if (col.type === 'expand') return '__n_expand__'
return col.key
}
@ -43,7 +43,7 @@ export function getFlagOfOrder (order: SortOrder): SortOrderFlag {
}
export function createCustomWidthStyle (
column: TableColumnInfo | SelectionColInfo
column: TableColumnInfo | SelectionColInfo | ExpandColInfo
): CSSProperties {
return {
width: pxfy(getColWidth(column))