Skip to content

Commit

Permalink
feat: add dynamic list input (#6146)
Browse files Browse the repository at this point in the history
#### What type of PR is this?

/kind feature
/area ui
/milestone 2.17.x

#### What this PR does / why we need it:

为 formkit 增加动态列表的 input。

使用方式:

```
- $formkit: list
  name: users
  label: Users
  addLabel: Add User
  min: 1
  max: 3
  itemType: string
  children:
    - $formkit: text
      index: "$index"
      validation: required
```

> [!NOTE]
> `list` 组件有且只有一个子节点,并且必须为子节点传递 `index` 属性。若想提供多个字段,则建议使用 `group` 组件包裹。

#### How to test it?

测试动态数组是否正常可用。保存的结果是否为 `{users: ["", ""]}`

#### Which issue(s) this PR fixes:

Fixes #6098

#### Does this PR introduce a user-facing change?
```release-note
为 Formkit 增加动态列表的 input 组件 list
```
  • Loading branch information
LIlGG authored Jun 26, 2024
1 parent ae6724a commit 5d5df7c
Show file tree
Hide file tree
Showing 8 changed files with 560 additions and 0 deletions.
74 changes: 74 additions & 0 deletions ui/docs/custom-formkit-input/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,17 @@
6. downControl: 是否显示下移按钮,默认为 `true`
7. insertControl: 是否显示插入按钮,默认为 `true`
8. removeControl: 是否显示删除按钮,默认为 `true`
- `list`: 动态列表,定义一个数组列表。
- 参数
1. itemType: 列表项的数据类型,用于初始化数据类型,可选参数 `string`, `number`, `boolean`, `object`,默认为 `string`
1. min: 最小数量,默认为 `0`
2. max: 最大数量,默认为 `Infinity`,即无限制。
3. addLabel: 添加按钮的文本,默认为 `添加`
4. addButton: 是否显示添加按钮,默认为 `true`
5. upControl: 是否显示上移按钮,默认为 `true`
6. downControl: 是否显示下移按钮,默认为 `true`
7. insertControl: 是否显示插入按钮,默认为 `true`
8. removeControl: 是否显示删除按钮,默认为 `true`
- `menuCheckbox`:选择一组菜单
- `menuRadio`:选择一个菜单
- `menuItemSelect`:选择菜单项
Expand Down Expand Up @@ -70,6 +81,69 @@ const postName = ref("");
label: 底部菜单组
```
### list
list 是一个数组类型的输入组件,可以让使用者可视化的操作数组。它支持动态添加、删除、上移、下移、插入数组项等操作。
在 Vue SFC 中以组件形式使用:
```vue
<script lang="ts" setup>
const users = ref([]);
</script>

<template>
<FormKit
:min="1"
:max="3"
type="list"
label="Users"
add-label="Add User"
item-type="string"
>
<template #default="{ index }">
<FormKit
type="text"
:index="index"
validation="required"
/>
</template>
</FormKit>
</template>
```

在 FormKit Schema 中使用:

```yaml
- $formkit: list
name: users
label: Users
addLabel: Add User
min: 1
max: 3
itemType: string
children:
- $formkit: text
index: "$index"
validation: required
```
> [!NOTE]
> `list` 组件有且只有一个子节点,并且必须为子节点传递 `index` 属性。若想提供多个字段,则建议使用 `group` 组件包裹。


最终得到的数据类似于:

```json
{
"users": [
"Jack",
"John"
]
}
```


### Repeater

Repeater 是一个集合类型的输入组件,可以让使用者可视化的操作集合。
Expand Down
2 changes: 2 additions & 0 deletions ui/src/formkit/formkit.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { attachmentPolicySelect } from "./inputs/attachment-policy-select";
import { attachmentGroupSelect } from "./inputs/attachment-group-select";
import { password } from "./inputs/password";
import { verificationForm } from "./inputs/verify-form";
import { list } from "./inputs/list";

import radioAlt from "./plugins/radio-alt";
import stopImplicitSubmission from "./plugins/stop-implicit-submission";
Expand All @@ -41,6 +42,7 @@ const config: DefaultConfigOptions = {
autoScrollToErrors,
],
inputs: {
list,
form,
password,
group,
Expand Down
37 changes: 37 additions & 0 deletions ui/src/formkit/inputs/list/AddButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<script lang="ts" setup>
import { VButton, IconAddCircle } from "@halo-dev/components";
import type { FormKitFrameworkContext } from "@formkit/core";
import type { PropType } from "vue";
const props = defineProps({
context: {
type: Object as PropType<FormKitFrameworkContext>,
required: true,
},
disabled: {
type: Boolean,
required: false,
},
onClick: {
type: Function as PropType<() => void>,
required: true,
},
});
const handleAppendClick = () => {
if (!props.disabled && props.onClick) {
props.onClick();
}
};
</script>

<template>
<div :class="context.classes.add" @click="handleAppendClick">
<VButton :disabled="disabled" type="secondary">
<template #icon>
<IconAddCircle class="h-full w-full" />
</template>
{{ context.addLabel || $t("core.common.buttons.add") }}
</VButton>
</div>
</template>
79 changes: 79 additions & 0 deletions ui/src/formkit/inputs/list/features/lists.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import type { FormKitNode } from "@formkit/core";
import { undefine } from "@formkit/utils";

export const lists = function (node: FormKitNode) {
node._c.sync = true;
node.on("created", listFeature.bind(null, node));
};

const fn = (node: FormKitNode): object | string | boolean | number => {
switch (node.props.itemType.toLocaleLowerCase()) {
case "object":
return {};
case "boolean":
return false;
case "number":
return 0;
default:
return "";
}
};

function createValue(num: number, node: FormKitNode) {
return new Array(num).fill("").map(() => fn(node));
}

function listFeature(node: FormKitNode) {
node.props.removeControl = node.props.removeControl ?? true;
node.props.upControl = node.props.upControl ?? true;
node.props.downControl = node.props.downControl ?? true;
node.props.insertControl = node.props.insertControl ?? true;
node.props.addButton = node.props.addButton ?? true;
node.props.addLabel = node.props.addLabel ?? false;
node.props.addAttrs = node.props.addAttrs ?? {};
node.props.min = node.props.min ? Number(node.props.min) : 0;
node.props.max = node.props.max ? Number(node.props.max) : Infinity;
node.props.itemType = node.props.itemType ?? "string";
if (node.props.min > node.props.max) {
throw Error("list: min must be less than max");
}

if ("disabled" in node.props) {
node.props.disabled = undefine(node.props.disabled);
}

if (Array.isArray(node.value)) {
if (node.value.length < node.props.min) {
const value = createValue(node.props.min - node.value.length, node);
node.input(node.value.concat(value), false);
} else {
if (node.value.length > node.props.max) {
node.input(node.value.slice(0, node.props.max), false);
}
}
} else {
node.input(createValue(node.props.min, node), false);
}

if (node.context) {
const fns = node.context.fns;
fns.createShift = (index: number, offset: number) => () => {
const value = node._value as unknown[];
value.splice(index + offset, 0, value.splice(index, 1)[0]),
node.input(value, false);
};
fns.createInsert = (index: number) => () => {
const value = node._value as unknown[];
value.splice(index + 1, 0, fn(node)), node.input(value, false);
};
fns.createAppend = () => () => {
const value = node._value as unknown[];
console.log(fn(node));
value.push(fn(node)), node.input(value, false);
};
fns.createRemover = (index: number) => () => {
const value = node._value as unknown[];
value.splice(index, 1), node.input(value, false);
};
}
}
116 changes: 116 additions & 0 deletions ui/src/formkit/inputs/list/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import type { FormKitTypeDefinition } from "@formkit/core";

import {
disablesChildren,
renamesRadios,
fieldset,
messages,
message,
outer,
legend,
help,
inner,
prefix,
$if,
suffix,
} from "@formkit/inputs";
import {
addButton,
content,
controls,
down,
downControl,
downIcon,
empty,
insert,
insertControl,
insertIcon,
item,
items,
remove,
removeControl,
removeIcon,
up,
upControl,
upIcon,
} from "./sections";
import { i18n } from "@/locales";
import {
IconAddCircle,
IconArrowDownCircleLine,
IconArrowUpCircleLine,
IconCloseCircle,
} from "@halo-dev/components";
import AddButton from "./AddButton.vue";
import { lists } from "./features/lists";

/**
* Input definition for a dynamic list input.
* @public
*/
export const list: FormKitTypeDefinition = {
/**
* The actual schema of the input, or a function that returns the schema.
*/
schema: outer(
fieldset(
legend("$label"),
help("$help"),
inner(
prefix(),
$if(
"$value.length === 0",
$if("$slots.empty", empty()),
$if(
"$slots.default",
items(
item(
content("$slots.default"),
controls(
up(upControl(upIcon())),
remove(removeControl(removeIcon())),
insert(insertControl(insertIcon())),
down(downControl(downIcon()))
)
)
),
suffix()
)
),
suffix(),
addButton(`$addLabel || (${i18n.global.t("core.common.buttons.add")})`)
)
),
messages(message("$message.value"))
),
/**
* The type of node, can be a list, group, or input.
*/
type: "list",
/**
* An array of extra props to accept for this input.
*/
props: [
"min",
"max",
"upControl",
"downControl",
"removeControl",
"insertControl",
"addLabel",
"addButton",
"itemType",
],
/**
* Additional features that should be added to your input
*/
features: [lists, disablesChildren, renamesRadios],

library: {
IconAddCircle,
IconCloseCircle,
IconArrowUpCircleLine,
IconArrowDownCircleLine,
AddButton,
},
};
Loading

0 comments on commit 5d5df7c

Please sign in to comment.