트랜스퍼

트랜스퍼 (Transfer)

두 컬럼 사이에서 항목을 직관적이고 효율적으로 이동시킬 수 있는 전송 컴포넌트입니다. 여러 항목을 선택하는 데 쓰여요.

출처: 문서

본문

언제 사용하나요 (When To Use)

  • 본질적으로 여러 항목을 선택하는 데 사용할 수 있는 셀렉트 컨트롤입니다.
  • Transfer는 항목에 대한 더 많은 정보를 표시할 수 있고 더 많은 공간을 차지합니다.

두 컬럼 사이에서 요소를 직관적이고 효율적으로 이동시킵니다.

어느 한 컬럼에서도 하나 이상의 요소를 선택할 수 있으며, 적절한 direction 버튼을 한 번 클릭하면 전송이 완료됩니다. 왼쪽 컬럼은 source, 오른쪽 컬럼은 target으로 간주됩니다. 이 이름들이 API에 반영되어 있는 것을 볼 수 있습니다.

참고: Transfer는 제어(controlled) 컴포넌트이며 비제어 모드는 지원하지 않습니다.

예제 (Examples)

기본 (Basic)

Transfer의 가장 기본적인 사용법은 소스 데이터와 타겟 키 배열, 렌더링, 그리고 일부 콜백 함수를 제공하는 것입니다.

import React, { useState } from 'react';
import { Transfer } from 'antd';
import type { TransferProps } from 'antd';

interface RecordType {
  key: string;
  title: string;
  description: string;
}

const mockData = Array.from({ length: 20 }).map<RecordType>((_, 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<TransferProps['targetKeys']>(initialTargetKeys);
  const [selectedKeys, setSelectedKeys] = useState<TransferProps['targetKeys']>([]);

  const onChange: TransferProps['onChange'] = (nextTargetKeys, direction, moveKeys) => {
    console.log('targetKeys:', nextTargetKeys);
    console.log('direction:', direction);
    console.log('moveKeys:', moveKeys);
    setTargetKeys(nextTargetKeys);
  };

  const onSelectChange: TransferProps['onSelectChange'] = (
    sourceSelectedKeys,
    targetSelectedKeys,
  ) => {
    console.log('sourceSelectedKeys:', sourceSelectedKeys);
    console.log('targetSelectedKeys:', targetSelectedKeys);
    setSelectedKeys([...sourceSelectedKeys, ...targetSelectedKeys]);
  };

  const onScroll: TransferProps['onScroll'] = (direction, e) => {
    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;

단방향 (One Way)

oneWay를 사용해 Transfer를 단방향 스타일로 만듭니다.

import React, { useState } from 'react';
import { Switch, Transfer } from 'antd';
import type { TransferProps } from 'antd';

interface RecordType {
  key: string;
  title: string;
  description: string;
  disabled: boolean;
}

const mockData = Array.from({ length: 20 }).map<RecordType>((_, 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<React.Key[]>(oriTargetKeys);
  const [selectedKeys, setSelectedKeys] = useState<React.Key[]>([]);
  const [disabled, setDisabled] = useState(false);

  const handleChange: TransferProps['onChange'] = (newTargetKeys, direction, moveKeys) => {
    setTargetKeys(newTargetKeys);

    console.log('targetKeys: ', newTargetKeys);
    console.log('direction: ', direction);
    console.log('moveKeys: ', moveKeys);
  };

  const handleSelectChange: TransferProps['onSelectChange'] = (
    sourceSelectedKeys,
    targetSelectedKeys,
  ) => {
    setSelectedKeys([...sourceSelectedKeys, ...targetSelectedKeys]);

    console.log('sourceSelectedKeys: ', sourceSelectedKeys);
    console.log('targetSelectedKeys: ', targetSelectedKeys);
  };

  const handleScroll: TransferProps['onScroll'] = (direction, e) => {
    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;

검색 상자가 있는 Transfer입니다.

import React, { useEffect, useState } from 'react';
import { Transfer } from 'antd';
import type { TransferProps } from 'antd';

interface RecordType {
  key: string;
  title: string;
  description: string;
  chosen: boolean;
}

const App: React.FC = () => {
  const [mockData, setMockData] = useState<RecordType[]>([]);
  const [targetKeys, setTargetKeys] = useState<TransferProps['targetKeys']>([]);

  const getMock = () => {
    const tempTargetKeys: React.Key[] = [];
    const tempMockData: RecordType[] = [];
    for (let i = 0; i < 20; i++) {
      const data = {
        key: i.toString(),
        title: `content${i + 1}`,
        description: `description of content${i + 1}`,
        chosen: i % 2 === 0,
      };
      if (data.chosen) {
        tempTargetKeys.push(data.key);
      }
      tempMockData.push(data);
    }
    setMockData(tempMockData);
    setTargetKeys(tempTargetKeys);
  };

  useEffect(() => {
    getMock();
  }, []);

  const filterOption = (inputValue: string, option: RecordType) =>
    option.description.includes(inputValue);

  const handleChange: TransferProps['onChange'] = (newTargetKeys) => {
    setTargetKeys(newTargetKeys);
  };

  const handleSearch: TransferProps['onSearch'] = (dir, value) => {
    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;

고급 (Advanced)

Transfer의 고급 사용법입니다.

전송 버튼의 라벨, 컬럼의 너비와 높이, 푸터에 표시할 내용을 커스터마이즈할 수 있습니다.

import React, { useEffect, useState } from 'react';
import { Button, Transfer } from 'antd';
import type { TransferProps } from 'antd';

interface RecordType {
  key: string;
  title: string;
  description: string;
  chosen: boolean;
}

const App: React.FC = () => {
  const [mockData, setMockData] = useState<RecordType[]>([]);
  const [targetKeys, setTargetKeys] = useState<TransferProps['targetKeys']>([]);

  const getMock = () => {
    const tempTargetKeys: React.Key[] = [];
    const tempMockData: RecordType[] = [];
    for (let i = 0; i < 20; i++) {
      const data = {
        key: i.toString(),
        title: `content${i + 1}`,
        description: `description of content${i + 1}`,
        chosen: i % 2 === 0,
      };
      if (data.chosen) {
        tempTargetKeys.push(data.key);
      }
      tempMockData.push(data);
    }
    setMockData(tempMockData);
    setTargetKeys(tempTargetKeys);
  };

  useEffect(() => {
    getMock();
  }, []);

  const handleChange: TransferProps['onChange'] = (newTargetKeys) => {
    setTargetKeys(newTargetKeys);
  };

  const renderFooter: TransferProps['footer'] = (_, info) => {
    if (info?.direction === 'left') {
      return (
        <Button
          size="small"
          style={{ display: 'flex', margin: 8, marginInlineEnd: 'auto' }}
          onClick={getMock}
        >
          Left button reload
        </Button>
      );
    }
    return (
      <Button
        size="small"
        style={{ display: 'flex', margin: 8, marginInlineStart: 'auto' }}
        onClick={getMock}
      >
        Right button reload
      </Button>
    );
  };

  return (
    <Transfer
      dataSource={mockData}
      showSearch
      styles={{
        section: {
          width: 250,
          height: 300,
        },
      }}
      actions={['to right', 'to left']}
      targetKeys={targetKeys}
      onChange={handleChange}
      render={(item) => `${item.title}-${item.description}`}
      footer={renderFooter}
    />
  );
};

export default App;

커스텀 데이터소스 (Custom datasource)

각 Transfer 항목을 커스터마이즈하여 복잡한 데이터소스를 렌더링할 수 있습니다.

import React, { useEffect, useState } from 'react';
import { Transfer } from 'antd';
import type { TransferProps } from 'antd';

interface RecordType {
  key: string;
  title: string;
  description: string;
  chosen: boolean;
}

const App: React.FC = () => {
  const [mockData, setMockData] = useState<RecordType[]>([]);
  const [targetKeys, setTargetKeys] = useState<React.Key[]>([]);

  const getMock = () => {
    const tempTargetKeys: React.Key[] = [];
    const tempMockData: RecordType[] = [];
    for (let i = 0; i < 20; i++) {
      const data = {
        key: i.toString(),
        title: `content${i + 1}`,
        description: `description of content${i + 1}`,
        chosen: i % 2 === 0,
      };
      if (data.chosen) {
        tempTargetKeys.push(data.key);
      }
      tempMockData.push(data);
    }
    setMockData(tempMockData);
    setTargetKeys(tempTargetKeys);
  };

  useEffect(() => {
    getMock();
  }, []);

  const handleChange: TransferProps['onChange'] = (newTargetKeys, direction, moveKeys) => {
    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}
      styles={{
        section: {
          width: 300,
          height: 300,
        },
      }}
      targetKeys={targetKeys}
      onChange={handleChange}
      render={renderItem}
    />
  );
};

export default App;

커스텀 액션 (Custom Actions)

actions prop으로 작업을 커스터마이즈할 수 있습니다. 이 예제는 비활성화 및 로딩 상태 처리 등을 포함해 액션을 커스터마이즈하는 방법을 보여줍니다.

actions가 문자열 배열이면 기본 Button 컴포넌트가 사용되고 문자열이 버튼 텍스트로 설정됩니다.

actions가 React 요소 배열이면 이 요소들을 액션 버튼으로 직접 사용하여, 예제처럼 로딩 상태가 있는 버튼 같은 커스텀 버튼 컴포넌트를 사용할 수 있습니다.

참고:

  1. 커스텀 버튼을 사용할 때 Transfer 컴포넌트가 버튼의 비활성화 상태와 클릭 이벤트를 자동으로 처리합니다.
  2. 커스텀 버튼에 disabled 속성을 추가해 비활성화 상태를 제어할 수 있습니다.
  3. 커스텀 버튼에 onClick 이벤트 핸들러를 추가할 수 있으며, Transfer 컴포넌트의 내부 핸들러와 병합됩니다.
import React, { useState } from 'react';
import { Button, message, Transfer } from 'antd';
import { DoubleLeftOutlined, DoubleRightOutlined } from '@ant-design/icons';
import type { TransferProps } from 'antd';

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 ${i + 1}`,
}));

const initialTargetKeys = mockData.filter((item) => Number(item.key) > 10).map((item) => item.key);

const App: React.FC = () => {
  const [messageApi, contextHolder] = message.useMessage();
  const [targetKeys, setTargetKeys] = useState<string[]>(initialTargetKeys);
  const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
  const [loadingRight, setLoadingRight] = useState<boolean>(false);
  const [loadingLeft, setLoadingLeft] = useState<boolean>(false);

  // Handle data transfer
  const handleChange: TransferProps['onChange'] = (newTargetKeys, direction, moveKeys) => {
    setTargetKeys(newTargetKeys as string[]);

    // Simulate async action
    if (direction === 'right') {
      setLoadingRight(true);
      setTimeout(() => {
        setLoadingRight(false);
        messageApi.success(`Successfully added ${moveKeys.length} items to the right`);
      }, 1000);
    } else {
      setLoadingLeft(true);
      setTimeout(() => {
        setLoadingLeft(false);
        messageApi.success(`Successfully added ${moveKeys.length} items to the left`);
      }, 1000);
    }
  };

  // Handle selection change
  const handleSelectChange: TransferProps['onSelectChange'] = (
    sourceSelectedKeys,
    targetSelectedKeys,
  ) => {
    setSelectedKeys([...sourceSelectedKeys, ...targetSelectedKeys] as string[]);
  };

  // Right button is disabled (no selected items on the left or all selected items are already in the right list)
  const rightButtonDisabled =
    selectedKeys.length === 0 || selectedKeys.every((key) => targetKeys.includes(key));

  // Left button is disabled (no selected items on the right)
  const leftButtonDisabled =
    selectedKeys.length === 0 || selectedKeys.every((key) => !targetKeys.includes(key));

  // Custom right button click handler
  const handleRightButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
    // You can add custom logic here, such as showing a confirmation dialog
    console.log('Right button clicked', event);
    // The Transfer component will automatically handle data transfer
  };

  // Custom left button click handler
  const handleLeftButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
    // You can add custom logic here, such as showing a confirmation dialog
    console.log('Left button clicked', event);
    // The Transfer component will automatically handle data transfer
  };

  return (
    <>
      {contextHolder}
      <Transfer
        dataSource={mockData}
        targetKeys={targetKeys}
        selectedKeys={selectedKeys}
        onChange={handleChange}
        onSelectChange={handleSelectChange}
        render={(item) => item.title}
        actions={[
          // Custom right button (transfer data to the right)
          <Button
            key="to-right"
            type="primary"
            icon={<DoubleRightOutlined />}
            loading={loadingRight}
            disabled={rightButtonDisabled}
            onClick={handleRightButtonClick}
          >
            Move To Right
          </Button>,
          // Custom left button (transfer data to the left)
          <Button
            key="to-left"
            type="primary"
            icon={<DoubleLeftOutlined />}
            loading={loadingLeft}
            disabled={leftButtonDisabled}
            onClick={handleLeftButtonClick}
          >
            Move To Left
          </Button>,
        ]}
      />
    </>
  );
};

export default App;

페이징 (Pagination)

페이징으로 많은 양의 항목을 저장합니다.

import React, { useEffect, useState } from 'react';
import { Switch, Transfer } from 'antd';
import type { TransferProps } from 'antd';

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<React.Key[]>([]);

  useEffect(() => {
    const newTargetKeys: React.Key[] = [];
    const newMockData: RecordType[] = [];
    for (let i = 0; i < 2000; i++) {
      const data = {
        key: i.toString(),
        title: `content${i + 1}`,
        description: `description of content${i + 1}`,
        chosen: i % 2 === 0,
      };
      if (data.chosen) {
        newTargetKeys.push(data.key);
      }
      newMockData.push(data);
    }

    setTargetKeys(newTargetKeys);
    setMockData(newMockData);
  }, []);

  const onChange: TransferProps['onChange'] = (newTargetKeys, direction, moveKeys) => {
    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;

테이블 전송 (Table Transfer)

Table 컴포넌트로 렌더 리스트를 커스터마이즈합니다.

import React, { useState } from 'react';
import { Flex, Switch, Table, Tag, Transfer } from 'antd';
import type { GetProp, TableColumnsType, TableProps, TransferProps } from 'antd';

type TransferItem = GetProp<TransferProps, 'dataSource'>[number];

type TableRowSelection<T extends object> = TableProps<T>['rowSelection'];

interface DataType {
  key: string;
  title: string;
  description: string;
  tag: string;
  disabled?: boolean;
}

interface TableTransferProps extends TransferProps<TransferItem> {
  dataSource: DataType[];
  leftColumns: TableColumnsType<DataType>;
  rightColumns: TableColumnsType<DataType>;
}

// Customize Table Transfer
const TableTransfer: React.FC<TableTransferProps> = (props) => {
  const { leftColumns, rightColumns, ...restProps } = props;
  return (
    <Transfer style={{ width: '100%' }} {...restProps}>
      {({
        direction,
        filteredItems,
        onItemSelect,
        onItemSelectAll,
        selectedKeys: listSelectedKeys,
        disabled: listDisabled,
      }) => {
        const columns = direction === 'left' ? leftColumns : rightColumns;
        const rowSelection: TableRowSelection<TransferItem> = {
          getCheckboxProps: () => ({ disabled: listDisabled }),
          onChange(selectedRowKeys) {
            onItemSelectAll(selectedRowKeys, 'replace');
          },
          selectedRowKeys: listSelectedKeys,
          selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT, Table.SELECTION_NONE],
        };

        return (
          <Table<DataType>
            rowSelection={rowSelection}
            columns={columns}
            dataSource={filteredItems}
            size="small"
            style={{ pointerEvents: listDisabled ? 'none' : undefined }}
            onRow={({ key, disabled: itemDisabled }) => ({
              onClick: () => {
                if (itemDisabled || listDisabled) {
                  return;
                }
                onItemSelect(key, !listSelectedKeys.includes(key));
              },
            })}
          />
        );
      }}
    </Transfer>
  );
};

const mockTags = ['cat', 'dog', 'bird'];

const mockData = Array.from({ length: 20 }).map<DataType>((_, i) => ({
  key: i.toString(),
  title: `content${i + 1}`,
  description: `description of content${i + 1}`,
  tag: mockTags[i % 3],
}));

const columns: TableColumnsType<DataType> = [
  {
    dataIndex: 'title',
    title: 'Name',
  },
  {
    dataIndex: 'tag',
    title: 'Tag',
    render: (tag: string) => (
      <Tag style={{ marginInlineEnd: 0 }} color="cyan">
        {tag.toUpperCase()}
      </Tag>
    ),
  },
  {
    dataIndex: 'description',
    title: 'Description',
  },
];

const filterOption = (input: string, item: DataType) =>
  item.title?.includes(input) || item.tag?.includes(input);

const App: React.FC = () => {
  const [targetKeys, setTargetKeys] = useState<TransferProps['targetKeys']>([]);
  const [disabled, setDisabled] = useState(false);

  const onChange: TableTransferProps['onChange'] = (nextTargetKeys) => {
    setTargetKeys(nextTargetKeys);
  };

  const toggleDisabled = (checked: boolean) => {
    setDisabled(checked);
  };

  return (
    <Flex align="start" gap="medium" vertical>
      <TableTransfer
        dataSource={mockData}
        targetKeys={targetKeys}
        disabled={disabled}
        showSearch
        showSelectAll={false}
        onChange={onChange}
        filterOption={filterOption}
        leftColumns={columns}
        rightColumns={columns}
      />
      <Switch
        unCheckedChildren="disabled"
        checkedChildren="disabled"
        checked={disabled}
        onChange={toggleDisabled}
      />
    </Flex>
  );
};

export default App;

트리 전송 (Tree Transfer)

Tree 컴포넌트로 렌더 리스트를 커스터마이즈합니다.

import React, { useState } from 'react';
import { theme, Transfer, Tree } from 'antd';
import type { GetProp, TransferProps, TreeDataNode } from 'antd';

type TransferItem = GetProp<TransferProps, 'dataSource'>[number];

interface TreeTransferProps {
  dataSource: TreeDataNode[];
  targetKeys: TransferProps['targetKeys'];
  onChange: TransferProps['onChange'];
}

// Customize Table Transfer
const isChecked = (selectedKeys: React.Key[], eventKey: React.Key) =>
  selectedKeys.includes(eventKey);

const generateTree = (
  treeNodes: TreeDataNode[] = [],
  checkedKeys: TreeTransferProps['targetKeys'] = [],
): TreeDataNode[] =>
  treeNodes.map(({ children, ...props }) => ({
    ...props,
    disabled: checkedKeys.includes(props.key as string),
    children: generateTree(children, checkedKeys),
  }));

const TreeTransfer: React.FC<TreeTransferProps> = (props) => {
  const { token } = theme.useToken();

  const { dataSource, targetKeys = [], ...restProps } = props;

  const transferDataSource: TransferItem[] = [];
  function flatten(list: TreeDataNode[] = []) {
    list.forEach((item) => {
      transferDataSource.push(item as TransferItem);
      flatten(item.children);
    });
  }
  flatten(dataSource);

  return (
    <Transfer
      {...restProps}
      targetKeys={targetKeys}
      dataSource={transferDataSource}
      render={(item) => item.title}
      showSelectAll={false}
    >
      {({ direction, onItemSelect, selectedKeys }) => {
        if (direction === 'left') {
          const checkedKeys = [...selectedKeys, ...targetKeys];
          return (
            <div style={{ padding: token.paddingXS }}>
              <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));
                }}
              />
            </div>
          );
        }
      }}
    </Transfer>
  );
};

const treeData: TreeDataNode[] = [
  { 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-2' },
  { key: '0-3', title: '0-3' },
  { key: '0-4', title: '0-4' },
];

const App: React.FC = () => {
  const [targetKeys, setTargetKeys] = useState<TreeTransferProps['targetKeys']>([]);
  const onChange: TreeTransferProps['onChange'] = (keys) => {
    setTargetKeys(keys);
  };
  return <TreeTransfer dataSource={treeData} targetKeys={targetKeys} onChange={onChange} />;
};

export default App;

상태 (Status)

status로 Transfer에 상태를 추가합니다. error 또는 warning이 될 수 있습니다.

import React from 'react';
import { Flex, Transfer } from 'antd';

const App: React.FC = () => (
  <Flex gap="medium" vertical>
    <Transfer status="error" />
    <Transfer status="warning" showSearch />
  </Flex>
);

export default App;

커스텀 시맨틱 DOM 스타일링 (Custom semantic dom styling)

classNames와 styles에 객체 또는 함수를 넘겨서 Transfer의 시맨틱 DOM 스타일을 커스터마이즈할 수 있습니다.

import React from 'react';
import { Flex, Transfer } from 'antd';
import type { GetProp, TransferProps } from 'antd';
import { createStyles } from 'antd-style';

const useStyles = createStyles(({ token, css }) => ({
  section: { backgroundColor: 'rgba(250,250,250, 0.5)' },
  header: { color: token.colorPrimary },
  actions: css`
    & button {
      background-color: rgba(255, 242, 232, 0.6);
    }
  `,
}));

const mockData = Array.from({ length: 20 }).map<any>((_, 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 stylesObject: TransferProps['styles'] = {
  header: { fontWeight: 'bold' },
};

const stylesFn: TransferProps['styles'] = (info): GetProp<TransferProps, 'styles', 'Return'> => {
  if (info.props.status === 'warning') {
    return {
      section: { backgroundColor: 'rgba(246,255,237, 0.6)', borderColor: '#b7eb8f' },
      header: { color: '#8DBCC7', fontWeight: 'normal' },
    };
  }
  return {};
};

const App: React.FC = () => {
  const { styles: classNames } = useStyles();
  const sharedProps: TransferProps = {
    dataSource: mockData,
    targetKeys: initialTargetKeys,
    render: (item) => item.title,
    classNames,
  };
  return (
    <Flex vertical gap="large" style={{ width: '100%' }}>
      <Transfer {...sharedProps} status="error" styles={stylesObject} />
      <Transfer {...sharedProps} status="warning" styles={stylesFn} />
    </Flex>
  );
};

export default App;

API

Common props ref:Common props

Transfer

Property Description Type Default Version Global Config
actions A set of operations that are sorted from top to bottom. When an array of strings is provided, default buttons will be used; when an array of ReactNode is provided, custom elements will be used ReactNode[] [>, <] 6.0.0 ×
classNames Customize class for each semantic structure inside the component. Supports object or function. Record<SemanticDOM, string> | (info: { props })=> Record<SemanticDOM, string> - 6.0.0 6.0.0
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 ×
selectionsIcon custom dropdown icon React.ReactNode 5.8.0 5.14.0
filterOption A function to determine whether an item should show in search result list, only works when searching, (add direction support since 5.9.0+) (inputValue, option, direction: left | right): 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. Use styles.section instead 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: No data, 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. Use actions instead. string[] [>, <] ×
operationStyle A custom CSS style used for rendering the operations column. Use styles.actions instead. 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 checkboxes on the header (ReactNode | (info: { selectedCount: number, totalCount: number }) => ReactNode)[] - ×
selectedKeys A set of keys of selected items string[] | number[] [] ×
showSearch If included, a search box is shown on each column boolean | { placeholder:string,defaultValue:string } false ×
showSelectAll Show select all checkbox on the header boolean true ×
status Set validation status 'error' | 'warning' - 4.19.0 ×
styles Customize inline style for each semantic structure inside the component. Supports object or function. Record<SemanticDOM, CSSProperties> | (info: { props })=> Record<SemanticDOM, CSSProperties> - 6.0.0 6.0.0
targetKeys A set of keys of elements that are listed on the right column string[] | number[] [] ×
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 - ×

렌더 props (Render Props)

Transfer는 children을 받아 렌더 리스트를 커스터마이즈하며, 다음 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[] | number[]
onItemSelect Select item (key: string | number, selected: boolean)
onItemSelectAll Select a group of items (keys: string[] | number[], selected: boolean)

예제 (example)

<Transfer {...props}>{(listProps) => <YourComponent {...listProps} />}</Transfer>

경고 (Warning)

React의 표준에 따라 key는 항상 배열의 요소에 직접 제공되어야 합니다. Transfer에서는 dataSource 배열에 포함된 요소에 key가 설정되어야 합니다. 기본적으로 key 속성이 고유 식별자로 사용됩니다.

데이터에 key가 없다면 rowKey를 사용해 각 요소를 고유하게 식별하는 key를 지정해야 합니다.

// eg. your primary key is `uid`
return <Transfer rowKey={(record) => record.uid} />;

시맨틱 DOM (Semantic DOM)

https://ant.design/components/transfer/semantic.md

디자인 토큰 (Design Token)

컴포넌트 토큰 (Component Token - Transfer)

Token Name Description Type Default Value
headerHeight Height of header string | number 40
itemHeight Height of list item string | number 32
itemPaddingBlock Vertical padding of list item string | number 5
listHeight Height of list string | number 200
listWidth Width of list string | number 180
listWidthLG Width of large list string | number 250

글로벌 토큰 (Global Token)

Token Name Description Type Default Value
borderRadiusLG LG size border radius, used in some large border radius components, such as Card, Modal and other components. number
colorBgContainer Container background color, e.g: default button, input box, etc. Be sure not to confuse this with colorBgElevated. string
colorBgContainerDisabled Control the background color of container in disabled state. string
colorBorder Default border color, used to separate different elements, such as: form separator, card separator, etc. string
colorError Used to represent the visual elements of the operation failure, such as the error Button, error Result component, etc. string
colorLink Control the color of hyperlink. string
colorLinkActive Control the color of hyperlink when clicked. string
colorLinkHover Control the color of hyperlink when hovering. string
colorPrimaryBorder The stroke color under the main color gradient, used on the stroke of components such as Slider. string
colorSplit Used as the color of separator, this color is the same as colorBorderSecondary but with transparency. string
colorText Default text color which comply with W3C standards, and this color is also the darkest neutral color. string
colorTextDisabled Control the color of text in disabled state. string
colorTextSecondary The second level of text color is generally used in scenarios where text color is not emphasized, such as label text, menu text selection state, etc. string
colorWarning Used to represent the warning map token, such as Notification, Alert, etc. Alert or Control component(like Input) will use these map tokens. string
controlHeightLG LG component height number
controlItemBgActive Control the background color of control component item when active. string
controlItemBgActiveHover Control the background color of control component item when hovering and active. string
controlItemBgHover Control the background color of control component item when hovering. string
fontFamily The font family of Ant Design prioritizes the default interface font of the system, and provides a set of alternative font libraries that are suitable for screen display to maintain the readability and readability of the font under different platforms and browsers, reflecting the friendly, stable and professional characteristics. string
fontSize The most widely used font size in the design system, from which the text gradient will be derived. number
fontSizeIcon Control the font size of operation icon in Select, Cascader, etc. Normally same as fontSizeSM. number
lineHeight Line height of text. number
lineType Border style of base components string
lineWidth Border width of base components number
lineWidthFocus Control the width of the line when the component is in focus state. number
linkDecoration Control the text decoration style of a link. TextDecoration<string | number> | undefined
linkFocusDecoration Control the text decoration style of a link on focus. TextDecoration<string | number> | undefined
linkHoverDecoration Control the text decoration style of a link on mouse hover. TextDecoration<string | number> | undefined
marginXS Control the margin of an element, with a small size. number
marginXXS Control the margin of an element, with the smallest size. number
motionDurationSlow Motion speed, slow speed. Used for large element animation interaction. string
paddingSM Control the small padding of the element. number
paddingXS Control the extra small padding of the element. number

FAQ

Transfer 컬럼에서 원격 서버의 데이터를 가져와 표시하는 방법이 궁금합니다. {#faq-async-data-loading}

페이지 번호를 동기화하려면 옵션을 제거하지 않고 체크한 컬럼을 비활성화하면 됩니다: https://codesandbox.io/s/objective-wing-6iqbx

더 알아보기 (Learn more)