feat: 表单项支持动态 name 可搭配 each 一起使用 Close: #4212 (#8452)

This commit is contained in:
liaoxuezhi 2023-10-24 15:16:05 +08:00 committed by GitHub
parent 47f6747213
commit 2b5efd478e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 215 additions and 52 deletions

View File

@ -10,6 +10,8 @@ order: 45
## 基本用法
通过 name 属性指定要循环的数组items 属性指定循环的内容。
```schema: scope="page"
{
"type": "page",
@ -29,7 +31,9 @@ order: 45
### 如果是对象数组
如果数组中的数据是对象,可以直接使用 data.xxx 来获取,另外能用 data.index 来获取数组索引,但如果对象本身也有名字为 index 的字段就会覆盖到,获取不到索引了。
如果数组中的数据是对象,可以直接使用内部变量 xxx 来获取,或者通过 `item.xxxx`。另外能用 index 来获取数组索引。
> 如果成员对象本身也有名字为 index 的字段就会覆盖到,获取不到索引了,请查看「循环嵌套」的章节解决
```schema:height="160" scope="page"
{
@ -42,12 +46,49 @@ order: 45
"name": "arr",
"items": {
"type": "tpl",
"tpl": "<span class='label label-default m-l-sm'><%= data.name %>:<%= data.index %></span> "
"tpl": "<span class='label label-default m-l-sm'>${name}:${index}</span> "
}
}
}
```
### 循环嵌套
如果存在嵌套使用,通过默认的 `item` 或者 `index` 始终是拿的最里面那层的信息,想要获取上层 each 的信息,则需要自定义 `itemKeyName``indexKeyName` 来指定字段名。
```schema:height="160" scope="page"
{
"type": "page",
"data": {
"arr": [{"name": "a", "subList": ["a1", "a2"]}, {"name": "b", "subList": ["b1", "b2"]}, {"name": "c", "subList": ["c1", "c2"]}]
},
"body": {
"type": "each",
"name": "arr",
"itemKeyName": "itemOutter",
"indexKeyName": "indexOutter",
"items": [
{
"type": "tpl",
"inline": false,
"tpl": "<span class='label label-default m-l-sm'>${name}:${index}</span> "
},
{
"type": "each",
"name": "subList",
"items": [
{
"type": "tpl",
"tpl": "<span class='label label-default m-l-sm'>${itemOutter.name}-${item}:${indexOutter}-${index}</span> "
}
]
}
]
}
}
```
## 用作 Field 时
当用在 Table 的列配置 Column、List 的内容、Card 卡片的内容和表单的 Static-XXX 中时,可以设置`name`属性,映射同名变量,然后用可以通过 `item` 变量获取单项值
@ -138,13 +179,47 @@ List 的内容、Card 卡片的内容配置同上
`name` 的优先级会比 `source` 更高
## 动态表单项
> 3.5.0 版本开始支持
表单项支持通过表达式配置动态表单项,可结合 `each` 组件一起使用。
```schema: scope="page"
{
"type": "page",
"data": {
"arr": ["A", "B", "C"]
},
"body": [
{
type: "form",
debug: true,
body: [
{
"type": "each",
"source": "${arr}",
"items": {
"type": "input-text",
"label": "Input${item}",
"name": "text${index}"
}
}
]
}
]
}
```
## 属性表
| 属性名 | 类型 | 默认值 | 说明 |
| ----------- | -------- | -------- | -------------------------------------------------------------------- |
| type | `string` | `"each"` | 指定为 Each 组件 |
| value | `array` | `[]` | 用于循环的值 |
| name | `string` | | 获取数据域中变量 |
| source | `string` | | 获取数据域中变量, 支持 [数据映射](../../docs/concepts/data-mapping) |
| items | `object` | | 使用`value`中的数据,循环输出渲染器。 |
| placeholder | `string` | | 当 `value` 值不存在或为空数组时的占位文本 |
| 属性名 | 类型 | 默认值 | 说明 |
| ------------ | -------- | -------- | -------------------------------------------------------------------- |
| type | `string` | `"each"` | 指定为 Each 组件 |
| value | `array` | `[]` | 用于循环的值 |
| name | `string` | | 获取数据域中变量 |
| source | `string` | | 获取数据域中变量, 支持 [数据映射](../../docs/concepts/data-mapping) |
| items | `object` | | 使用`value`中的数据,循环输出渲染器。 |
| placeholder | `string` | | 当 `value` 值不存在或为空数组时的占位文本 |
| itemKeyName | `string` | `item` | 获取循环当前数组成员 |
| indexKeyName | `string` | `index` | 获取循环当前索引 |

View File

@ -31,7 +31,7 @@ import {FormBaseControl, FormItemWrap} from './Item';
import {Api} from '../types';
import {TableStore} from '../store/table';
import pick from 'lodash/pick';
import {callStrFunction, changedEffect} from '../utils';
import {callStrFunction, changedEffect, tokenize} from '../utils';
export interface ControlOutterProps extends RendererProps {
formStore?: IFormStore;
@ -129,7 +129,6 @@ export function wrapControl<
colIndex,
rowIndex,
$schema: {
name,
id,
type,
required,
@ -163,6 +162,13 @@ export function wrapControl<
this.handleBlur = this.handleBlur.bind(this);
this.validate = this.validate.bind(this);
this.flushChange = this.flushChange.bind(this);
let name = this.props.$schema.name;
// 如果 name 是表达式
// 扩充 each 用法
if (isExpression(name)) {
name = tokenize(name, data);
}
if (!name) {
// 一般情况下这些表单项都是需要 name 的,提示一下
@ -308,7 +314,7 @@ export function wrapControl<
const {
store,
formStore: form,
$schema: {name, validate},
$schema: {validate},
addHook
} = this.props;
@ -326,7 +332,7 @@ export function wrapControl<
return finalValidate(
this.props.data,
this.getValue(),
name
formItem.name
).then((ret: any) => {
if ((typeof ret === 'string' || Array.isArray(ret)) && ret) {
formItem.addError(ret, 'control:valdiate');
@ -516,12 +522,7 @@ export function wrapControl<
}
controlRef(control: any) {
const {
addHook,
removeHook,
formStore: form,
$schema: {name}
} = this.props;
const {addHook, removeHook, formStore: form} = this.props;
// 因为 control 有可能被 n 层 hoc 包裹。
while (control && control.getWrappedInstance) {
@ -534,16 +535,15 @@ export function wrapControl<
this.hook = () => {
formItem.clearError('component:valdiate');
return validate(this.props.data, this.getValue(), name).then(
ret => {
if (
(typeof ret === 'string' || Array.isArray(ret)) &&
ret
) {
formItem.setError(ret, 'component:valdiate');
}
return validate(
this.props.data,
this.getValue(),
formItem.name
).then(ret => {
if ((typeof ret === 'string' || Array.isArray(ret)) && ret) {
formItem.setError(ret, 'component:valdiate');
}
);
});
};
addHook?.(this.hook);
} else if (!control && this.hook) {
@ -675,7 +675,6 @@ export function wrapControl<
formStore: form,
onChange,
$schema: {
name,
id,
label,
type,
@ -713,7 +712,7 @@ export function wrapControl<
eventType: 'formItemChange',
eventData: {
id,
name,
name: model.name,
label,
type,
value
@ -737,10 +736,10 @@ export function wrapControl<
if (model.extraName) {
const values = model.splitExtraValue(value);
onChange?.(values[0], name!);
onChange?.(values[0], model.name);
onChange?.(values[1], model.extraName, submitOnChange === true);
} else {
onChange?.(value, name!, submitOnChange === true);
onChange?.(value, model.name, submitOnChange === true);
}
this.checkValidate();
}
@ -766,7 +765,6 @@ export function wrapControl<
const model = this.model;
const {
formStore: form,
name,
$schema: {pipeOut},
onChange,
value: oldValue,
@ -786,10 +784,10 @@ export function wrapControl<
if (model.extraName) {
const values = model.splitExtraValue(value);
onChange?.(values[0], name!, false, true);
onChange?.(values[0], model.name!, false, true);
onChange?.(values[1], model.extraName!, false, true);
} else {
onChange?.(value, name!, false, true);
onChange?.(value, model.name!, false, true);
}
}
@ -812,12 +810,9 @@ export function wrapControl<
// 兼容老版本用法,新版本直接用 onChange 就可以。
setValue(value: any, key?: string) {
const {
$schema: {name},
onBulkChange
} = this.props;
const {onBulkChange} = this.props;
if (!key || key === name) {
if (!key || (this.model && key === this.model.name)) {
this.handleChange(value);
} else {
onBulkChange &&
@ -854,6 +849,7 @@ export function wrapControl<
formMode: control.mode || formMode,
ref: this.controlRef,
data: data || store?.data,
name: model?.name ?? control.name,
value,
changeMotivation: model?.changeMotivation,
defaultValue: control.value,

View File

@ -244,3 +244,49 @@ test('Renderer:FormItem:extraName', async () => {
end: `${moment().format('YYYY-MM')}-16`
});
});
test('Renderer:FormItem:dynamicName', async () => {
const onSubmit = jest.fn();
const {container, getByText} = render(
amisRender(
{
type: 'form',
id: 'theform',
submitText: 'Submit',
body: [
{
type: 'input-text',
name: '${a}',
label: 'Label'
}
],
title: 'The form'
},
{
onSubmit,
data: {
a: 'abc'
}
},
makeEnv({})
)
);
const input = container.querySelector('input[name=abc]');
expect(input).toBeTruthy();
fireEvent.change(input!, {
target: {
value: '123'
}
});
await wait(500); // 有 250 秒左右的节流
fireEvent.click(getByText('Submit'));
await wait(300);
expect(onSubmit).toHaveBeenCalled();
expect(onSubmit.mock.calls[0][0]).toMatchObject({
abc: '123'
});
});

View File

@ -5,6 +5,34 @@ import {resolveVariable, resolveVariableAndFilter} from 'amis-core';
import {createObject, getPropValue, isObject} from 'amis-core';
import {BaseSchema, SchemaCollection} from '../Schema';
export interface EachExtraProps extends RendererProps {
items: any;
item: any;
index: number;
itemKeyName: string;
indexKeyName: string;
name: string;
}
function EachItem(props: EachExtraProps) {
const {render, data, items, item, name, index, itemKeyName, indexKeyName} =
props;
const ctx = React.useMemo(
() =>
createObject(data, {
...(isObject(item) ? item : {}),
[name]: item,
[itemKeyName || 'item']: item,
[indexKeyName || 'index']: index
}),
[item, data, name, index, itemKeyName, indexKeyName]
);
return render(`item/${index}`, items, {
data: ctx
});
}
/**
* Each
* https://aisuda.bce.baidu.com/amis/zh-CN/components/each
@ -25,6 +53,20 @@ export interface EachSchema extends BaseSchema {
*/
source?: string;
/**
*
*
* @default item
*/
itemKeyName?: string;
/**
*
*
* @default index
*/
indexKeyName?: string;
items?: SchemaCollection;
placeholder?: string;
@ -50,6 +92,8 @@ export default class Each extends React.Component<EachProps> {
style,
render,
items,
itemKeyName,
indexKeyName,
placeholder,
classnames: cx,
translate: __
@ -82,17 +126,19 @@ export default class Each extends React.Component<EachProps> {
return (
<div className={cx('Each', className)} style={buildStyle(style, data)}>
{Array.isArray(arr) && arr.length && items ? (
arr.map((item: any, index: number) =>
render(`item/${index}`, items, {
data: createObject(
data,
isObject(item)
? {index, ...item}
: {[name]: item, item: item, index}
),
key: index
})
)
arr.map((item: any, index: number) => (
<EachItem
{...this.props}
items={items}
key={index}
index={index}
data={data}
item={item}
name={name}
itemKeyName={itemKeyName}
indexKeyName={indexKeyName}
/>
))
) : (
<div className={cx('Each-placeholder')}>
{render('placeholder', __(placeholder))}