Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/antd/components/table/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export default = () => {
const tableProps: TableProps = {
columns: [],
handle,
rowSelection: {} // 开启可选择
}

return (
Expand Down
21 changes: 20 additions & 1 deletion packages/antd/components/table/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ import type {
FilterValue,
SorterResult,
TableCurrentDataSource,
TableRowSelection,
} from 'antd/es/table/interface'
import { TooltipProps } from 'antd/es/tooltip'
import useTableSelections, { AntdTableSelectionResult } from './useSelections'

// 🚧-①: 屏蔽 React.StrictMode 副作用
// 🐞-①: 使用 render 实现的动态 Form.Item 会在表格增加、减少行时造成 Form 数据丢失!可以通过 🚧-② 绕开!
Expand Down Expand Up @@ -62,7 +64,9 @@ export interface TableProps<RecordType = Record<string, any>> extends Omit<AntdT
extra: TableCurrentDataSource<RecordType>
}
}) => Promise<({ data: RecordType[] } & Partial<Pick<TablePaginationConfig, 'current' | 'pageSize' | 'total'>>) | void>
rowSelection?: Partial<TableRowSelection<RecordType> & { disabled: boolean | ((row: RecordType) => boolean) }>
handle?: {
selection: AntdTableSelectionResult<RecordType>['action'] & AntdTableSelectionResult<RecordType>['state'],
query: (args?: Omit<Parameters<TableQuery<RecordType>>[0], 'count'>) => void
// React 单项数据流设计,遂抛出 dataSource
data: RecordType[]
Expand All @@ -77,6 +81,8 @@ export type TableColumn<RecordType = Record<string, any>> = Required<TableProps<
export type TableQuery<RecordType = Record<string, any>> = Required<TableProps<RecordType>>['query']
export type TableHandle<RecordType = Record<string, any>> = Required<TableProps<RecordType>>['handle']

const isObject = (any:any) => typeof any === 'object' && any !== null;

// Table 的可编辑表格的表单组件样式(对齐单元格)
function formatStyle(prefixCls = 'ant') {
const id = 'tr-form-item_style'
Expand All @@ -98,6 +104,7 @@ function TableAntd<RecordType = Record<string, any>, FormValues = Record<string,
query,
onChange,
pagination: props_pagination,
rowSelection: props_rowSelection,
...rest
} = props
const { getPrefixCls } = React.useContext(ConfigContext)
Expand All @@ -117,6 +124,10 @@ function TableAntd<RecordType = Record<string, any>, FormValues = Record<string,
const unMounted = useRef(false)
const refTimer = useRef<any>() // NodeJS.Timeout 不一定会有
const editable = useMemo(() => columns?.find(col => col.formItem), [columns])
const { state: selection, action: selectionAction, rowSelection } = useTableSelections(
data as Readonly<RecordType>[],
{ ...props_rowSelection, rowKey: rest.rowKey as any }
)

useLayoutEffect(() => {
unMounted.current = false // 🚧-①
Expand Down Expand Up @@ -187,8 +198,15 @@ function TableAntd<RecordType = Record<string, any>, FormValues = Record<string,
form.resetFields()
}
}
if (isObject(props_rowSelection)) {
handle.selection = {
// 🤔 handle只是引用,无法承载 React State
...selectionAction,
...selection,
}
}
}
}, [handle, data])
}, [handle, data, selection, selectionAction])

// init
useEffect(() => {
Expand Down Expand Up @@ -234,6 +252,7 @@ function TableAntd<RecordType = Record<string, any>, FormValues = Record<string,
},
rowKey: (_, index) => String(index), // Expect to pass from props!
pagination: page,
rowSelection: isObject(props_rowSelection) ? rowSelection : undefined,
...rest,
})

Expand Down
274 changes: 274 additions & 0 deletions packages/antd/components/table/useSelections.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
import React, { useMemo, useState, useRef } from 'react'

const isUndef = (value: unknown): value is undefined =>
typeof value === 'undefined'

type noop = (this: any, ...args: any[]) => any

type PickFunction<T extends noop> = (
this: ThisParameterType<T>,
...args: Parameters<T>
) => ReturnType<T>

function useMemoizedFn<T extends noop>(fn: T) {
if (typeof fn !== 'function') {
console.error(
`useMemoizedFn expected parameter is a function, got ${typeof fn}`
)
}

const fnRef = useRef<T>(fn)

// why not write `fnRef.current = fn`?
// https://github.com/alibaba/hooks/issues/728
fnRef.current = useMemo(() => fn, [fn])

const memoizedFn = useRef<PickFunction<T>>()
if (!memoizedFn.current) {
memoizedFn.current = function (this, ...args) {
return fnRef.current.apply(this, args)
}
}

return memoizedFn.current as T
}

export type RowSelectionType = 'checkbox' | 'radio'
export type RowSelectMethod = 'all' | 'none' | 'invert' | 'single' | 'multiple'
export type GetRowKey<RecordType> = (record: RecordType) => React.Key

export interface AntdTableRowSelection<RecordType> {
type: RowSelectionType
selectedRowKeys: React.Key[]
defaultSelectedRowKeys: React.Key[]
getCheckboxProps: (row: RecordType) => Partial<{ disabled: boolean }>
onChange: (
selectedRowKeys: React.Key[],
selectedRows: RecordType[],
info: { type: RowSelectMethod }
) => void
rowKey: string | keyof RecordType | GetRowKey<RecordType>
disabled: boolean | ((row: RecordType) => boolean)
[key: string]: any
}

export interface AntdTableSelectionResult<RecordType> {
state: {
allSelected: boolean
selectedRows: RecordType[]
selectedRowKeys: React.Key[]
}
action: {
select: (item: RecordType) => void
toggle: (item: RecordType) => void
unSelect: (item: RecordType) => void
toggleAll: () => void
selectAll: () => void
isSelected: (item: RecordType) => boolean
unSelectAll: () => void
setSelected: (keys: React.Key[]) => void
}
rowSelection: Omit<AntdTableRowSelection<RecordType>, 'rowKey' | 'disabled'>
}

function useAntdTableSelection<RecordType>(
rows: RecordType[] = [],
config?: Partial<AntdTableRowSelection<RecordType>>
): AntdTableSelectionResult<RecordType> {
if (!isUndef(config?.rowKey)) {
console.error(
`useAntdTableSelection expected config.rowKey is a function|string|number, got undefined.
Will default to using "key" as rowKey.
The value must be the same as the rowKey of the Antd Table component`
)
}

const {
rowKey = 'key',
disabled = false,
type = 'checkbox',
defaultSelectedRowKeys = [],
...rest
} = config || {}

const getRowKey = React.useMemo<GetRowKey<RecordType>>(() => {
if (typeof rowKey === 'function') {
return rowKey
}

return (record: RecordType) => (record as any)?.[rowKey]
}, [rowKey])

const allRowKeys = useMemo(() => rows.map((t) => getRowKey(t)), [rows])

const rowKeyMapRow = React.useMemo(
() =>
rows.reduce<Record<React.Key, RecordType>>((prev, cur) => {
prev[getRowKey(cur)] = cur
return prev
}, {}),
[rows]
)

// ========================= States =========================
const [selectedRows, setSelectedRows] = useState<RecordType[]>(
defaultSelectedRowKeys
.map((key) => rowKeyMapRow[key])
.filter((t) => !isUndef(t))
)
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>(
defaultSelectedRowKeys
)

const getDisabled = React.useMemo(() => {
if (typeof disabled === 'function') {
return (row: RecordType) => disabled(row)
}
if (typeof disabled === 'boolean') {
return () => disabled
}
return () => false
}, [disabled])

const getCheckboxProps = useMemoizedFn((row: RecordType) => {
const checkboxProps =
typeof rest?.getCheckboxProps === 'function'
? rest.getCheckboxProps?.(row)
: {}

return Object.assign({ disabled: getDisabled(row) }, checkboxProps)
})

const isValidRow = useMemoizedFn(
(row: RecordType) =>
!getCheckboxProps(row).disabled && allRowKeys.includes(getRowKey(row))
)

// ========================= Funcs =========================
const onSelectionChange: AntdTableRowSelection<RecordType>['onChange'] =
useMemoizedFn((rowKeys, records, info) => {
setSelectedRows(records)
setSelectedRowKeys(rowKeys)
rest?.onChange?.(rowKeys, records, info)
})

const onRowsChange = useMemoizedFn((records: RecordType[]) => {
/** based action change state, must have rowKey and row is't disabled.
* because of this, the config.rowKey is must
*/
const willSelected = records.filter((t) => isValidRow(t))
setSelectedRows(willSelected)
setSelectedRowKeys(willSelected.map((t) => getRowKey(t)))
})

// ========================= Select Memo States =========================
const selectedSet = useMemo(() => new Set(selectedRows), [selectedRows])

const noneSelected = useMemo(
() => rows.every((o) => !selectedSet.has(o)),
[rows, selectedSet]
)

const allSelected = useMemo(
() =>
rows.filter((t) => !getCheckboxProps(t).disabled).length ===
selectedRows.length && !noneSelected,
[rows, selectedSet, noneSelected]
)

// ========================= Select Action =========================
const isSelected = useMemoizedFn((item: RecordType) => selectedSet.has(item))

const select = useMemoizedFn((item: RecordType) => {
if (isValidRow(item)) {
selectedSet.add(item)
onRowsChange(Array.from(selectedSet))
}
})

const unSelect = useMemoizedFn((item: RecordType) => {
selectedSet.delete(item)
onRowsChange(Array.from(selectedSet))
})

const selectAll = useMemoizedFn(() => {
selectedSet.clear()

for (let i = 0; i < rows.length; i++) {
const row = rows[i]
if (isValidRow(row)) {
selectedSet.add(row)
if (type === 'radio') break
}
}

onRowsChange(Array.from(selectedSet))
})

const unSelectAll = useMemoizedFn(() => {
selectedSet.clear()
onRowsChange(Array.from(selectedSet))
})

const toggle = useMemoizedFn((item: RecordType) => {
if (isSelected(item)) {
unSelect(item)
} else {
select(item)
}
})

const toggleAll = useMemoizedFn(() =>
allSelected ? unSelectAll() : selectAll()
)

const setSelected = useMemoizedFn((rowKeys: React.Key[]) => {
selectedSet.clear()

const willRowKeys: React.Key[] = []
let key: any = null
let row: any = null

for (let i = 0; i < rowKeys.length; i++) {
key = rowKeys[i]
row = rowKeyMapRow[key]
if (isValidRow(row)) {
selectedSet.add(rowKeyMapRow[key])
willRowKeys.push(key)
/* if type is radio, should be used the first */
if (type === 'radio') break
}
}

setSelectedRows(Array.from(selectedSet))
setSelectedRowKeys(willRowKeys)
})

return {
state: {
allSelected,
selectedRows,
selectedRowKeys,
},
action: {
select,
toggle,
unSelect,
toggleAll,
selectAll,
isSelected,
unSelectAll,
setSelected,
},
rowSelection: {
...rest,
type,
onChange: onSelectionChange,
getCheckboxProps,
selectedRowKeys,
defaultSelectedRowKeys,
},
}
}

export default useAntdTableSelection
12 changes: 11 additions & 1 deletion packages/antd/view/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import {
UserOutlined,
VideoCameraOutlined,
} from '@ant-design/icons'
import { ConfigContext } from 'antd/es/config-provider'

const { Header, Sider, Content } = Layout

const AppLayout = () => {
const [collapsed, setCollapsed] = useState(false)
const navigate = useNavigate()
const location = useLocation()
const { getPrefixCls } = React.useContext(ConfigContext);

return (
<Layout style={{ height: '100%' }}>
Expand Down Expand Up @@ -60,6 +62,14 @@ const AppLayout = () => {
onClick() {
navigate('/table')
},
},
{
key: '/selection-table',
icon: <VideoCameraOutlined />,
label: 'Selection-table',
onClick() {
navigate('/selection-table')
},
},
{
key: '/table-edit',
Expand All @@ -81,7 +91,7 @@ const AppLayout = () => {
backgroundColor: 'white',
}}
>
<h2>Hey here! 👋</h2>
<h2>Hey here! 👋{getPrefixCls()}</h2>
<Divider />
<Outlet />
</Content>
Expand Down
Loading