使用 DualListInput 编辑数组与多对多关系:react-admin 双列穿梭选择器完整指南
2026/9/20 19:34:14 网站建设 项目流程

使用 DualListInput 编辑数组与多对多关系:react-admin 双列穿梭选择器完整指南

【免费下载链接】react-adminA frontend Framework for single-page applications on top of REST/GraphQL APIs, using TypeScript, React and Material Design项目地址: https://gitcode.com/gh_mirrors/re/react-admin

<DualListInput>是 react-admin 企业版(Enterprise Edition,来自@react-admin/ra-relationships包)提供的双列穿梭选择组件,用户通过在"可选列表"与"已选列表"之间移动条目,即可编辑标量数组值(如['u001', 'u003'])、一对多或多对多关系。读完本文,你将掌握其全部 props 的用法、choices数据的各种构造方式、如何嵌套在<ReferenceArrayInput>中编辑外键数组,以及如何用sx与全局主题覆盖进行样式定制。

DualListInput 是什么:双列穿梭选择的定位

<DualListInput>通过并排的两个列表让用户完成选择:左侧列出所有可选条目,右侧列出当前已选条目,中间的按钮负责在两个列表之间移动条目。它没有下拉菜单、没有输入框联想,交互直观,尤其适合可选数量有限、但希望一眼看到全部选项及当前选中状态的场景。

组件编辑的是"数组值",包括:

  • 标量数组,如[123, 456]
  • 一对多关系(一个资源记录引用另一资源的多个 id);
  • 多对多关系(通过中间表关联的两组记录)。

该组件属于 react-admin 企业版组件,需要安装独立的商业许可包@react-admin/ra-relationships后按如下方式引入:

import { DualListInput } from "@react-admin/ra-relationships";

从 文档索引 可以看到,<DualListInput>在官方文档导航中带有企业版专属标记(premium 徽标),这也解释了为何它不出现在开箱即用的react-admin主包中,而需要单独授权。

与其他数组编辑组件的取舍

react-admin 提供了多套编辑数组值的输入组件,Inputs.md 数据类型表 将 "Array of Enums"(如['foo', 'bar'])这一数据类型明确列出了五个可选组件:

组件交互形态适用场景
<TextArrayInput>芯片 + 自由输入数组值不可穷举,如邮箱列表(见 TextArrayInput 文档)
<AutocompleteArrayInput>自动补全下拉可选值很多,需要输入搜索
<SelectArrayInput>下拉多选可选值有限且需要节省表单空间
<CheckboxGroupInput>复选框组可选值很少,希望所有选项平铺展示
<DualListInput>双列穿梭需要同时看到全部可选值与已选值

当你编辑的数组是另一资源的 id 集合(即外键数组)时,还应配合<ReferenceArrayInput>使用(详见后文 在 ReferenceArrayInput 中使用)。总的原则是:<DualListInput>适合"可选条目数量适中、用户需要对比和确认"的场景;如果参考资源条目非常多,它因为没有自动补全能力而不太合适(原文档明确提示了这一点)。

快速上手:最简用法

<DualListInput>在使用上比普通输入组件多一个必需概念:除了source之外,还需要一个choices属性来声明所有可能的取值。

import { Create, SimpleForm } from 'react-admin'; import { DualListInput } from "@react-admin/ra-relationships"; const UserCreate = () => ( <Create> <SimpleForm> <DualListInput source="roles" choices={[ { id: 'admin', name: 'Admin' }, { id: 'u001', name: 'Editor' }, { id: 'u002', name: 'Moderator' }, { id: 'u003', name: 'Reviewer' }, ]} /> </SimpleForm> </Create> );

默认情况下,可选项按如下规则由choices构建:

  • id字段作为选项的值(写入表单);
  • name字段作为选项显示的文本。

source对应的表单值必须是已选值的数组,例如:

{ id: 123, name: 'John Doe', roles: ['u001', 'u003'], }

Props 总览

原文档给出了完整属性表,整理如下(所有类型与默认值均以原文档为准):

PropRequiredTypeDefaultDescription
choicesOptionalObject[]-要作为选项展示的条目列表。除非位于<ReferenceArrayInput>内部,否则必需。
addButtonOptional'outlined' | 'contained' | 'text' |element-添加按钮的 Material UIvariant值,或一个用于完全替换它的 React 元素
addButtonLabelOptionalstringra-relationships.duallistinput.select添加按钮的文本或翻译 key
availableItemsLabelOptionalstringra-relationships.duallistinput.availableItems可选列表标题的文本或翻译 key
denseOptionalbooleanfalse列表组件的视觉密度
disableValueOptionalstring'disabled'用于在choices中标记"禁用选项"的自定义字段名
optionTextOptionalstring|Functionname用于显示选项文本的记录字段名,或接收当前记录并返回字符串的函数record => {string}
optionValueOptionalstringid记录中用于作为输入值的字段名
removeButtonOptional'outlined' | 'contained' | 'text' |element-移除按钮的 Material UIvariant值,或一个用于完全替换它的 React 元素
removeButtonLabelOptionalstringra-relationships.duallistinput.unselect移除按钮的文本或翻译 key
selectedItemsLabelOptionalstringra-relationships.duallistinput.selectedItems已选列表标题的文本或翻译 key
translateChoiceOptionalbooleantrue选项文本是否应被翻译

此外,<DualListInput>还接受所有通用输入属性(common input props),包括sourcelabeldefaultValuedisabledreadOnlyfullWidthhelperTextformatparsevalidateclassNamesx

按钮与列表标题的本地化

由于组件属于企业版包,其内置文案通过翻译 key 提供,默认值分别是:

  • 添加按钮:ra-relationships.duallistinput.select("select" 语义,即加入已选列表);
  • 移除按钮:ra-relationships.duallistinput.unselect
  • 可选列表标题:ra-relationships.duallistinput.availableItems
  • 已选列表标题:ra-relationships.duallistinput.selectedItems

如果你使用 polyglot 翻译体系,可在应用的 messages 中覆盖这些 key;也可以直接传字符串覆盖:

<DualListInput source="roles" choices={choices} addButtonLabel="加入" removeButtonLabel="移除" availableItemsLabel="可用角色" selectedItemsLabel="已分配角色" />

按钮外观默认使用 Material UI 的某种 variant,可通过addButton/removeButton传入'outlined''contained''text'之一,或传入自定义 React 元素完全替换。

choices:构造可选列表

choices必须是对象数组——每个对象对应一个可选条目,其中id是值,name是展示给用户的标签:

<DualListInput source="roles" choices={[ { id: 'admin', name: 'Admin' }, { id: 'u001', name: 'Editor' }, { id: 'u002', name: 'Moderator' }, { id: 'u003', name: 'Reviewer' }, ]} />

用 disabled 字段禁用部分选项

在某条选项上设置disabled字段即可将其渲染为禁用状态(用户无法移动它):

<DualListInput source="roles" choices={[ { _id: 'admin', label: 'Admin', disabled: true }, { _id: 'u001', label: 'Editor' }, { _id: 'u002', label: 'Moderator' }, { _id: 'u003', label: 'Reviewer' }, ]} />

注意,禁用字段名默认是disabled,想改用其他字段(如not_available)需配合disableValue属性,详见后文。

自定义标签字段与值字段

如果选项对象的值字段、标签字段不叫id/name,可以通过optionTextoptionValue指定:

<DualListInput source="roles" choices={[ { _id: 'admin', label: 'Admin' }, { _id: 'u001', label: 'Editor' }, { _id: 'u002', label: 'Moderator' }, { _id: 'u003', label: 'Reviewer' }, ]} optionValue="_id" optionText="label" />

使用翻译标识符作为选项文本

choices默认会经过翻译函数处理,因此可以直接把翻译 key 当作name

const choices = [ { id: 'admin', name: 'myroot.roles.admin' }, { id: 'u001', name: 'myroot.roles.u001' }, { id: 'u002', name: 'myroot.roles.u002' }, { id: 'u003', name: 'myroot.roles.u003' }, ];

这样当用户切换到其他语言时,选项文本会自动跟随翻译。如果不想翻译选项文本,设置translateChoice={false}即可。

由纯字符串数组构造 choices

如果你拿到的是一组字符串值,需要先映射成带id/name的对象数组:

const possibleValues = ['programming', 'lifestyle', 'photography']; const ucfirst = name => name.charAt(0).toUpperCase() + name.slice(1); const choices = possibleValues.map(value => ({ id: value, name: ucfirst(value) })); <DualListInput source="roles" choices={choices} />

选项来自另一资源:交给父组件注入

如果需要从另一资源拉取选项,你实际上是在编辑一对多或多对多关系。此时应把<DualListInput>包进<ReferenceArrayInput>(一对多)或<ReferenceManyToManyInput>(多对多)中,无需再传choices——父组件会根据关联资源的可选值自动注入:

<ReferenceArrayInput source="tag_ids" reference="tags"> <DualListInput /> </ReferenceArrayInput>

提示:如果DualListInput用于ReferenceArrayInput内部,translateChoice会被自动置为false(因为关联记录文本通常不需要再翻译)。

关于校验:如果需要校验(如required()),请把validate放在子组件<DualListInput>上。<ReferenceArrayInput>本身不接受任何校验属性——这一点在原文档与 ReferenceArrayInput 文档的 Validation 小节 中均有明确说明,其原因是"通用输入属性应作用于子组件"。

disableValue:自定义禁用字段名

默认情况下,<DualListInput>会把choicesdisabled字段为真的选项渲染为禁用:

const choices = [ { _id: 'admin', label: 'Admin', disabled: true }, { _id: 'u001', label: 'Editor' }, { _id: 'u002', label: 'Moderator' }, { _id: 'u003', label: 'Reviewer' }, ]; <DualListInput source="roles" choices={choices} />

如果你的数据用其他字段表示禁用(例如not_available),设置disableValue属性:

const choices = [ { _id: 'admin', label: 'Admin', not_available: true }, { _id: 'u001', label: 'Editor' }, { _id: 'u002', label: 'Moderator' }, { _id: 'u003', label: 'Reviewer' }, ]; <DualListInput source="roles" choices={choices} disableValue="not_available" />

optionText:自定义选项文本

默认用name字段显示选项文本,可以通过optionText改为其他字段:

const choices = [ { id: 'admin', label: 'Admin' }, { id: 'u001', label: 'Editor' }, { id: 'u002', label: 'Moderator' }, { id: 'u003', label: 'Reviewer' }, ]; <DualListInput source="roles" choices={choices} optionText="label" />

当选项是来自<ReferenceArrayInput><ReferenceManyToManyInput>的关联记录时,optionText尤其有用。默认情况下 react-admin 使用资源的recordRepresentation函数生成记录标签;一旦显式设置了optionText,则优先使用它:

<ReferenceArrayInput source="tag_ids" reference="tags"> <DualListInput optionText="tag" /> </ReferenceArrayInput>

函数形式的 optionText

optionText也接受函数,从而基于整个选项对象拼装文本:

const choices = [ { id: 123, first_name: 'Leo', last_name: 'Tolstoi' }, { id: 456, first_name: 'Jane', last_name: 'Austen' }, ]; const optionRenderer = choice => `${choice.first_name} ${choice.last_name}`; <DualListInput source="authors" choices={choices} optionText={optionRenderer} />

React 元素形式的 optionText

optionText还可以是一个 React 元素,该元素会在<RecordContext>中以对应选项作为record渲染,因此可以直接在里面使用 Field 组件:

const choices = [ { id: 123, first_name: 'Leo', last_name: 'Tolstoi' }, { id: 456, first_name: 'Jane', last_name: 'Austen' }, ]; const FullNameField = () => { const record = useRecordContext(); return <span>{record.first_name} {record.last_name}</span>; } <DualListInput source="authors" choices={choices} optionText={<FullNameField />}/>

optionValue:自定义选项值字段

默认使用id字段作为写入表单的值,可通过optionValue改成其他字段:

const choices = [ { _id: 'admin', name: 'Admin' }, { _id: 'u001', name: 'Editor' }, { _id: 'u002', name: 'Moderator' }, { _id: 'u003', name: 'Reviewer' }, ]; <DualListInput source="roles" choices={choices} optionValue="_id" />

重要限制optionValue仅在choiceschoices属性直接提供时生效。当<DualListInput>位于<ReferenceArrayInput>内部时,optionValue恒为id——因为此时选项是从关联资源拉取的记录,而 react-admin 要求记录必须始终具有id字段。

translateChoice:关闭选项翻译

choices默认会被翻译,因此可以直接把翻译标识符作为选项文本:

const choices = [ { id: 'admin', name: 'myroot.roles.admin' }, { id: 'u001', name: 'myroot.roles.u001' }, { id: 'u002', name: 'myroot.roles.u002' }, { id: 'u003', name: 'myroot.roles.u003' }, ];

某些场景下你不希望选项被翻译(例如选项文本本身就是专有名词或代码),将translateChoice设为false

<DualListInput source="roles" choices={choices} translateChoice={false}/>

再次强调:当<DualListInput><ReferenceArrayInput>的子组件时,translateChoice会被自动设置为false

在 ReferenceArrayInput 中使用

choices需要由另一资源的记录填充时,用<ReferenceArrayInput>包裹<DualListInput>留空choices

import { Create, DateInput, ReferenceArrayInput, SimpleForm, TextInput, } from 'react-admin'; import { DualListInput } from "@react-admin/ra-relationships"; export const PostCreate = () => ( <Create> <SimpleForm> <TextInput source="title" /> <TextInput multiline source="body" /> <DateInput source="published_at" /> <ReferenceArrayInput reference="tags" source="tags"> <DualListInput optionText="name" /> </ReferenceArrayInput> </SimpleForm> </Create> );

底层数据获取原理

从 ReferenceArrayInput 文档 可以了解到这种嵌套方式背后的数据流。<ReferenceArrayInput>会根据source中的外键数组发起两类查询:

dataProvider.getMany('tags', { ids: [1, 23, 4] }); dataProvider.getList('tags', { filter: {}, sort: { field: 'id', order: 'DESC' }, pagination: { page: 1, perPage: 25 } });
  • getMany用于取回当前已选记录,渲染右侧"已选列表";
  • getList用于拉取全部候选记录,填充左侧"可选列表"。

正因为候选列表来自一次getList查询,<ReferenceArrayInput>pageperPagesortfilter等属性都会间接影响<DualListInput>看到的可选条目。默认只取前 25 条(perPage默认 25);如果可选条目很多但你又坚持使用<DualListInput>,务必通过perPage调大拉取数量,同时接受它没有自动补全、无法按需搜索的局限。

校验放在子组件上

需要校验时(例如至少选择一项),把validate放到<DualListInput>上,而不是<ReferenceArrayInput>上:

<ReferenceArrayInput source="tag_ids" reference="tags"> <DualListInput validate={required()} /> </ReferenceArrayInput>

这也与 Validation 文档 中"校验属性作用于子输入组件"的约定一致。

sx:CSS API 与全局主题覆盖

<DualListInput>接受常规的className属性,也支持用sx覆盖内部各子元素的样式(sx语法与示例参见 SX 文档)。可用的样式子类如下:

Rule nameDescription
& .RaDualListInput-main主容器
& .RaDualListInput-label标签
& .RaDualListInput-actions按钮容器
& .RaDualListInput-button每个按钮
& .RaDualListInput-addButton添加按钮
& .RaDualListInput-removeButton移除按钮
& .RaDualListInput-list每个列表
& .RaDualListInput-listHeader每个列表的标题栏
& .RaDualListInput-selectedList已选列表
& .RaDualListInput-availableList可选列表

例如,调整两个列表的固定高度与按钮列布局:

<DualListInput source="roles" choices={choices} sx={{ '& .RaDualListInput-list': { height: 240 }, '& .RaDualListInput-availableList': { bgcolor: 'grey.50' }, '& .RaDualListInput-selectedList': { bgcolor: 'primary.light' }, }} />

若要使用应用级样式覆盖(theme overrides)一次性作用于所有<DualListInput>实例,请使用RaDualListInput作为组件覆写 key,相关机制见 AppTheme 文档的 Theming Individual Components 小节。

常见问题与注意事项

  • 数组值必须是source上的数组字段:表单值形如roles: ['u001', 'u003']DualListInput读写该数组,移动操作实时更新数组内容。
  • optionValue在 ReferenceArrayInput 内不生效:此时恒为id,且要求关联资源记录带id字段。
  • translateChoice在 ReferenceArrayInput 内自动为false
  • 校验只能放子组件<ReferenceArrayInput>不接受validate
  • 不适合超多候选条目:由于没有自动补全,当参考资源条目非常多时,<DualListInput>可能不是最佳选择,此时可考虑<AutocompleteArrayInput>(自动补全)或<DataTableInput>(支持多列比较)作为<ReferenceArrayInput>的子组件。
  • 选项禁用只影响移动操作disabled(或disableValue指定的字段)为真的选项无法被用户加入/移出已选列表。

总结

<DualListInput>用最直观的"双列穿梭"交互解决了数组值与关联关系的编辑问题:静态场景下通过choices直接声明可选列表,配合optionTextoptionValuedisableValuetranslateChoice覆盖各种数据形态;动态场景下则作为<ReferenceArrayInput>/<ReferenceManyToManyInput>的子组件,由父组件注入关联记录。借助sx子类与RaDualListInput全局覆写键,还能将它的外观完全纳入你的主题体系。在 react-admin 的输入组件体系中,它和TextArrayInputAutocompleteArrayInputSelectArrayInputCheckboxGroupInput互为补充,按候选集规模与交互需求选择合适的方案即可。

【免费下载链接】react-adminA frontend Framework for single-page applications on top of REST/GraphQL APIs, using TypeScript, React and Material Design项目地址: https://gitcode.com/gh_mirrors/re/react-admin

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询