feat(components): [virtual-table] renderers (#7195)

- Move render function into rendering units
This commit is contained in:
JeremyWuuuuu 2022-04-17 15:34:52 +08:00 committed by GitHub
parent 8b7d29989a
commit da63b35c6b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 545 additions and 327 deletions

View File

@ -42,7 +42,6 @@ export const tableV2GridProps = buildProps({
* Special attributes
*/
cache: virtualizedListProps.cache,
rowKey: tableV2RowProps.rowKey,
useIsScrolling: Boolean,
/**

View File

@ -0,0 +1,127 @@
import { get } from 'lodash-unified'
import { isFunction, isObject } from '@element-plus/utils'
import TableCell from '../table-cell'
import ExpandIcon from '../expand-icon'
import { Alignment } from '../constants'
import { placeholderSign } from '../private'
import { enforceUnit, tryCall } from '../utils'
import type { FunctionalComponent, UnwrapNestedRefs, VNode } from 'vue'
import type { TableV2RowCellRenderParam } from '../table-row'
import type { UseNamespaceReturn } from '@element-plus/hooks'
import type { UseTableReturn } from '../use-table'
import type { TableV2Props } from '../table'
type CellRendererProps = TableV2RowCellRenderParam &
Pick<
TableV2Props,
'cellProps' | 'expandColumnKey' | 'indentSize' | 'iconSize' | 'rowKey'
> &
UnwrapNestedRefs<
Pick<UseTableReturn, 'columnsStyles' | 'expandedRowKeys'>
> & {
ns: UseNamespaceReturn
}
const CellRenderer: FunctionalComponent<CellRendererProps> = (
{
// renderer props
columns,
column,
columnIndex,
depth,
expandIconProps,
isScrolling,
rowData,
rowIndex,
// from use-table
columnsStyles,
expandedRowKeys,
ns,
// derived props
expandColumnKey,
indentSize,
iconSize,
rowKey,
},
{ slots }
) => {
const cellStyle = enforceUnit(columnsStyles[column.key])
if (column.placeholderSign === placeholderSign) {
return <div class={ns.em('row-cell', 'placeholder')} style={cellStyle} />
}
const { dataKey, dataGetter } = column
const CellComponent = slots.cell || ((props) => <TableCell {...props} />)
const cellData = isFunction(dataGetter)
? dataGetter({ columns, column, columnIndex, rowData, rowIndex })
: get(rowData, dataKey ?? '')
const cellProps = {
class: ns.e('cell-text'),
columns,
column,
columnIndex,
cellData,
isScrolling,
rowData,
rowIndex,
}
const Cell = CellComponent(cellProps)
const kls = [
ns.e('row-cell'),
column.align === Alignment.CENTER && ns.is('align-center'),
column.align === Alignment.RIGHT && ns.is('align-right'),
]
const expandable = rowIndex >= 0 && column.key === expandColumnKey
const expanded = rowIndex >= 0 && expandedRowKeys.includes(rowData[rowKey])
let IconOrPlaceholder: VNode | undefined
const iconStyle = `margin-inline-start: ${depth * indentSize}px;`
if (expandable) {
if (isObject(expandIconProps)) {
IconOrPlaceholder = (
<ExpandIcon
{...expandIconProps}
class={[ns.e('expand-icon'), ns.is('expanded', expanded)]}
size={iconSize}
expanded={expanded}
style={iconStyle}
expandable
/>
)
} else {
IconOrPlaceholder = (
<div
style={[
iconStyle,
`width: ${iconSize}px; height: ${iconSize}px;`,
].join(' ')}
/>
)
}
}
return (
<div
{...tryCall(cellProps, {
columns,
column,
columnIndex,
rowData,
rowIndex,
})}
class={kls}
style={cellStyle}
>
{IconOrPlaceholder}
{Cell}
</div>
)
}
export default CellRenderer

View File

@ -0,0 +1,112 @@
import HeaderCell from '../table-header-cell'
import ColumnResizer from '../table-column-resizer'
import SortIcon from '../sort-icon'
import { Alignment, SortOrder, oppositeOrderMap } from '../constants'
import { placeholderSign } from '../private'
import { tryCall } from '../utils'
import type { FunctionalComponent, UnwrapNestedRefs } from 'vue'
import type { UseNamespaceReturn } from '@element-plus/hooks'
import type { TableV2HeaderRowCellRendererParams } from '../table-header-row'
import type { UseTableReturn } from '../use-table'
import type { TableV2Props } from '../table'
import type { TableV2HeaderCell } from '../header-cell'
type HeaderCellRendererProps = TableV2HeaderRowCellRendererParams &
UnwrapNestedRefs<
Pick<
UseTableReturn,
| 'columnsStyles'
| 'resizingKey'
| 'onColumnSorted'
| 'onColumnResized'
| 'onColumnResizeEnd'
| 'onColumnResizeStart'
>
> &
Pick<TableV2Props, 'sortBy' | 'sortState' | 'headerCellProps'> & {
ns: UseNamespaceReturn
}
const HeaderCellRenderer: FunctionalComponent<HeaderCellRendererProps> = (
props
) => {
const {
column,
ns,
resizingKey,
columnsStyles,
onColumnResizeEnd,
onColumnResizeStart,
onColumnResized,
onColumnSorted,
} = props
if (column.placeholderSign === placeholderSign) {
return
}
const { headerCellRenderer, headerClass, sortable, resizable } = column
/**
* render Cell children
*/
const cellRenderer =
headerCellRenderer ||
((props: TableV2HeaderCell) => <HeaderCell {...props} />)
const Cell = cellRenderer({
...props,
class: ns.e('header-cell-text'),
})
/**
* Render cell container and sort indicator
*/
const { sortBy, sortState, headerCellProps } = props
const cellKls = [
ns.e('header-cell'),
...tryCall(headerClass, props, ''),
column.align === Alignment.CENTER && ns.is('align-center'),
column.align === Alignment.RIGHT && ns.is('align-right'),
sortable && ns.is('sortable'),
column.key === resizingKey && ns.is('resizing'),
]
let sorting: boolean, sortOrder: SortOrder
if (sortState) {
const order = sortState[column.key]
sorting = Boolean(oppositeOrderMap[order])
sortOrder = sorting ? order : SortOrder.ASC
} else {
sorting = column.key === sortBy.key
sortOrder = sorting ? sortBy.order : SortOrder.ASC
}
const cellProps = {
...tryCall(headerCellProps, props),
onClick: column.sortable ? onColumnSorted : undefined,
class: cellKls,
style: columnsStyles[column.key],
['data-key']: column.key,
}
return (
<div {...cellProps}>
{Cell}
{sortable && <SortIcon sortOrder={sortOrder} />}
{resizable && (
<ColumnResizer
class={ns.e('column-resizer')}
column={column}
onResize={onColumnResized}
onResizeStart={onColumnResizeStart}
onResizeStop={onColumnResizeEnd}
/>
)}
</div>
)
}
export default HeaderCellRenderer

View File

@ -0,0 +1,52 @@
import HeaderRow from '../table-header-row'
import { tryCall } from '../utils'
import type { FunctionalComponent, UnwrapNestedRefs } from 'vue'
import type { UseNamespaceReturn } from '@element-plus/hooks'
import type { TableV2HeaderRendererParams } from '../table-header'
import type { UseTableReturn } from '../use-table'
import type { TableV2Props } from '../table'
type HeaderRendererProps = TableV2HeaderRendererParams &
Pick<TableV2Props, 'headerClass' | 'headerProps'> & {
ns: UseNamespaceReturn
} & UnwrapNestedRefs<Pick<UseTableReturn, 'resizingKey'>>
const HeaderRenderer: FunctionalComponent<HeaderRendererProps> = (
{
columns,
headerIndex,
style,
// derived from root
headerClass,
headerProps,
ns,
// returned by use-table
resizingKey,
},
{ slots }
) => {
const param = { columns, headerIndex }
const kls = [
ns.e('header-row'),
tryCall(headerClass, param, ''),
{
[ns.is('resizing')]: Boolean(resizingKey),
[ns.is('customized')]: Boolean(slots.header),
},
]
const extraProps = {
...tryCall(headerProps, param),
class: kls,
columns,
headerIndex,
style,
}
return <HeaderRow {...extraProps}>{slots}</HeaderRow>
}
export default HeaderRenderer

View File

@ -0,0 +1,23 @@
import Table from '../table-grid'
import type { FunctionalComponent, Ref } from 'vue'
import type { TableV2GridProps } from '../grid'
import type { TableGridInstance } from '../table-grid'
export type MainTableRendererProps = TableV2GridProps & {
mainTableRef: Ref<TableGridInstance | undefined>
}
const MainTable: FunctionalComponent<MainTableRendererProps> = (
props: MainTableRendererProps,
{ slots }
) => {
const { mainTableRef, ...rest } = props
return (
<Table ref={mainTableRef} {...rest}>
{slots}
</Table>
)
}
export default MainTable

View File

@ -0,0 +1,105 @@
import Row from '../table-row'
import { tryCall } from '../utils'
import type { FunctionalComponent, UnwrapNestedRefs } from 'vue'
import type { UseNamespaceReturn } from '@element-plus/hooks'
import type { UseTableReturn } from '../use-table'
import type { TableV2Props } from '../table'
import type { TableGridRowSlotParams } from '../table-grid'
type RowRendererProps = TableGridRowSlotParams &
Pick<
TableV2Props,
| 'expandColumnKey'
| 'estimatedRowHeight'
| 'rowProps'
| 'rowClass'
| 'rowKey'
| 'rowEventHandlers'
> &
UnwrapNestedRefs<
Pick<
UseTableReturn,
| 'depthMap'
| 'expandedRowKeys'
| 'hasFixedColumns'
| 'hoveringRowKey'
| 'onRowHovered'
| 'onRowExpanded'
>
> & {
ns: UseNamespaceReturn
}
const RowRenderer: FunctionalComponent<RowRendererProps> = (
props,
{ slots }
) => {
const {
columns,
depthMap,
expandColumnKey,
expandedRowKeys,
estimatedRowHeight,
hasFixedColumns,
hoveringRowKey,
rowData,
rowIndex,
style,
isScrolling,
rowProps,
rowClass,
rowKey,
rowEventHandlers,
ns,
onRowHovered,
onRowExpanded,
} = props
const rowKls = tryCall(rowClass, { columns, rowData, rowIndex }, '')
const additionalProps = tryCall(rowProps, {
columns,
rowData,
rowIndex,
})
const _rowKey = rowData[rowKey]
const depth = depthMap[_rowKey] || 0
const canExpand = Boolean(expandColumnKey)
const isFixedRow = rowIndex < 0
const kls = [
ns.e('row'),
rowKls,
{
[ns.e(`row-depth-${depth}`)]: canExpand && rowIndex >= 0,
[ns.is('expanded')]: canExpand && expandedRowKeys.includes(_rowKey),
[ns.is('hovered')]: !isScrolling && _rowKey === hoveringRowKey,
[ns.is('fixed')]: !depth && isFixedRow,
[ns.is('customized')]: Boolean(slots.row),
},
]
const onRowHover = hasFixedColumns ? onRowHovered : undefined
const _rowProps = {
...additionalProps,
columns,
class: kls,
depth,
expandColumnKey,
estimatedRowHeight: isFixedRow ? undefined : estimatedRowHeight,
isScrolling,
rowIndex,
rowData,
rowKey: _rowKey,
rowEventHandlers,
style,
}
return (
<Row {..._rowProps} onRowHover={onRowHover} onRowExpand={onRowExpanded}>
{slots}
</Row>
)
}
export default RowRenderer

View File

@ -19,8 +19,8 @@ const TableV2HeaderRow = defineComponent({
})
})
if (slots.default) {
Cells = slots.default({
if (slots.header) {
Cells = slots.header({
cells: Cells,
columns,
headerIndex,

View File

@ -177,8 +177,8 @@ const TableV2Row = defineComponent({
})
})
if (slots.default) {
ColumnCells = slots.default({
if (slots.row) {
ColumnCells = slots.row({
cells: ColumnCells.map((node) => {
if (isArray(node) && node.length === 1) {
return node[0]

View File

@ -1,28 +1,21 @@
import { computed, defineComponent, provide, unref } from 'vue'
import { get } from 'lodash-unified'
import { useNamespace } from '@element-plus/hooks'
import { isFunction, isObject } from '@element-plus/utils'
import { useTable } from './use-table'
import { enforceUnit, tryCall } from './utils'
import { enforceUnit } from './utils'
import { TableV2InjectionKey } from './tokens'
import { Alignment, SortOrder, oppositeOrderMap } from './constants'
import { placeholderSign } from './private'
import { tableV2Props } from './table'
// components
import Table from './table-grid'
import TableRow from './table-row'
import TableHeaderRow from './table-header-row'
import TableCell from './table-cell'
import TableHeaderCell from './table-header-cell'
import ColumnResizer from './table-column-resizer'
import ExpandIcon from './expand-icon'
import SortIcon from './sort-icon'
// renderers
import MainTable from './renderers/main-table'
import Row from './renderers/row'
import Cell from './renderers/cell'
import Header from './renderers/header'
import HeaderCell from './renderers/header-cell'
import type { CSSProperties, VNode } from 'vue'
import type { CSSProperties } from 'vue'
import type { TableV2GridProps } from './grid'
import type { TableGridRowSlotParams } from './table-grid'
import type { TableV2RowCellRenderParam } from './table-row'
import type { TableV2HeaderRendererParams } from './table-header'
import type { TableV2HeaderCell } from './header-cell'
import type { TableV2HeaderRowCellRendererParams } from './table-header-row'
@ -80,39 +73,6 @@ const TableV2 = defineComponent({
() => unref(bodyWidth) + (props.fixed ? unref(vScrollbarSize) : 0)
)
function renderMainTable() {
const {
cache,
fixedData,
estimatedRowHeight,
headerHeight,
rowHeight,
width,
} = props
return (
<Table
ref={mainTableRef}
cache={cache}
class={ns.e('main')}
columns={unref(mainColumns)}
data={unref(data)}
fixedData={fixedData}
estimatedRowHeight={estimatedRowHeight}
bodyWidth={unref(bodyWidth)}
headerHeight={headerHeight}
headerWidth={unref(headerWidth)}
rowHeight={rowHeight}
height={unref(mainTableHeight)}
width={width}
onRowsRendered={onRowsRendered}
onScroll={onScroll}
>
{{ row: renderTableRow, header: renderHeader }}
</Table>
)
}
// function renderLeftTable() {
// const columns = unref(fixedColumnsOnLeft)
// if (columns.length === 0) return
@ -124,279 +84,8 @@ const TableV2 = defineComponent({
// function renderRightTable() {}
function renderHeader({
columns,
headerIndex,
style,
}: TableV2HeaderRendererParams) {
const param = { columns, headerIndex }
const headerClass = [
ns.e('header-row'),
tryCall(props.headerClass, param, ''),
{
[ns.is('resizing')]: unref(resizingKey),
[ns.is('customized')]: Boolean(slots.header),
},
]
const headerProps = {
...tryCall(props.headerProps, param),
class: headerClass,
columns,
headerIndex,
style,
}
return (
<TableHeaderRow {...headerProps}>
{{
default: slots.header,
cell: renderHeaderCell,
}}
</TableHeaderRow>
)
}
// function renderFooter() {}
function renderTableRow({
columns,
rowData,
rowIndex,
style,
isScrolling,
}: TableGridRowSlotParams) {
const {
expandColumnKey,
estimatedRowHeight,
rowProps,
rowClass,
rowKey,
rowEventHandlers,
} = props
const rowKls = tryCall(rowClass, { columns, rowData, rowIndex }, '')
const additionalProps = tryCall(rowProps, {
columns,
rowData,
rowIndex,
})
const _rowKey = rowData[rowKey]
const depth = unref(depthMap)[_rowKey] || 0
const canExpand = Boolean(expandColumnKey)
const isFixedRow = rowIndex < 0
const kls = [
ns.e('row'),
rowKls,
{
[ns.e(`row-depth-${depth}`)]: canExpand && rowIndex >= 0,
[ns.is('expanded')]:
canExpand && unref(expandedRowKeys).includes(_rowKey),
[ns.is('hovered')]: !isScrolling && _rowKey === unref(hoveringRowKey),
[ns.is('fixed')]: !depth && isFixedRow,
[ns.is('customized')]: Boolean(slots.row),
},
]
const onRowHover = unref(hasFixedColumns) ? onRowHovered : undefined
const _rowProps = {
...additionalProps,
columns,
class: kls,
depth,
expandColumnKey,
estimatedRowHeight: isFixedRow ? undefined : estimatedRowHeight,
isScrolling,
rowIndex,
rowData,
rowKey: _rowKey,
rowEventHandlers,
style,
}
const children = {
...(slots.row ? { default: slots.row } : {}),
cell: renderRowCell,
}
return (
<TableRow
{..._rowProps}
onRowHover={onRowHover}
onRowExpand={onRowExpanded}
>
{children}
</TableRow>
)
}
function renderRowCell({
columns,
column,
columnIndex,
depth,
expandIconProps,
isScrolling,
rowData,
rowIndex,
}: TableV2RowCellRenderParam) {
const cellStyle = enforceUnit(unref(columnsStyles)[column.key])
if (column.placeholderSign === placeholderSign) {
return (
<div class={ns.em('row-cell', 'placeholder')} style={cellStyle} />
)
}
const { dataKey, dataGetter } = column
const CellComponent = slots.cell || ((props) => <TableCell {...props} />)
const cellData = isFunction(dataGetter)
? dataGetter({ columns, column, columnIndex, rowData, rowIndex })
: get(rowData, dataKey ?? '')
const cellProps = {
class: ns.e('cell-text'),
columns,
column,
columnIndex,
cellData,
isScrolling,
rowData,
rowIndex,
}
const Cell = CellComponent(cellProps)
const kls = [
ns.e('row-cell'),
column.align === Alignment.CENTER && ns.is('align-center'),
column.align === Alignment.RIGHT && ns.is('align-right'),
]
const { expandColumnKey, indentSize, iconSize, rowKey } = props
const expandable = rowIndex >= 0 && column.key === expandColumnKey
const expanded =
rowIndex >= 0 && unref(expandedRowKeys).includes(rowData[rowKey])
let IconOrPlaceholder: VNode | undefined
const iconStyle = `margin-inline-start: ${depth * indentSize}px;`
if (expandable) {
if (isObject(expandIconProps)) {
IconOrPlaceholder = (
<ExpandIcon
{...expandIconProps}
class={[ns.e('expand-icon'), ns.is('expanded', expanded)]}
size={iconSize}
expanded={expanded}
style={iconStyle}
expandable
/>
)
} else {
IconOrPlaceholder = (
<div
style={[
iconStyle,
`width: ${iconSize}px; height: ${iconSize}px;`,
].join(' ')}
/>
)
}
}
return (
<div
{...tryCall(props.cellProps, {
columns,
column,
columnIndex,
rowData,
rowIndex,
})}
class={kls}
style={cellStyle}
>
{IconOrPlaceholder}
{Cell}
</div>
)
}
function renderHeaderCell(
renderHeaderCellProps: TableV2HeaderRowCellRendererParams
) {
const { column } = renderHeaderCellProps
if (column.placeholderSign === placeholderSign) {
return
}
const { headerCellRenderer, headerClass, sortable, resizable } = column
/**
* render Cell children
*/
const cellRenderer =
headerCellRenderer ||
((props: TableV2HeaderCell) => <TableHeaderCell {...props} />)
const Cell = cellRenderer({
...renderHeaderCellProps,
class: ns.e('header-cell-text'),
})
/**
* Render cell container and sort indicator
*/
const { sortBy, sortState, headerCellProps } = props
const cellKls = [
ns.e('header-cell'),
...tryCall(headerClass, renderHeaderCellProps, ''),
column.align === Alignment.CENTER && ns.is('align-center'),
column.align === Alignment.RIGHT && ns.is('align-right'),
sortable && ns.is('sortable'),
column.key === unref(resizingKey) && ns.is('resizing'),
]
let sorting: boolean, sortOrder: SortOrder
if (sortState) {
const order = sortState[column.key]
sorting = Boolean(oppositeOrderMap[order])
sortOrder = sorting ? order : SortOrder.ASC
} else {
sorting = column.key === sortBy.key
sortOrder = sorting ? sortBy.order : SortOrder.ASC
}
const cellProps = {
...tryCall(headerCellProps, renderHeaderCellProps),
onClick: column.sortable ? onColumnSorted : undefined,
class: cellKls,
style: unref(columnsStyles)[column.key],
['data-key']: column.key,
}
return (
<div {...cellProps}>
{Cell}
{sortable && <SortIcon sortOrder={sortOrder} />}
{resizable && (
<ColumnResizer
class={ns.e('column-resizer')}
column={column}
onResize={onColumnResized}
onResizeStart={onColumnResizeStart}
onResizeStop={onColumnResizeEnd}
/>
)}
</div>
)
}
provide(TableV2InjectionKey, {
ns,
isResetting,
@ -405,9 +94,118 @@ const TableV2 = defineComponent({
})
return () => {
const {
cache,
estimatedRowHeight,
expandColumnKey,
fixedData,
headerHeight,
headerClass,
headerProps,
headerCellProps,
sortBy,
sortState,
rowHeight,
rowClass,
rowEventHandlers,
rowKey,
rowProps,
indentSize,
iconSize,
useIsScrolling,
width,
} = props
const mainTableProps: TableV2GridProps = {
cache,
class: ns.e('main'),
columns: unref(mainColumns),
data: unref(data),
fixedData,
estimatedRowHeight,
bodyWidth: unref(bodyWidth),
headerHeight,
headerWidth: unref(headerWidth),
rowHeight,
height: unref(mainTableHeight),
useIsScrolling,
width,
onRowsRendered,
onScroll,
}
const tableRowProps = {
ns,
depthMap: unref(depthMap),
expandedRowKeys: unref(expandedRowKeys),
estimatedRowHeight,
hasFixedColumns: unref(hasFixedColumns),
hoveringRowKey: unref(hoveringRowKey),
rowProps,
rowClass,
rowKey,
rowEventHandlers,
onRowHovered,
onRowExpanded,
}
const tableCellProps = {
expandColumnKey,
indentSize,
iconSize,
rowKey,
columnsStyles: unref(columnsStyles),
expandedRowKeys: unref(expandedRowKeys),
ns,
}
const tableHeaderProps = {
ns,
headerClass,
headerProps,
resizingKey: unref(resizingKey),
}
const tableHeaderCellProps = {
ns,
sortBy,
sortState,
headerCellProps,
resizingKey: unref(resizingKey),
columnsStyles: unref(columnsStyles),
onColumnResizeEnd,
onColumnResizeStart,
onColumnResized,
onColumnSorted,
}
return (
<div class={[ns.b(), ns.e('root')]} style={unref(rootStyle)}>
{renderMainTable()}
<MainTable mainTableRef={mainTableRef} {...mainTableProps}>
{{
row: (props: TableGridRowSlotParams) => (
<Row {...props} {...tableRowProps}>
{{
row: slots.row,
cell: (props: TableV2RowCellRenderParam) => (
<Cell {...props} {...tableCellProps} />
),
}}
</Row>
),
header: (props: TableV2HeaderRendererParams) => (
<Header {...props} {...tableHeaderProps}>
{{
header: slots.header,
cell: (props: TableV2HeaderRowCellRendererParams) => (
<HeaderCell {...props} {...tableHeaderCellProps} />
),
}}
</Header>
),
}}
</MainTable>
</div>
)
}

View File

@ -149,6 +149,7 @@ export const tableV2Props = buildProps({
width: requiredNumber,
height: requiredNumber,
maxHeight: Number,
useIsScrolling: Boolean,
indentSize: {
type: Number,
default: 12,

View File

@ -396,3 +396,5 @@ function useTable(props: TableV2Props) {
}
export { useTable }
export type UseTableReturn = ReturnType<typeof useTable>

View File

@ -186,7 +186,6 @@ export type GridDefaultSlotParams = {
columnIndex: number
rowIndex: number
data: any
depth
key: number | string
isScrolling?: boolean
style: CSSProperties