Transfer穿梭框
双栏穿梭选择框。
何时使用#
需要在多个可选项中进行多选时。
比起 Select 和 TreeSelect,穿梭框占据更大的空间,可以展示可选项的更多信息。
穿梭选择框用直观的方式在两栏中移动元素,完成选择行为。
选择一个或以上的选项后,点击对应的方向键,可以把选中的选项移动到另一栏。其中,左边一栏为 source
,右边一栏为 target
,API 的设计也反映了这两个概念。
代码演示
11 项Source
- content1
- content2
- content3
- content4
- content5
- content6
- content7
- content8
- content9
- content10
- content11
9 项Target
- content12
- content13
- content14
- content15
- content16
- content17
- content18
- content19
- content20
TypeScript
JavaScript
import { Transfer } from 'infrad';
import type { TransferDirection } from 'infrad/es/transfer';
import React, { useState } from 'react';
interface RecordType {
key: string;
title: string;
description: string;
}
const mockData: RecordType[] = Array.from({ length: 20 }).map((_, i) => ({
key: i.toString(),
title: `content${i + 1}`,
description: `description of content${i + 1}`,
}));
const initialTargetKeys = mockData.filter(item => Number(item.key) > 10).map(item => item.key);
const App: React.FC = () => {
const [targetKeys, setTargetKeys] = useState(initialTargetKeys);
const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
const onChange = (nextTargetKeys: string[], direction: TransferDirection, moveKeys: string[]) => {
console.log('targetKeys:', nextTargetKeys);
console.log('direction:', direction);
console.log('moveKeys:', moveKeys);
setTargetKeys(nextTargetKeys);
};
const onSelectChange = (sourceSelectedKeys: string[], targetSelectedKeys: string[]) => {
console.log('sourceSelectedKeys:', sourceSelectedKeys);
console.log('targetSelectedKeys:', targetSelectedKeys);
setSelectedKeys([...sourceSelectedKeys, ...targetSelectedKeys]);
};
const onScroll = (direction: TransferDirection, e: React.SyntheticEvent<HTMLUListElement>) => {
console.log('direction:', direction);
console.log('target:', e.target);
};
return (
<Transfer
dataSource={mockData}
titles={['Source', 'Target']}
targetKeys={targetKeys}
selectedKeys={selectedKeys}
onChange={onChange}
onSelectChange={onSelectChange}
onScroll={onScroll}
render={item => item.title}
/>
);
};
export default App;
14 项Source
- content1
- content2
- content4
- content5
- content7
- content8
- content10
- content11
- content13
- content14
- content16
- content17
- content19
- content20
6 项Target
- content3
- content6
- content9
- content12
- content15
- content18
TypeScript
JavaScript
import { Switch, Transfer } from 'infrad';
import type { TransferDirection } from 'infrad/es/transfer';
import React, { useState } from 'react';
interface RecordType {
key: string;
title: string;
description: string;
disabled: boolean;
}
const mockData: RecordType[] = Array.from({ length: 20 }).map((_, i) => ({
key: i.toString(),
title: `content${i + 1}`,
description: `description of content${i + 1}`,
disabled: i % 3 < 1,
}));
const oriTargetKeys = mockData.filter(item => Number(item.key) % 3 > 1).map(item => item.key);
const App: React.FC = () => {
const [targetKeys, setTargetKeys] = useState<string[]>(oriTargetKeys);
const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
const [disabled, setDisabled] = useState(false);
const handleChange = (
newTargetKeys: string[],
direction: TransferDirection,
moveKeys: string[],
) => {
setTargetKeys(newTargetKeys);
console.log('targetKeys: ', newTargetKeys);
console.log('direction: ', direction);
console.log('moveKeys: ', moveKeys);
};
const handleSelectChange = (sourceSelectedKeys: string[], targetSelectedKeys: string[]) => {
setSelectedKeys([...sourceSelectedKeys, ...targetSelectedKeys]);
console.log('sourceSelectedKeys: ', sourceSelectedKeys);
console.log('targetSelectedKeys: ', targetSelectedKeys);
};
const handleScroll = (
direction: TransferDirection,
e: React.SyntheticEvent<HTMLUListElement, Event>,
) => {
console.log('direction:', direction);
console.log('target:', e.target);
};
const handleDisable = (checked: boolean) => {
setDisabled(checked);
};
return (
<>
<Transfer
dataSource={mockData}
titles={['Source', 'Target']}
targetKeys={targetKeys}
selectedKeys={selectedKeys}
onChange={handleChange}
onSelectChange={handleSelectChange}
onScroll={handleScroll}
render={item => item.title}
disabled={disabled}
oneWay
style={{ marginBottom: 16 }}
/>
<Switch
unCheckedChildren="disabled"
checkedChildren="disabled"
checked={disabled}
onChange={handleDisable}
/>
</>
);
};
export default App;
0 项
No Data
0 项
No Data
TypeScript
JavaScript
import { Transfer } from 'infrad';
import type { TransferDirection } from 'infrad/es/transfer';
import React, { useEffect, useState } from 'react';
interface RecordType {
key: string;
title: string;
description: string;
chosen: boolean;
}
const App: React.FC = () => {
const [mockData, setMockData] = useState<RecordType[]>([]);
const [targetKeys, setTargetKeys] = useState<string[]>([]);
const getMock = () => {
const tempTargetKeys = [];
const tempMockData = [];
for (let i = 0; i < 20; i++) {
const data = {
key: i.toString(),
title: `content${i + 1}`,
description: `description of content${i + 1}`,
chosen: Math.random() * 2 > 1,
};
if (data.chosen) {
tempTargetKeys.push(data.key);
}
tempMockData.push(data);
}
setMockData(tempMockData);
setTargetKeys(tempTargetKeys);
};
useEffect(() => {
getMock();
}, []);
const filterOption = (inputValue: string, option: RecordType) =>
option.description.indexOf(inputValue) > -1;
const handleChange = (newTargetKeys: string[]) => {
setTargetKeys(newTargetKeys);
};
const handleSearch = (dir: TransferDirection, value: string) => {
console.log('search:', dir, value);
};
return (
<Transfer
dataSource={mockData}
showSearch
filterOption={filterOption}
targetKeys={targetKeys}
onChange={handleChange}
onSearch={handleSearch}
render={item => item.title}
/>
);
};
export default App;
TypeScript
JavaScript
import { Button, Transfer } from 'infrad';
import type { TransferDirection, TransferListProps } from 'infrad/es/transfer';
import React, { useEffect, useState } from 'react';
interface RecordType {
key: string;
title: string;
description: string;
chosen: boolean;
}
const App: React.FC = () => {
const [mockData, setMockData] = useState<RecordType[]>([]);
const [targetKeys, setTargetKeys] = useState<string[]>([]);
const getMock = () => {
const tempTargetKeys = [];
const tempMockData = [];
for (let i = 0; i < 20; i++) {
const data = {
key: i.toString(),
title: `content${i + 1}`,
description: `description of content${i + 1}`,
chosen: Math.random() * 2 > 1,
};
if (data.chosen) {
tempTargetKeys.push(data.key);
}
tempMockData.push(data);
}
setMockData(tempMockData);
setTargetKeys(tempTargetKeys);
};
useEffect(() => {
getMock();
}, []);
const handleChange = (newTargetKeys: string[]) => {
setTargetKeys(newTargetKeys);
};
const renderFooter = (
_: TransferListProps<any>,
{
direction,
}: {
direction: TransferDirection;
},
) => {
if (direction === 'left') {
return (
<Button size="small" style={{ float: 'left', margin: 5 }} onClick={getMock}>
Left button reload
</Button>
);
}
return (
<Button size="small" style={{ float: 'right', margin: 5 }} onClick={getMock}>
Right button reload
</Button>
);
};
return (
<Transfer
dataSource={mockData}
showSearch
listStyle={{
width: 250,
height: 300,
}}
operations={['to right', 'to left']}
targetKeys={targetKeys}
onChange={handleChange}
render={item => `${item.title}-${item.description}`}
footer={renderFooter}
/>
);
};
export default App;
0 项
No Data
0 项
No Data
TypeScript
JavaScript
import { Transfer } from 'infrad';
import type { TransferDirection } from 'infrad/es/transfer';
import React, { useEffect, useState } from 'react';
interface RecordType {
key: string;
title: string;
description: string;
chosen: boolean;
}
const App: React.FC = () => {
const [mockData, setMockData] = useState<RecordType[]>([]);
const [targetKeys, setTargetKeys] = useState<string[]>([]);
const getMock = () => {
const tempTargetKeys = [];
const tempMockData = [];
for (let i = 0; i < 20; i++) {
const data = {
key: i.toString(),
title: `content${i + 1}`,
description: `description of content${i + 1}`,
chosen: Math.random() * 2 > 1,
};
if (data.chosen) {
tempTargetKeys.push(data.key);
}
tempMockData.push(data);
}
setMockData(tempMockData);
setTargetKeys(tempTargetKeys);
};
useEffect(() => {
getMock();
}, []);
const handleChange = (
newTargetKeys: string[],
direction: TransferDirection,
moveKeys: string[],
) => {
console.log(newTargetKeys, direction, moveKeys);
setTargetKeys(newTargetKeys);
};
const renderItem = (item: RecordType) => {
const customLabel = (
<span className="custom-item">
{item.title} - {item.description}
</span>
);
return {
label: customLabel, // for displayed item
value: item.title, // for title and filter matching
};
};
return (
<Transfer
dataSource={mockData}
listStyle={{
width: 300,
height: 300,
}}
targetKeys={targetKeys}
onChange={handleChange}
render={renderItem}
/>
);
};
export default App;
0 项
No Data
0 项
No Data
TypeScript
JavaScript
import { Switch, Transfer } from 'infrad';
import type { TransferDirection } from 'infrad/es/transfer';
import React, { useEffect, useState } from 'react';
interface RecordType {
key: string;
title: string;
description: string;
chosen: boolean;
}
const App: React.FC = () => {
const [oneWay, setOneWay] = useState(false);
const [mockData, setMockData] = useState<RecordType[]>([]);
const [targetKeys, setTargetKeys] = useState<string[]>([]);
useEffect(() => {
const newTargetKeys = [];
const newMockData = [];
for (let i = 0; i < 2000; i++) {
const data = {
key: i.toString(),
title: `content${i + 1}`,
description: `description of content${i + 1}`,
chosen: Math.random() * 2 > 1,
};
if (data.chosen) {
newTargetKeys.push(data.key);
}
newMockData.push(data);
}
setTargetKeys(newTargetKeys);
setMockData(newMockData);
}, []);
const onChange = (newTargetKeys: string[], direction: TransferDirection, moveKeys: string[]) => {
console.log(newTargetKeys, direction, moveKeys);
setTargetKeys(newTargetKeys);
};
return (
<>
<Transfer
dataSource={mockData}
targetKeys={targetKeys}
onChange={onChange}
render={item => item.title}
oneWay={oneWay}
pagination
/>
<br />
<Switch
unCheckedChildren="one way"
checkedChildren="one way"
checked={oneWay}
onChange={setOneWay}
/>
</>
);
};
export default App;
14 项
Name | Tag | Description | |
---|---|---|---|
content1 | cat | description of content1 | |
content2 | dog | description of content2 | |
content4 | cat | description of content4 | |
content5 | dog | description of content5 | |
content7 | cat | description of content7 | |
content8 | dog | description of content8 | |
content10 | cat | description of content10 | |
content11 | dog | description of content11 | |
content13 | cat | description of content13 | |
content14 | dog | description of content14 |
6 项
Name | |
---|---|
content3 | |
content6 | |
content9 | |
content12 | |
content15 | |
content18 |
TypeScript
JavaScript
import { Switch, Table, Tag, Transfer } from 'infrad';
import type { ColumnsType, TableRowSelection } from 'infrad/es/table/interface';
import type { TransferItem, TransferProps } from 'infrad/es/transfer';
import difference from 'lodash/difference';
import React, { useState } from 'react';
interface RecordType {
key: string;
title: string;
description: string;
disabled: boolean;
tag: string;
}
interface DataType {
title: string;
tag: string;
description: string;
}
interface TableTransferProps extends TransferProps<TransferItem> {
dataSource: DataType[];
leftColumns: ColumnsType<DataType>;
rightColumns: ColumnsType<DataType>;
}
// Customize Table Transfer
const TableTransfer = ({ leftColumns, rightColumns, ...restProps }: TableTransferProps) => (
<Transfer {...restProps}>
{({
direction,
filteredItems,
onItemSelectAll,
onItemSelect,
selectedKeys: listSelectedKeys,
disabled: listDisabled,
}) => {
const columns = direction === 'left' ? leftColumns : rightColumns;
const rowSelection: TableRowSelection<TransferItem> = {
getCheckboxProps: item => ({ disabled: listDisabled || item.disabled }),
onSelectAll(selected, selectedRows) {
const treeSelectedKeys = selectedRows
.filter(item => !item.disabled)
.map(({ key }) => key);
const diffKeys = selected
? difference(treeSelectedKeys, listSelectedKeys)
: difference(listSelectedKeys, treeSelectedKeys);
onItemSelectAll(diffKeys as string[], selected);
},
onSelect({ key }, selected) {
onItemSelect(key as string, selected);
},
selectedRowKeys: listSelectedKeys,
};
return (
<Table
rowSelection={rowSelection}
columns={columns}
dataSource={filteredItems}
size="small"
style={{ pointerEvents: listDisabled ? 'none' : undefined }}
onRow={({ key, disabled: itemDisabled }) => ({
onClick: () => {
if (itemDisabled || listDisabled) return;
onItemSelect(key as string, !listSelectedKeys.includes(key as string));
},
})}
/>
);
}}
</Transfer>
);
const mockTags = ['cat', 'dog', 'bird'];
const mockData: RecordType[] = Array.from({ length: 20 }).map((_, i) => ({
key: i.toString(),
title: `content${i + 1}`,
description: `description of content${i + 1}`,
disabled: i % 4 === 0,
tag: mockTags[i % 3],
}));
const originTargetKeys = mockData.filter(item => Number(item.key) % 3 > 1).map(item => item.key);
const leftTableColumns: ColumnsType<DataType> = [
{
dataIndex: 'title',
title: 'Name',
},
{
dataIndex: 'tag',
title: 'Tag',
render: tag => <Tag>{tag}</Tag>,
},
{
dataIndex: 'description',
title: 'Description',
},
];
const rightTableColumns: ColumnsType<Pick<DataType, 'title'>> = [
{
dataIndex: 'title',
title: 'Name',
},
];
const App: React.FC = () => {
const [targetKeys, setTargetKeys] = useState<string[]>(originTargetKeys);
const [disabled, setDisabled] = useState(false);
const [showSearch, setShowSearch] = useState(false);
const onChange = (nextTargetKeys: string[]) => {
setTargetKeys(nextTargetKeys);
};
const triggerDisable = (checked: boolean) => {
setDisabled(checked);
};
const triggerShowSearch = (checked: boolean) => {
setShowSearch(checked);
};
return (
<>
<TableTransfer
dataSource={mockData}
targetKeys={targetKeys}
disabled={disabled}
showSearch={showSearch}
onChange={onChange}
filterOption={(inputValue, item) =>
item.title!.indexOf(inputValue) !== -1 || item.tag.indexOf(inputValue) !== -1
}
leftColumns={leftTableColumns}
rightColumns={rightTableColumns}
/>
<Switch
unCheckedChildren="disabled"
checkedChildren="disabled"
checked={disabled}
onChange={triggerDisable}
style={{ marginTop: 16 }}
/>
<Switch
unCheckedChildren="showSearch"
checkedChildren="showSearch"
checked={showSearch}
onChange={triggerShowSearch}
style={{ marginTop: 16 }}
/>
</>
);
};
export default App;
#components-transfer-demo-table-transfer .ant-table td {
background: transparent;
}
5 项
0 项
No Data
TypeScript
JavaScript
import { Transfer, Tree } from 'infrad';
import type { TransferDirection, TransferItem } from 'infrad/es/transfer';
import type { DataNode } from 'infrad/es/tree';
import React, { useState } from 'react';
interface TreeTransferProps {
dataSource: DataNode[];
targetKeys: string[];
onChange: (targetKeys: string[], direction: TransferDirection, moveKeys: string[]) => void;
}
// Customize Table Transfer
const isChecked = (selectedKeys: (string | number)[], eventKey: string | number) =>
selectedKeys.includes(eventKey);
const generateTree = (treeNodes: DataNode[] = [], checkedKeys: string[] = []): DataNode[] =>
treeNodes.map(({ children, ...props }) => ({
...props,
disabled: checkedKeys.includes(props.key as string),
children: generateTree(children, checkedKeys),
}));
const TreeTransfer = ({ dataSource, targetKeys, ...restProps }: TreeTransferProps) => {
const transferDataSource: TransferItem[] = [];
function flatten(list: DataNode[] = []) {
list.forEach(item => {
transferDataSource.push(item as TransferItem);
flatten(item.children);
});
}
flatten(dataSource);
return (
<Transfer
{...restProps}
targetKeys={targetKeys}
dataSource={transferDataSource}
className="tree-transfer"
render={item => item.title!}
showSelectAll={false}
>
{({ direction, onItemSelect, selectedKeys }) => {
if (direction === 'left') {
const checkedKeys = [...selectedKeys, ...targetKeys];
return (
<Tree
blockNode
checkable
checkStrictly
defaultExpandAll
checkedKeys={checkedKeys}
treeData={generateTree(dataSource, targetKeys)}
onCheck={(_, { node: { key } }) => {
onItemSelect(key as string, !isChecked(checkedKeys, key));
}}
onSelect={(_, { node: { key } }) => {
onItemSelect(key as string, !isChecked(checkedKeys, key));
}}
/>
);
}
}}
</Transfer>
);
};
const treeData: DataNode[] = [
{ key: '0-0', title: '0-0' },
{
key: '0-1',
title: '0-1',
children: [
{ key: '0-1-0', title: '0-1-0' },
{ key: '0-1-1', title: '0-1-1' },
],
},
{ key: '0-2', title: '0-3' },
];
const App: React.FC = () => {
const [targetKeys, setTargetKeys] = useState<string[]>([]);
const onChange = (keys: string[]) => {
setTargetKeys(keys);
};
return <TreeTransfer dataSource={treeData} targetKeys={targetKeys} onChange={onChange} />;
};
export default App;
0 项
No Data
0 项
No Data
0 项
No Data
0 项
No Data
TypeScript
JavaScript
import { Space, Transfer } from 'infrad';
import React from 'react';
const App: React.FC = () => (
<Space direction="vertical">
<Transfer status="error" />
<Transfer status="warning" showSearch />
</Space>
);
export default App;
4.19.0
API#
Transfer#
参数 | 说明 | 类型 | 默认值 | 版本 |
---|---|---|---|---|
dataSource | 数据源,其中的数据将会被渲染到左边一栏中,targetKeys 中指定的除外 | RecordType extends TransferItem = TransferItem[] | [] | |
disabled | 是否禁用 | boolean | false | |
filterOption | 接收 inputValue option 两个参数,当 option 符合筛选条件时,应返回 true,反之则返回 false | (inputValue, option): boolean | - | |
footer | 底部渲染函数 | (props, { direction }) => ReactNode | - | direction: 4.17.0 |
listStyle | 两个穿梭框的自定义样式 | object|({direction: left | right }) => object | - | |
locale | 各种语言 | { itemUnit: string; itemsUnit: string; searchPlaceholder: string; notFoundContent: ReactNode | ReactNode[]; } | { itemUnit: 项 , itemsUnit: 项 , searchPlaceholder: 请输入搜索内容 } | |
oneWay | 展示为单向样式 | boolean | false | 4.3.0 |
operations | 操作文案集合,顺序从上至下 | string[] | [> , < ] | |
operationStyle | 操作栏的自定义样式 | CSSProperties | - | |
pagination | 使用分页样式,自定义渲染列表下无效 | boolean | { pageSize: number, simple: boolean, showSizeChanger?: boolean, showLessItems?: boolean } | false | 4.3.0 |
render | 每行数据渲染函数,该函数的入参为 dataSource 中的项,返回值为 ReactElement。或者返回一个普通对象,其中 label 字段为 ReactElement,value 字段为 title | (record) => ReactNode | - | |
selectAllLabels | 自定义顶部多选框标题的集合 | (ReactNode | (info: { selectedCount: number, totalCount: number }) => ReactNode)[] | - | |
selectedKeys | 设置哪些项应该被选中 | string[] | [] | |
showSearch | 是否显示搜索框 | boolean | false | |
showSelectAll | 是否展示全选勾选框 | boolean | true | |
status | 设置校验状态 | 'error' | 'warning' | - | 4.19.0 |
targetKeys | 显示在右侧框数据的 key 集合 | string[] | [] | |
titles | 标题集合,顺序从左至右 | ReactNode[] | - | |
onChange | 选项在两栏之间转移时的回调函数 | (targetKeys, direction, moveKeys): void | - | |
onScroll | 选项列表滚动时的回调函数 | (direction, event): void | - | |
onSearch | 搜索框内容时改变时的回调函数 | (direction: left | right , value: string): void | - | |
onSelectChange | 选中项发生改变时的回调函数 | (sourceSelectedKeys, targetSelectedKeys): void | - |
Render Props#
Transfer 支持接收 children
自定义渲染列表,并返回以下参数:
参数 | 说明 | 类型 | 版本 |
---|---|---|---|
direction | 渲染列表的方向 | left | right | |
disabled | 是否禁用列表 | boolean | |
filteredItems | 过滤后的数据 | RecordType[] | |
selectedKeys | 选中的条目 | string[] | |
onItemSelect | 勾选条目 | (key: string, selected: boolean) | |
onItemSelectAll | 勾选一组条目 | (keys: string[], selected: boolean) |
参考示例#
<Transfer {...props}>{listProps => <YourComponent {...listProps} />}</Transfer>
注意#
按照 React 的规范,所有的组件数组必须绑定 key。在 Transfer 中,dataSource
里的数据值需要指定 key
值。对于 dataSource
默认将每列数据的 key
属性作为唯一的标识。
如果你的数据没有这个属性,务必使用 rowKey
来指定数据列的主键。
// 比如你的数据主键是 uid
return <Transfer rowKey={record => record.uid} />;
FAQ#
怎样让 Transfer 穿梭框列表支持异步数据加载#
为了保持页码同步,在勾选时可以不移除选项而以禁用代替:https://codesandbox.io/s/objective-wing-6iqbx