使用 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 总览
原文档给出了完整属性表,整理如下(所有类型与默认值均以原文档为准):
| Prop | Required | Type | Default | Description |
|---|---|---|---|---|
choices | Optional | Object[] | - | 要作为选项展示的条目列表。除非位于<ReferenceArrayInput>内部,否则必需。 |
addButton | Optional | 'outlined' | 'contained' | 'text' |element | - | 添加按钮的 Material UIvariant值,或一个用于完全替换它的 React 元素 |
addButtonLabel | Optional | string | ra-relationships.duallistinput.select | 添加按钮的文本或翻译 key |
availableItemsLabel | Optional | string | ra-relationships.duallistinput.availableItems | 可选列表标题的文本或翻译 key |
dense | Optional | boolean | false | 列表组件的视觉密度 |
disableValue | Optional | string | 'disabled' | 用于在choices中标记"禁用选项"的自定义字段名 |
optionText | Optional | string|Function | name | 用于显示选项文本的记录字段名,或接收当前记录并返回字符串的函数record => {string} |
optionValue | Optional | string | id | 记录中用于作为输入值的字段名 |
removeButton | Optional | 'outlined' | 'contained' | 'text' |element | - | 移除按钮的 Material UIvariant值,或一个用于完全替换它的 React 元素 |
removeButtonLabel | Optional | string | ra-relationships.duallistinput.unselect | 移除按钮的文本或翻译 key |
selectedItemsLabel | Optional | string | ra-relationships.duallistinput.selectedItems | 已选列表标题的文本或翻译 key |
translateChoice | Optional | boolean | true | 选项文本是否应被翻译 |
此外,<DualListInput>还接受所有通用输入属性(common input props),包括source、label、defaultValue、disabled、readOnly、fullWidth、helperText、format、parse、validate、className与sx。
按钮与列表标题的本地化
由于组件属于企业版包,其内置文案通过翻译 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,可以通过optionText与optionValue指定:
<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>会把choices中disabled字段为真的选项渲染为禁用:
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仅在choices由choices属性直接提供时生效。当<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>的page、perPage、sort、filter等属性都会间接影响<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 name | Description |
|---|---|
& .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直接声明可选列表,配合optionText、optionValue、disableValue、translateChoice覆盖各种数据形态;动态场景下则作为<ReferenceArrayInput>/<ReferenceManyToManyInput>的子组件,由父组件注入关联记录。借助sx子类与RaDualListInput全局覆写键,还能将它的外观完全纳入你的主题体系。在 react-admin 的输入组件体系中,它和TextArrayInput、AutocompleteArrayInput、SelectArrayInput、CheckboxGroupInput互为补充,按候选集规模与交互需求选择合适的方案即可。
【免费下载链接】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),仅供参考