Transfer穿梭框

双栏穿梭选择框。

何时使用

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

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

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

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

代码演示

Transfer穿梭框 - 图1

基本用法

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

  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. <div>
  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. />
  51. <Switch
  52. unCheckedChildren="disabled"
  53. checkedChildren="disabled"
  54. checked={disabled}
  55. onChange={this.handleDisable}
  56. style={{ marginTop: 16 }}
  57. />
  58. </div>
  59. );
  60. }
  61. }
  62. ReactDOM.render(<App />, mountNode);

Transfer穿梭框 - 图2

带搜索框

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

  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穿梭框 - 图3

高级用法

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

  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穿梭框 - 图4

自定义渲染行数据

自定义渲染每一个 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穿梭框 - 图5

表格穿梭框

使用 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. <div>
  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. </div>
  128. );
  129. }
  130. }
  131. ReactDOM.render(<App />, mountNode);

Transfer穿梭框 - 图6

树穿梭框

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

  1. import { Transfer, Tree } from 'antd';
  2. const { TreeNode } = Tree;
  3. // Customize Table Transfer
  4. const isChecked = (selectedKeys, eventKey) => {
  5. return selectedKeys.indexOf(eventKey) !== -1;
  6. };
  7. const generateTree = (treeNodes = [], checkedKeys = []) => {
  8. return treeNodes.map(({ children, ...props }) => (
  9. <TreeNode {...props} disabled={checkedKeys.includes(props.key)} key={props.key}>
  10. {generateTree(children, checkedKeys)}
  11. </TreeNode>
  12. ));
  13. };
  14. const TreeTransfer = ({ dataSource, targetKeys, ...restProps }) => {
  15. const transferDataSource = [];
  16. function flatten(list = []) {
  17. list.forEach(item => {
  18. transferDataSource.push(item);
  19. flatten(item.children);
  20. });
  21. }
  22. flatten(dataSource);
  23. return (
  24. <Transfer
  25. {...restProps}
  26. targetKeys={targetKeys}
  27. dataSource={transferDataSource}
  28. className="tree-transfer"
  29. render={item => item.title}
  30. showSelectAll={false}
  31. >
  32. {({ direction, onItemSelect, selectedKeys }) => {
  33. if (direction === 'left') {
  34. const checkedKeys = [...selectedKeys, ...targetKeys];
  35. return (
  36. <Tree
  37. blockNode
  38. checkable
  39. checkStrictly
  40. defaultExpandAll
  41. checkedKeys={checkedKeys}
  42. onCheck={(
  43. _,
  44. {
  45. node: {
  46. props: { eventKey },
  47. },
  48. },
  49. ) => {
  50. onItemSelect(eventKey, !isChecked(checkedKeys, eventKey));
  51. }}
  52. onSelect={(
  53. _,
  54. {
  55. node: {
  56. props: { eventKey },
  57. },
  58. },
  59. ) => {
  60. onItemSelect(eventKey, !isChecked(checkedKeys, eventKey));
  61. }}
  62. >
  63. {generateTree(dataSource, targetKeys)}
  64. </Tree>
  65. );
  66. }
  67. }}
  68. </Transfer>
  69. );
  70. };
  71. const treeData = [
  72. { key: '0-0', title: '0-0' },
  73. {
  74. key: '0-1',
  75. title: '0-1',
  76. children: [{ key: '0-1-0', title: '0-1-0' }, { key: '0-1-1', title: '0-1-1' }],
  77. },
  78. { key: '0-2', title: '0-3' },
  79. ];
  80. class App extends React.Component {
  81. state = {
  82. targetKeys: [],
  83. };
  84. onChange = targetKeys => {
  85. console.log('Target Keys:', targetKeys);
  86. this.setState({ targetKeys });
  87. };
  88. render() {
  89. const { targetKeys } = this.state;
  90. return (
  91. <div>
  92. <TreeTransfer dataSource={treeData} targetKeys={targetKeys} onChange={this.onChange} />
  93. </div>
  94. );
  95. }
  96. }
  97. ReactDOM.render(<App />, mountNode);

API

Transfer

参数说明类型默认值版本
className自定义类string
dataSource数据源,其中的数据将会被渲染到左边一栏中,targetKeys 中指定的除外。TransferItem[][]
disabled是否禁用booleanfalse3.10.0
filterOption接收 inputValue option 两个参数,当 option 符合筛选条件时,应返回 true,反之则返回 false(inputValue, option): boolean
footer底部渲染函数(props) => ReactNode
lazyTransfer 使用了 react-lazy-load 优化性能,这里可以设置相关参数。设为 false 可以关闭懒加载。object|boolean{ height: 32, offset: 32 }
listStyle两个穿梭框的自定义样式object|({direction: 'left'|'right'}) => object
locale各种语言{ itemUnit: string; itemsUnit: string; searchPlaceholder: string; notFoundContent: ReactNode; }{ itemUnit: '项', itemsUnit: '项', searchPlaceholder: '请输入搜索内容' }3.9.0
operations操作文案集合,顺序从上至下string[]['>', '<']
render每行数据渲染函数,该函数的入参为 dataSource 中的项,返回值为 ReactElement。或者返回一个普通对象,其中 label 字段为 ReactElement,value 字段为 title(record) => ReactNode
selectedKeys设置哪些项应该被选中string[][]
showSearch是否显示搜索框booleanfalse
showSelectAll是否展示全选勾选框booleantrue3.18.0
style容器的自定义样式object3.6.0
targetKeys显示在右侧框数据的 key 集合string[][]
titles标题集合,顺序从左至右ReactNode[]['', '']
onChange选项在两栏之间转移时的回调函数(targetKeys, direction, moveKeys): void
onScroll选项列表滚动时的回调函数(direction, event): void
onSearch搜索框内容时改变时的回调函数(direction: 'left'|'right', value: string): void-3.11.0
onSelectChange选中项发生改变时的回调函数(sourceSelectedKeys, targetSelectedKeys): void

Render Props

3.18.0 新增。Transfer 支持接收 children 自定义渲染列表,并返回以下参数:

参数说明类型版本
direction渲染列表的方向'left' | 'right'3.18.0
disabled是否禁用列表boolean3.18.0
filteredItems过滤后的数据TransferItem[]3.18.0
onItemSelect勾选条目(key: string, selected: boolean)3.18.0
onItemSelectAll勾选一组条目(keys: string[], selected: boolean)3.18.0
selectedKeys选中的条目string[]3.18.0

参考示例

  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/93xeb