You need to enable JavaScript to run this app. 如何在React Hook Form中对接Material UI Transfer List组件取值
优惠活动
大模型
产品
解决方案
定价
更多

如何在React Hook Form中对接Material UI Transfer List组件取值

实现方案

Material UI的Transfer List属于完全受控组件,更适合用react-hook-form的Controller组件来封装,而非register(register更适配原生表单元素)。完整实现步骤如下:

1. 依赖版本确认

确保项目已安装对应版本依赖:

  • react-hook-form 任意稳定版本均可,以下示例以常用的v7版本为例
  • @material-ui/core v4.3.3

2. 封装支持react-hook-form的受控Transfer组件

基于官方Transfer List示例改造,核心是通过Controller的field属性同步值和变更事件,直接接管左右侧数据状态:

import { Controller, useForm } from 'react-hook-form';
import React, { useState } from 'react';
import { List, ListItem, ListItemText, Checkbox, Button, Paper, Grid } from '@material-ui/core';

// 可复用的受控穿梭框组件
const ControlledTransfer = ({ allItems, field }) => {
  // 约定field.value结构为 { left: string[], right: string[] },分别存储左右侧选项值
  const { left = allItems.map(i => i.value), right = [] } = field.value || {};
  const [checked, setChecked] = useState([]);

  // 处理勾选逻辑
  const handleToggle = (value) => {
    const currentIndex = checked.indexOf(value);
    const newChecked = [...checked];
    currentIndex === -1 ? newChecked.push(value) : newChecked.splice(currentIndex, 1);
    setChecked(newChecked);
  };

  // 全部移到右侧
  const handleAllRight = () => {
    field.onChange({ left: [], right: [...right, ...left] });
    setChecked([]);
  };

  // 选中项移到右侧
  const handleCheckedRight = () => {
    const moveItems = left.filter(item => checked.includes(item));
    const newLeft = left.filter(item => !checked.includes(item));
    const newRight = [...right, ...moveItems];
    field.onChange({ left: newLeft, right: newRight });
    setChecked([]);
  };

  // 选中项移到左侧
  const handleCheckedLeft = () => {
    const moveItems = right.filter(item => checked.includes(item));
    const newRight = right.filter(item => !checked.includes(item));
    const newLeft = [...left, ...moveItems];
    field.onChange({ left: newLeft, right: newRight });
    setChecked([]);
  };

  // 全部移到左侧
  const handleAllLeft = () => {
    field.onChange({ left: [...left, ...right], right: [] });
    setChecked([]);
  };

  // 列表渲染公共逻辑
  const customList = (items) => (
    <Paper style={{ height: 230, overflow: 'auto' }}>
      <List dense component="div" role="list">
        {items.map((value) => {
          const label = allItems.find(i => i.value === value)?.label || value;
          const isChecked = checked.includes(value);
          return (
            <ListItem
              key={value}
              role="listitem"
              button
              onClick={() => handleToggle(value)}
            >
              <Checkbox
                checked={isChecked}
                tabIndex={-1}
                disableRipple
              />
              <ListItemText primary={label} />
            </ListItem>
          );
        })}
      </List>
    </Paper>
  );

  return (
    <Grid container spacing={2} justifyContent="center" alignItems="center">
      <Grid item>{customList(left)}</Grid>
      <Grid item>
        <Grid container direction="column" alignItems="center">
          <Button
            variant="outlined"
            size="small"
            onClick={handleAllRight}
            disabled={left.length === 0}
            aria-label="move all right"
            style={{ margin: 1 }}
          >
            ≫
          </Button>
          <Button
            variant="outlined"
            size="small"
            onClick={handleCheckedRight}
            disabled={checked.filter(i => left.includes(i)).length === 0}
            aria-label="move selected right"
            style={{ margin: 1 }}
          >
            >
          </Button>
          <Button
            variant="outlined"
            size="small"
            onClick={handleCheckedLeft}
            disabled={checked.filter(i => right.includes(i)).length === 0}
            aria-label="move selected left"
            style={{ margin: 1 }}
          >
            <
          </Button>
          <Button
            variant="outlined"
            size="small"
            onClick={handleAllLeft}
            disabled={right.length === 0}
            aria-label="move all left"
            style={{ margin: 1 }}
          >
            ≪
          </Button>
        </Grid>
      </Grid>
      <Grid item>{customList(right)}</Grid>
    </Grid>
  );
};

3. 表单内使用

通过Controller引用封装好的组件,提交时可直接拿到左右两侧的字符串数组:

const MyForm = () => {
  const { control, handleSubmit } = useForm({
    defaultValues: {
      // 初始值默认所有选项在左侧
      transferData: { left: [], right: [] }
    }
  });

  const onSubmit = (data) => {
    // data.transferData 直接返回 { left: string[], right: string[] } 结构
    console.log('表单提交数据', data);
    // 仅需要右侧列表时直接取 data.transferData.right 即可
  };

  // 所有可选项配置,label为显示文本,value为提交的字符串值
  const transferOptions = [
    { label: '选项1', value: 'opt1' },
    { label: '选项2', value: 'opt2' },
    { label: '选项3', value: 'opt3' },
    { label: '选项4', value: 'opt4' },
  ];

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <Controller
        name="transferData"
        control={control}
        render={({ field }) => <ControlledTransfer allItems={transferOptions} field={field} />}
      />
      <Button type="submit" variant="contained" color="primary" style={{ marginTop: 20 }}>
        提交表单
      </Button>
    </form>
  );
};

调整说明

  • 如果不需要保留左侧列表的值,仅需要提交右侧选中列表,可以在field.onChange时直接传入right数组,不需要包裹对象结构
  • 若要添加校验规则,直接在Controller上添加rules属性即可,和其他表单元素校验用法完全一致
  • 如果使用的是react-hook-form v6及以下版本,把Controller render回调的field参数改为input对应调整即可,其他逻辑不变

内容的提问来源于stack exchange,提问作者AM-314

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.14 06:36:02