Transfer穿梭框

双栏穿梭选择框。

何时使用

  • 需要在多个可选项中进行多选时。

  • 比起 Select 和 TreeSelect,穿梭框占据更大的空间,可以展示可选项的更多信息。

穿梭选择框用直观的方式在两栏中移动元素,完成选择行为。

选择一个或以上的选项后,点击对应的方向键,可以把选中的选项移动到另一栏。其中,左边一栏为 source,右边一栏为 target,API 的设计也反映了这两个概念。

代码演示

Transfer穿梭框 - 图1

基本用法

最基本的用法,展示了 dataSourcetargetKeys、每行的渲染函数 render 以及回调函数 onChange onSelectChange onScroll 的用法。

  1. import React, { useState } from 'react';
  2. import { Transfer } from 'antd';
  3. const mockData = [];
  4. for (let i = 0; i < 20; i++) {
  5. mockData.push({
  6. key: i.toString(),
  7. title: `content${i + 1}`,
  8. description: `description of content${i + 1}`,
  9. });
  10. }
  11. const initialTargetKeys = mockData.filter(item => +item.key > 10).map(item => item.key);
  12. const App = () => {
  13. const [targetKeys, setTargetKeys] = useState(initialTargetKeys);
  14. const [selectedKeys, setSelectedKeys] = useState([]);
  15. const onChange = (nextTargetKeys, direction, moveKeys) => {
  16. console.log('targetKeys:', nextTargetKeys);
  17. console.log('direction:', direction);
  18. console.log('moveKeys:', moveKeys);
  19. setTargetKeys(nextTargetKeys);
  20. };
  21. const onSelectChange = (sourceSelectedKeys, targetSelectedKeys) => {
  22. console.log('sourceSelectedKeys:', sourceSelectedKeys);
  23. console.log('targetSelectedKeys:', targetSelectedKeys);
  24. setSelectedKeys([...sourceSelectedKeys, ...targetSelectedKeys]);
  25. };
  26. const onScroll = (direction, e) => {
  27. console.log('direction:', direction);
  28. console.log('target:', e.target);
  29. };
  30. return (
  31. <Transfer
  32. dataSource={mockData}
  33. titles={['Source', 'Target']}
  34. targetKeys={targetKeys}
  35. selectedKeys={selectedKeys}
  36. onChange={onChange}
  37. onSelectChange={onSelectChange}
  38. onScroll={onScroll}
  39. render={item => item.title}
  40. />
  41. );
  42. };
  43. ReactDOM.render(<App />, mountNode);

Transfer穿梭框 - 图2

单向样式

通过 oneWay 将 Transfer 转为单向样式。

  1. import { Transfer, Switch } from 'antd';
  2. const mockData = [];
  3. for (let i = 0; i < 20; i++) {
  4. mockData.push({
  5. key: i.toString(),
  6. title: `content${i + 1}`,
  7. description: `description of content${i + 1}`,
  8. disabled: i % 3 < 1,
  9. });
  10. }
  11. const oriTargetKeys = mockData.filter(item => +item.key % 3 > 1).map(item => item.key);
  12. class App extends React.Component {
  13. state = {
  14. targetKeys: oriTargetKeys,
  15. selectedKeys: [],
  16. disabled: false,
  17. };
  18. handleChange = (nextTargetKeys, direction, moveKeys) => {
  19. this.setState({ targetKeys: nextTargetKeys });
  20. console.log('targetKeys: ', nextTargetKeys);
  21. console.log('direction: ', direction);
  22. console.log('moveKeys: ', moveKeys);
  23. };
  24. handleSelectChange = (sourceSelectedKeys, targetSelectedKeys) => {
  25. this.setState({ selectedKeys: [...sourceSelectedKeys, ...targetSelectedKeys] });
  26. console.log('sourceSelectedKeys: ', sourceSelectedKeys);
  27. console.log('targetSelectedKeys: ', targetSelectedKeys);
  28. };
  29. handleScroll = (direction, e) => {
  30. console.log('direction:', direction);
  31. console.log('target:', e.target);
  32. };
  33. handleDisable = disabled => {
  34. this.setState({ disabled });
  35. };
  36. render() {
  37. const { targetKeys, selectedKeys, disabled } = this.state;
  38. return (
  39. <>
  40. <Transfer
  41. dataSource={mockData}
  42. titles={['Source', 'Target']}
  43. targetKeys={targetKeys}
  44. selectedKeys={selectedKeys}
  45. onChange={this.handleChange}
  46. onSelectChange={this.handleSelectChange}
  47. onScroll={this.handleScroll}
  48. render={item => item.title}
  49. disabled={disabled}
  50. oneWay
  51. style={{ marginBottom: 16 }}
  52. />
  53. <Switch
  54. unCheckedChildren="disabled"
  55. checkedChildren="disabled"
  56. checked={disabled}
  57. onChange={this.handleDisable}
  58. />
  59. </>
  60. );
  61. }
  62. }
  63. ReactDOM.render(<App />, mountNode);

Transfer穿梭框 - 图3

带搜索框

带搜索框的穿梭框,可以自定义搜索函数。

  1. import { Transfer } from 'antd';
  2. class App extends React.Component {
  3. state = {
  4. mockData: [],
  5. targetKeys: [],
  6. };
  7. componentDidMount() {
  8. this.getMock();
  9. }
  10. getMock = () => {
  11. const targetKeys = [];
  12. const mockData = [];
  13. for (let i = 0; i < 20; i++) {
  14. const data = {
  15. key: i.toString(),
  16. title: `content${i + 1}`,
  17. description: `description of content${i + 1}`,
  18. chosen: Math.random() * 2 > 1,
  19. };
  20. if (data.chosen) {
  21. targetKeys.push(data.key);
  22. }
  23. mockData.push(data);
  24. }
  25. this.setState({ mockData, targetKeys });
  26. };
  27. filterOption = (inputValue, option) => option.description.indexOf(inputValue) > -1;
  28. handleChange = targetKeys => {
  29. this.setState({ targetKeys });
  30. };
  31. handleSearch = (dir, value) => {
  32. console.log('search:', dir, value);
  33. };
  34. render() {
  35. return (
  36. <Transfer
  37. dataSource={this.state.mockData}
  38. showSearch
  39. filterOption={this.filterOption}
  40. targetKeys={this.state.targetKeys}
  41. onChange={this.handleChange}
  42. onSearch={this.handleSearch}
  43. render={item => item.title}
  44. />
  45. );
  46. }
  47. }
  48. ReactDOM.render(<App />, mountNode);

Transfer穿梭框 - 图4

高级用法

穿梭框高级用法,可配置操作文案,可定制宽高,可对底部进行自定义渲染。

  1. import { Transfer, Button } from 'antd';
  2. class App extends React.Component {
  3. state = {
  4. mockData: [],
  5. targetKeys: [],
  6. };
  7. componentDidMount() {
  8. this.getMock();
  9. }
  10. getMock = () => {
  11. const targetKeys = [];
  12. const mockData = [];
  13. for (let i = 0; i < 20; i++) {
  14. const data = {
  15. key: i.toString(),
  16. title: `content${i + 1}`,
  17. description: `description of content${i + 1}`,
  18. chosen: Math.random() * 2 > 1,
  19. };
  20. if (data.chosen) {
  21. targetKeys.push(data.key);
  22. }
  23. mockData.push(data);
  24. }
  25. this.setState({ mockData, targetKeys });
  26. };
  27. handleChange = targetKeys => {
  28. this.setState({ targetKeys });
  29. };
  30. renderFooter = () => (
  31. <Button size="small" style={{ float: 'right', margin: 5 }} onClick={this.getMock}>
  32. reload
  33. </Button>
  34. );
  35. render() {
  36. return (
  37. <Transfer
  38. dataSource={this.state.mockData}
  39. showSearch
  40. listStyle={{
  41. width: 250,
  42. height: 300,
  43. }}
  44. operations={['to right', 'to left']}
  45. targetKeys={this.state.targetKeys}
  46. onChange={this.handleChange}
  47. render={item => `${item.title}-${item.description}`}
  48. footer={this.renderFooter}
  49. />
  50. );
  51. }
  52. }
  53. ReactDOM.render(<App />, mountNode);

Transfer穿梭框 - 图5

自定义渲染行数据

自定义渲染每一个 Transfer Item,可用于渲染复杂数据。

  1. import { Transfer } from 'antd';
  2. class App extends React.Component {
  3. state = {
  4. mockData: [],
  5. targetKeys: [],
  6. };
  7. componentDidMount() {
  8. this.getMock();
  9. }
  10. getMock = () => {
  11. const targetKeys = [];
  12. const mockData = [];
  13. for (let i = 0; i < 20; i++) {
  14. const data = {
  15. key: i.toString(),
  16. title: `content${i + 1}`,
  17. description: `description of content${i + 1}`,
  18. chosen: Math.random() * 2 > 1,
  19. };
  20. if (data.chosen) {
  21. targetKeys.push(data.key);
  22. }
  23. mockData.push(data);
  24. }
  25. this.setState({ mockData, targetKeys });
  26. };
  27. handleChange = (targetKeys, direction, moveKeys) => {
  28. console.log(targetKeys, direction, moveKeys);
  29. this.setState({ targetKeys });
  30. };
  31. renderItem = item => {
  32. const customLabel = (
  33. <span className="custom-item">
  34. {item.title} - {item.description}
  35. </span>
  36. );
  37. return {
  38. label: customLabel, // for displayed item
  39. value: item.title, // for title and filter matching
  40. };
  41. };
  42. render() {
  43. return (
  44. <Transfer
  45. dataSource={this.state.mockData}
  46. listStyle={{
  47. width: 300,
  48. height: 300,
  49. }}
  50. targetKeys={this.state.targetKeys}
  51. onChange={this.handleChange}
  52. render={this.renderItem}
  53. />
  54. );
  55. }
  56. }
  57. ReactDOM.render(<App />, mountNode);

Transfer穿梭框 - 图6

分页

大数据下使用分页。

  1. import { Transfer, Switch } from 'antd';
  2. const App = () => {
  3. const [oneWay, setOneWay] = React.useState(false);
  4. const [mockData, setMockData] = React.useState([]);
  5. const [targetKeys, setTargetKeys] = React.useState([]);
  6. React.useEffect(() => {
  7. const newTargetKeys = [];
  8. const newMockData = [];
  9. for (let i = 0; i < 2000; i++) {
  10. const data = {
  11. key: i.toString(),
  12. title: `content${i + 1}`,
  13. description: `description of content${i + 1}`,
  14. chosen: Math.random() * 2 > 1,
  15. };
  16. if (data.chosen) {
  17. newTargetKeys.push(data.key);
  18. }
  19. newMockData.push(data);
  20. }
  21. setTargetKeys(newTargetKeys);
  22. setMockData(newMockData);
  23. }, []);
  24. const onChange = (newTargetKeys, direction, moveKeys) => {
  25. console.log(newTargetKeys, direction, moveKeys);
  26. setTargetKeys(newTargetKeys);
  27. };
  28. return (
  29. <>
  30. <Transfer
  31. dataSource={mockData}
  32. targetKeys={targetKeys}
  33. onChange={onChange}
  34. render={item => item.title}
  35. oneWay={oneWay}
  36. pagination
  37. />
  38. <br />
  39. <Switch
  40. unCheckedChildren="one way"
  41. checkedChildren="one way"
  42. checked={oneWay}
  43. onChange={setOneWay}
  44. />
  45. </>
  46. );
  47. };
  48. ReactDOM.render(<App />, mountNode);

Transfer穿梭框 - 图7

表格穿梭框

使用 Table 组件作为自定义渲染列表。

  1. import { Transfer, Switch, Table, Tag } from 'antd';
  2. import difference from 'lodash/difference';
  3. // Customize Table Transfer
  4. const TableTransfer = ({ leftColumns, rightColumns, ...restProps }) => (
  5. <Transfer {...restProps} showSelectAll={false}>
  6. {({
  7. direction,
  8. filteredItems,
  9. onItemSelectAll,
  10. onItemSelect,
  11. selectedKeys: listSelectedKeys,
  12. disabled: listDisabled,
  13. }) => {
  14. const columns = direction === 'left' ? leftColumns : rightColumns;
  15. const rowSelection = {
  16. getCheckboxProps: item => ({ disabled: listDisabled || item.disabled }),
  17. onSelectAll(selected, selectedRows) {
  18. const treeSelectedKeys = selectedRows
  19. .filter(item => !item.disabled)
  20. .map(({ key }) => key);
  21. const diffKeys = selected
  22. ? difference(treeSelectedKeys, listSelectedKeys)
  23. : difference(listSelectedKeys, treeSelectedKeys);
  24. onItemSelectAll(diffKeys, selected);
  25. },
  26. onSelect({ key }, selected) {
  27. onItemSelect(key, selected);
  28. },
  29. selectedRowKeys: listSelectedKeys,
  30. };
  31. return (
  32. <Table
  33. rowSelection={rowSelection}
  34. columns={columns}
  35. dataSource={filteredItems}
  36. size="small"
  37. style={{ pointerEvents: listDisabled ? 'none' : null }}
  38. onRow={({ key, disabled: itemDisabled }) => ({
  39. onClick: () => {
  40. if (itemDisabled || listDisabled) return;
  41. onItemSelect(key, !listSelectedKeys.includes(key));
  42. },
  43. })}
  44. />
  45. );
  46. }}
  47. </Transfer>
  48. );
  49. const mockTags = ['cat', 'dog', 'bird'];
  50. const mockData = [];
  51. for (let i = 0; i < 20; i++) {
  52. mockData.push({
  53. key: i.toString(),
  54. title: `content${i + 1}`,
  55. description: `description of content${i + 1}`,
  56. disabled: i % 4 === 0,
  57. tag: mockTags[i % 3],
  58. });
  59. }
  60. const originTargetKeys = mockData.filter(item => +item.key % 3 > 1).map(item => item.key);
  61. const leftTableColumns = [
  62. {
  63. dataIndex: 'title',
  64. title: 'Name',
  65. },
  66. {
  67. dataIndex: 'tag',
  68. title: 'Tag',
  69. render: tag => <Tag>{tag}</Tag>,
  70. },
  71. {
  72. dataIndex: 'description',
  73. title: 'Description',
  74. },
  75. ];
  76. const rightTableColumns = [
  77. {
  78. dataIndex: 'title',
  79. title: 'Name',
  80. },
  81. ];
  82. class App extends React.Component {
  83. state = {
  84. targetKeys: originTargetKeys,
  85. disabled: false,
  86. showSearch: false,
  87. };
  88. onChange = nextTargetKeys => {
  89. this.setState({ targetKeys: nextTargetKeys });
  90. };
  91. triggerDisable = disabled => {
  92. this.setState({ disabled });
  93. };
  94. triggerShowSearch = showSearch => {
  95. this.setState({ showSearch });
  96. };
  97. render() {
  98. const { targetKeys, disabled, showSearch } = this.state;
  99. return (
  100. <>
  101. <TableTransfer
  102. dataSource={mockData}
  103. targetKeys={targetKeys}
  104. disabled={disabled}
  105. showSearch={showSearch}
  106. onChange={this.onChange}
  107. filterOption={(inputValue, item) =>
  108. item.title.indexOf(inputValue) !== -1 || item.tag.indexOf(inputValue) !== -1
  109. }
  110. leftColumns={leftTableColumns}
  111. rightColumns={rightTableColumns}
  112. />
  113. <Switch
  114. unCheckedChildren="disabled"
  115. checkedChildren="disabled"
  116. checked={disabled}
  117. onChange={this.triggerDisable}
  118. style={{ marginTop: 16 }}
  119. />
  120. <Switch
  121. unCheckedChildren="showSearch"
  122. checkedChildren="showSearch"
  123. checked={showSearch}
  124. onChange={this.triggerShowSearch}
  125. style={{ marginTop: 16 }}
  126. />
  127. </>
  128. );
  129. }
  130. }
  131. ReactDOM.render(<App />, mountNode);
  1. #components-transfer-demo-table-transfer .ant-table td {
  2. background: transparent;
  3. }

Transfer穿梭框 - 图8

树穿梭框

使用 Tree 组件作为自定义渲染列表。

  1. import React, { useState } from 'react';
  2. import { Transfer, Tree } from 'antd';
  3. // Customize Table Transfer
  4. const isChecked = (selectedKeys, eventKey) => selectedKeys.indexOf(eventKey) !== -1;
  5. const generateTree = (treeNodes = [], checkedKeys = []) =>
  6. treeNodes.map(({ children, ...props }) => ({
  7. ...props,
  8. disabled: checkedKeys.includes(props.key),
  9. children: generateTree(children, checkedKeys),
  10. }));
  11. const TreeTransfer = ({ dataSource, targetKeys, ...restProps }) => {
  12. const transferDataSource = [];
  13. function flatten(list = []) {
  14. list.forEach(item => {
  15. transferDataSource.push(item);
  16. flatten(item.children);
  17. });
  18. }
  19. flatten(dataSource);
  20. return (
  21. <Transfer
  22. {...restProps}
  23. targetKeys={targetKeys}
  24. dataSource={transferDataSource}
  25. className="tree-transfer"
  26. render={item => item.title}
  27. showSelectAll={false}
  28. >
  29. {({ direction, onItemSelect, selectedKeys }) => {
  30. if (direction === 'left') {
  31. const checkedKeys = [...selectedKeys, ...targetKeys];
  32. return (
  33. <Tree
  34. blockNode
  35. checkable
  36. checkStrictly
  37. defaultExpandAll
  38. checkedKeys={checkedKeys}
  39. treeData={generateTree(dataSource, targetKeys)}
  40. onCheck={(_, { node: { key } }) => {
  41. onItemSelect(key, !isChecked(checkedKeys, key));
  42. }}
  43. onSelect={(_, { node: { key } }) => {
  44. onItemSelect(key, !isChecked(checkedKeys, key));
  45. }}
  46. />
  47. );
  48. }
  49. }}
  50. </Transfer>
  51. );
  52. };
  53. const treeData = [
  54. { key: '0-0', title: '0-0' },
  55. {
  56. key: '0-1',
  57. title: '0-1',
  58. children: [
  59. { key: '0-1-0', title: '0-1-0' },
  60. { key: '0-1-1', title: '0-1-1' },
  61. ],
  62. },
  63. { key: '0-2', title: '0-3' },
  64. ];
  65. const App = () => {
  66. const [targetKeys, setTargetKeys] = useState([]);
  67. const onChange = keys => {
  68. setTargetKeys(keys);
  69. };
  70. return <TreeTransfer dataSource={treeData} targetKeys={targetKeys} onChange={onChange} />;
  71. };
  72. ReactDOM.render(<App />, mountNode);

API

Transfer

参数说明类型默认值版本
dataSource数据源,其中的数据将会被渲染到左边一栏中,targetKeys 中指定的除外RecordType extends TransferItem = TransferItem[][]
disabled是否禁用booleanfalse
filterOption接收 inputValue option 两个参数,当 option 符合筛选条件时,应返回 true,反之则返回 false(inputValue, option): boolean-
footer底部渲染函数(props) => ReactNode-
listStyle两个穿梭框的自定义样式object|({direction: left | right}) => object-
locale各种语言{ itemUnit: string; itemsUnit: string; searchPlaceholder: string; notFoundContent: ReactNode; }{ itemUnit: , itemsUnit: , searchPlaceholder: 请输入搜索内容 }
oneWay展示为单向样式booleanfalse4.3.0
operations操作文案集合,顺序从上至下string[][>, <]
pagination使用分页样式,自定义渲染列表下无效boolean | { pageSize: number }false4.3.0
render每行数据渲染函数,该函数的入参为 dataSource 中的项,返回值为 ReactElement。或者返回一个普通对象,其中 label 字段为 ReactElement,value 字段为 title(record) => ReactNode-
selectAllLabels自定义顶部多选框标题的集合(ReactNode | (info: { selectedCount: number, totalCount: number }) => ReactNode)[]-
selectedKeys设置哪些项应该被选中string[][]
showSearch是否显示搜索框booleanfalse
showSelectAll是否展示全选勾选框booleantrue
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)

参考示例

  1. <Transfer {...props}>{listProps => <YourComponent {...listProps} />}</Transfer>

注意

按照 React 的规范,所有的组件数组必须绑定 key。在 Transfer 中,dataSource 里的数据值需要指定 key 值。对于 dataSource 默认将每列数据的 key 属性作为唯一的标识。

如果你的数据没有这个属性,务必使用 rowKey 来指定数据列的主键。

  1. // 比如你的数据主键是 uid
  2. return <Transfer rowKey={record => record.uid} />;

FAQ

怎样让 Transfer 穿梭框列表支持异步数据加载

为了保持页码同步,在勾选时可以不移除选项而以禁用代替:https://codesandbox.io/s/objective-wing-6iqbx