Transfer
Double column transfer choice box.
When To Use#
It is a select control essentially which can be use for selecting multiple items.
Transfer can display more information for items and take up more space.
Transfer the elements between two columns in an intuitive and efficient way.
One or more elements can be selected from either column, one click on the proper direction
button, and the transfer is done. The left column is considered the source
and the right column is considered the target
. As you can see in the API description, these names are reflected in.
Examples
- content1
- content2
- content3
- content4
- content5
- content6
- content7
- content8
- content9
- content10
- content11
- content12
- content13
- content14
- content15
- content16
- content17
- content18
- content19
- content20
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;
- content1
- content2
- content4
- content5
- content7
- content8
- content10
- content11
- content13
- content14
- content16
- content17
- content19
- content20
- content3
- content6
- content9
- content12
- content15
- content18
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;
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;
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;
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;
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;
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 |
Name | |
---|---|
content3 | |
content6 | |
content9 | |
content12 | |
content15 | |
content18 |
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;
}
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;
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;
API#
Property | Description | Type | Default | Version |
---|---|---|---|---|
dataSource | Used for setting the source data. The elements that are part of this array will be present the left column. Except the elements whose keys are included in targetKeys prop | RecordType extends TransferItem = TransferItem[] | [] | |
disabled | Whether disabled transfer | boolean | false | |
filterOption | A function to determine whether an item should show in search result list | (inputValue, option): boolean | - | |
footer | A function used for rendering the footer | (props, { direction }) => ReactNode | - | direction: 4.17.0 |
listStyle | A custom CSS style used for rendering the transfer columns | object | ({direction: left | right }) => object | - | |
locale | The i18n text including filter, empty text, item unit, etc | { itemUnit: string; itemsUnit: string; searchPlaceholder: string; notFoundContent: ReactNode | ReactNode[]; } | { itemUnit: item , itemsUnit: items , notFoundContent: The list is empty , searchPlaceholder: Search here } | |
oneWay | Display as single direction style | boolean | false | 4.3.0 |
operations | A set of operations that are sorted from top to bottom | string[] | [> , < ] | |
operationStyle | A custom CSS style used for rendering the operations column | object | - | |
pagination | Use pagination. Not work in render props | boolean | { pageSize: number, simple: boolean, showSizeChanger?: boolean, showLessItems?: boolean } | false | 4.3.0 |
render | The function to generate the item shown on a column. Based on an record (element of the dataSource array), this function should return a React element which is generated from that record. Also, it can return a plain object with value and label , label is a React element and value is for title | (record) => ReactNode | - | |
selectAllLabels | A set of customized labels for select all checkboxs on the header | (ReactNode | (info: { selectedCount: number, totalCount: number }) => ReactNode)[] | - | |
selectedKeys | A set of keys of selected items | string[] | [] | |
showSearch | If included, a search box is shown on each column | boolean | false | |
showSelectAll | Show select all checkbox on the header | boolean | true | |
status | Set validation status | 'error' | 'warning' | - | 4.19.0 |
targetKeys | A set of keys of elements that are listed on the right column | string[] | [] | |
titles | A set of titles that are sorted from left to right | ReactNode[] | - | |
onChange | A callback function that is executed when the transfer between columns is complete | (targetKeys, direction, moveKeys): void | - | |
onScroll | A callback function which is executed when scroll options list | (direction, event): void | - | |
onSearch | A callback function which is executed when search field are changed | (direction: left | right , value: string): void | - | |
onSelectChange | A callback function which is executed when selected items are changed | (sourceSelectedKeys, targetSelectedKeys): void | - |
Render Props#
Transfer accept children
to customize render list, using follow props:
Property | Description | Type | Version |
---|---|---|---|
direction | List render direction | left | right | |
disabled | Disable list or not | boolean | |
filteredItems | Filtered items | RecordType[] | |
selectedKeys | Selected items | string[] | |
onItemSelect | Select item | (key: string, selected: boolean) | |
onItemSelectAll | Select a group of items | (keys: string[], selected: boolean) |
example#
<Transfer {...props}>{listProps => <YourComponent {...listProps} />}</Transfer>
Warning#
According the standard of React, the key should always be supplied directly to the elements in the array. In Transfer, the keys should be set on the elements included in dataSource
array. By default, key
property is used as an unique identifier.
If there's no key
in your data, you should use rowKey
to specify the key that will be used for uniquely identify each element.
// eg. your primary key is `uid`
return <Transfer rowKey={record => record.uid} />;
FAQ#
How to support fetch and present data from a remote server in Transfer column.#
In order to keep the page number synchronized, you can disable columns you checked without removing the option: https://codesandbox.io/s/objective-wing-6iqbx