FormList 表单列表
简介
FormList 继承于 FormItem 组件,自带了新增、删除等操作按钮,用于处理数组结构的表单数据。
代码演示
基础演示
FormList 会给默认插槽传递必要的数据,row 用于绑定一行的数据,getProp 用于拼接表单组件的 prop。
<template>
<co-form :model="formModel" label-width="auto" :submit="onSubmit">
<co-form-item
v-model="formModel.activity"
prop="activity"
label="活动名称"
width="md"
required
/>
<co-form-list
v-model="formModel.awards"
prop="awards"
label="活动奖品"
required
v-slot="{ row, getProp }"
>
<co-form-item
v-model="row.award"
:prop="getProp('award')"
label="奖品"
field-type="select"
:field-props="{ options: awards }"
width="sm"
required
/>
<co-form-item v-model="row.num" :prop="getProp('num')" label="数量" width="sm" required />
</co-form-list>
</co-form>
</template>
<script lang="ts" setup>
import { reactive } from 'vue';
import { awards } from './data';
import { ElMessage } from 'element-plus';
const formModel = reactive<{
activity: string;
awards: {
award: string;
num: string;
}[];
}>({
activity: '',
awards: [],
});
const onSubmit = async () => {
await new Promise((resolve) => setTimeout(resolve, 500));
console.log(formModel);
ElMessage.success('提交成功');
};
</script>
自定义列
默认情况下,FormList 会读取默认插槽内子组件中为 FormItem 组件的 width、required、rules required 以及 label 等信息生成列表头部。
如果 FormItem 不是作为其直接子组件或有其他影响布局的组件,则需手动通过 columns 属性声明列头信息。
<template>
<co-form :model="formModel" label-width="auto" :submit="onSubmit">
<co-form-item
v-model="formModel.activity"
prop="activity"
label="活动名称"
width="md"
required
/>
<co-form-list
v-model="formModel.awards"
prop="awards"
label="活动奖品"
required
:columns="columns"
v-slot="{ row, getProp }"
>
<co-form-item
v-model="row.award"
:prop="getProp('award')"
field-type="select"
:field-props="{ options: awards }"
width="sm"
required
/>
<div>
<co-form-item v-model="row.num" :prop="getProp('num')" width="sm" required />
</div>
</co-form-list>
</co-form>
</template>
<script lang="ts" setup>
import { reactive } from 'vue';
import { awards } from './data';
import { ElMessage } from 'element-plus';
const formModel = reactive<{
activity: string;
awards: {
award: string;
num: string;
}[];
}>({
activity: '',
awards: [],
});
const columns = [
{ label: '奖品名称', width: 'sm', required: true },
{ label: '奖品数量', width: 'sm', required: true },
];
const onSubmit = async () => {
await new Promise((resolve) => setTimeout(resolve, 500));
console.log(formModel);
ElMessage.success('提交成功');
};
</script>
表格布局
layout="table" 把每一行换成 el-table 的一行,列头、列宽、对齐交给表格,适合行数多、需要横向对照的场景。
列与默认插槽里的子节点按顺序一一对应,因此同一份字段模板可以同时喂给两种布局。要在列里放非表单内容(计算列、纯文本),显式声明 columns 把列数对齐即可。
校验、min / max / draggable / readonly 的行为与默认布局一致。表格里 draggable 只让左侧的手柄可拖,避免在输入框里按下鼠标变成拖整行。拖到某一行的上半部分就插到它前面、下半部分就插到它后面,落点线画在真实的插入位置上。
<template>
<co-form :model="formModel" label-width="auto" :submit="onSubmit">
<co-form-item v-model="formModel.title" prop="title" label="报销单标题" width="md" required />
<co-form-list
v-model="formModel.items"
prop="items"
label="费用明细"
layout="table"
draggable
:columns="columns"
:default-value="defaultItem"
:min="1"
:max="5"
add-text="添加一行"
v-slot="{ row, getProp }"
>
<co-form-item v-model="row.name" :prop="getProp('name')" label="费用项" required />
<co-form-item
v-model="row.type"
:prop="getProp('type')"
label="类型"
field-type="select"
:field-props="{ options: types }"
required
/>
<co-form-item
v-model="row.amount"
:prop="getProp('amount')"
label="金额"
field-type="number"
:field-props="{ min: 0, precision: 2, controls: false }"
:rules="amountRules"
required
/>
</co-form-list>
</co-form>
</template>
<script lang="ts" setup>
import { reactive } from 'vue';
import { ElMessage } from 'element-plus';
import { type FormListColumn } from 'cosey/components';
const types = [
{ value: 'travel', label: '差旅费' },
{ value: 'meal', label: '餐费' },
{ value: 'office', label: '办公用品' },
];
const formModel = reactive<{
title: string;
items: {
name: string;
type: string;
amount: number | undefined;
}[];
}>({
title: '上海出差报销',
items: [
{ name: '高铁票', type: 'travel', amount: 553 },
{ name: '酒店', type: 'travel', amount: 1280 },
{ name: '工作餐', type: 'meal', amount: 88 },
],
});
// 列与默认插槽里的子节点按顺序一一对应,宽度可以用数字或 xs~xl 预设
const columns: FormListColumn[] = [
{ label: '费用项', width: 170, required: true },
{ label: '类型', width: 140, required: true },
{ label: '金额', width: 120, required: true },
];
const defaultItem = { name: '', type: 'travel', amount: undefined };
const amountRules = [
{ required: true, message: '请输入金额', trigger: 'change' },
{ type: 'number' as const, min: 0.01, message: '金额需大于 0', trigger: 'change' },
];
const onSubmit = async () => {
await new Promise((resolve) => setTimeout(resolve, 500));
console.log(formModel);
ElMessage.success('提交成功');
};
</script>
默认值
可以通过 default-value 属性定义新增时的默认行数据。
<template>
<co-form :model="formModel" label-width="auto" :submit="onSubmit">
<co-form-item
v-model="formModel.activity"
prop="activity"
label="活动名称"
width="md"
required
/>
<co-form-list
v-model="formModel.awards"
prop="awards"
label="活动奖品"
required
:default-value="defaultAward"
v-slot="{ row, getProp }"
>
<co-form-item
v-model="row.award"
:prop="getProp('award')"
label="奖品"
field-type="select"
:field-props="{ options: awards }"
width="sm"
required
/>
<co-form-item v-model="row.num" :prop="getProp('num')" label="数量" width="sm" required />
</co-form-list>
</co-form>
</template>
<script lang="ts" setup>
import { reactive } from 'vue';
import { awards } from './data';
import { ElMessage } from 'element-plus';
const formModel = reactive<{
activity: string;
awards: {
award: string;
num: string;
}[];
}>({
activity: '',
awards: [],
});
const defaultAward = {
award: '手机',
num: '100',
};
const onSubmit = async () => {
await new Promise((resolve) => setTimeout(resolve, 500));
console.log(formModel);
ElMessage.success('提交成功');
};
</script>
行数量范围
可以通过 min、max 属性设置最小、最大行数。
<template>
<co-form :model="formModel" label-width="auto" :submit="onSubmit">
<co-form-item
v-model="formModel.activity"
prop="activity"
label="活动名称"
width="md"
required
/>
<co-form-list
v-model="formModel.awards"
prop="awards"
label="活动奖品"
required
:min="1"
:max="3"
v-slot="{ row, getProp }"
>
<co-form-item
v-model="row.award"
:prop="getProp('award')"
label="奖品"
field-type="select"
:field-props="{ options: awards }"
width="sm"
required
/>
<co-form-item v-model="row.num" :prop="getProp('num')" label="数量" width="sm" required />
</co-form-list>
</co-form>
</template>
<script lang="ts" setup>
import { reactive } from 'vue';
import { awards } from './data';
import { ElMessage } from 'element-plus';
const formModel = reactive<{
activity: string;
awards: {
award: string;
num: string;
}[];
}>({
activity: '',
awards: [],
});
const onSubmit = async () => {
await new Promise((resolve) => setTimeout(resolve, 500));
console.log(formModel);
ElMessage.success('提交成功');
};
</script>
拖拽排序
设置 draggable 属性可以在每行前面生成拖拽按钮,可以通过上下拖拽来调整其顺序。
<template>
<co-form :model="formModel" label-width="auto" :submit="onSubmit">
<co-form-item
v-model="formModel.activity"
prop="activity"
label="活动名称"
width="md"
required
/>
<co-form-list
v-model="formModel.awards"
prop="awards"
label="活动奖品"
required
draggable
v-slot="{ row, getProp }"
>
<co-form-item
v-model="row.award"
:prop="getProp('award')"
label="奖品"
field-type="select"
:field-props="{ options: awards }"
width="sm"
required
/>
<co-form-item v-model="row.num" :prop="getProp('num')" label="数量" width="sm" required />
</co-form-list>
</co-form>
</template>
<script lang="ts" setup>
import { reactive } from 'vue';
import { awards } from './data';
import { ElMessage } from 'element-plus';
const formModel = reactive({
activity: '',
awards: Array.from({ length: 4 }).map((_, i) => {
return {
award: '手机',
num: String(i),
};
}),
});
const onSubmit = async () => {
await new Promise((resolve) => setTimeout(resolve, 500));
console.log(formModel);
ElMessage.success('提交成功');
};
</script>
自定义结构
如果默认结构不满足需求,可以使用 custom 插槽自定义结构,custom 插槽接收以下参数:
list列表数据,用于遍历并绑定到表单项。getKey接收行对象作为参数,返回对应的全局唯一的key。getProp用于拼接表单项的prop。add添加一行数据,不传递参数则使用默认值。remove接收行下标,用于移除一行数据。move移动一行数据,通常用于配合DndSort组件使用。
如果需要拖拽排序,可以配合 DndSort 组件使用。
如果需要在列表组件变动时添加动画效果,可以配合 TransitionGroup 组件使用。
<template>
<co-form :model="formModel" label-width="80px" :submit="onSubmit">
<co-form-item
v-model="formModel.department"
prop="department"
label="部门"
width="md"
required
/>
<co-form-list v-model="formModel.employees" prop="employees" label="员工" required>
<template #custom="{ list, getKey, getProp, add, remove, move }">
<div class="flex flex-col gap-y-4">
<co-dnd-sort @move="move">
<co-transition-group effect="fade">
<co-dnd-sort-item v-for="(row, index) in list" :key="getKey(row)" :index="index">
<el-card shadow="never" class="relative gap-4">
<el-space direction="vertical" fill size="large">
<co-form-group size="large">
<co-form-item
v-model="row.name"
:prop="getProp(index, 'name')"
label="姓名"
width="sm"
required
/>
<co-form-item
v-model="row.gender"
:prop="getProp(index, 'gender')"
label="性别"
field-type="radiogroup"
:field-props="{ options: genders }"
width="sm"
required
/>
</co-form-group>
<co-form-group size="large">
<co-form-item
v-model="row.num"
:prop="getProp(index, 'num')"
label="工号"
width="sm"
required
/>
<co-form-item
v-model="row.tel"
:prop="getProp(index, 'tel')"
label="电话"
width="sm"
required
/>
</co-form-group>
<co-form-item
v-model="row.address"
:prop="getProp(index, 'address')"
label="地址"
field-type="textarea"
required
/>
</el-space>
<el-button class="absolute end-1 top-1" link type="danger" @click="remove(index)">
<co-icon name="bi bi-trash3" size="lg" />
</el-button>
</el-card>
</co-dnd-sort-item>
</co-transition-group>
</co-dnd-sort>
<el-button plain class="w-full" @click="() => add()">
<co-icon name="bi bi-plus-lg" class="me-1" />
新增
</el-button>
</div>
</template>
</co-form-list>
</co-form>
</template>
<script lang="ts" setup>
import { reactive } from 'vue';
import { genders } from './data';
import { ElMessage } from 'element-plus';
const formModel = reactive<{
department: string;
employees: {
name: string;
gender: string;
num: string;
tel: string;
address: string;
}[];
}>({
department: '',
employees: [],
});
const onSubmit = async () => {
await new Promise((resolve) => setTimeout(resolve, 500));
console.log(formModel);
ElMessage.success('提交成功');
};
</script>
嵌套
列表里面也可以嵌套列表。
<template>
<co-form :model="formModel" label-width="80px" :submit="onSubmit">
<co-form-list
v-model="formModel.departments"
prop="departments"
label="部门"
:rules="[{ type: 'array', required: true, message: '不能为空' }]"
>
<template #custom="{ list, getKey, getProp, add, remove, move }">
<div class="flex flex-col gap-y-4">
<co-dnd-sort @move="move">
<co-transition-group effect="fade">
<co-dnd-sort-item v-for="(row, index) in list" :key="getKey(row)" :index="index">
<el-card shadow="never" class="relative">
<el-space direction="vertical" fill size="large">
<co-form-item
v-model="row.name"
:prop="getProp(index, 'name')"
label="部门名称"
width="md"
:rules="[{ required: true, message: '请输入部门名称' }]"
/>
<co-form-list
v-model="row.employees"
:prop="getProp(index, 'employees')"
label="员工"
:rules="[
{
type: 'array',
required: true,
message: '不能为空',
},
]"
draggable
v-slot="{ row, getProp }"
>
<co-form-item
v-model="row.num"
:prop="getProp('num')"
label="工号"
width="sm"
:rules="[{ required: true, message: '请输入工号' }]"
/>
<co-form-item
v-model="row.name"
:prop="getProp('name')"
label="姓名"
width="sm"
:rules="[{ required: true, message: '请输入姓名' }]"
/>
</co-form-list>
</el-space>
<el-button class="absolute end-3 top-3" link type="danger" @click="remove(index)">
<co-icon name="bi bi-trash3" size="lg" />
</el-button>
</el-card>
</co-dnd-sort-item>
</co-transition-group>
</co-dnd-sort>
<el-button plain class="w-full" @click="() => add()">
<co-icon name="bi bi-plus-lg" class="me-1" />
新增
</el-button>
</div>
</template>
</co-form-list>
</co-form>
</template>
<script lang="ts" setup>
import { reactive } from 'vue';
import { ElMessage } from 'element-plus';
const formModel = reactive<{
departments: {
name: string;
employees: {
num: string;
name: string;
}[];
}[];
}>({
departments: [],
});
const onSubmit = async () => {
await new Promise((resolve) => setTimeout(resolve, 500));
console.log(formModel);
ElMessage.success('提交成功');
};
</script>
API
FormListProps<T>
继承 FormProps ,并有以下复写或额外属性。
| 属性 | 描述 | 类型 | 默认值 |
|---|---|---|---|
| default-value | 新增行的默认值 | T | {} |
| model-value | 绑定的值 | T[] | [] |
| min | 最少行数,少于等于此数值则隐藏删除按钮 | number | - |
| max | 最多行数,大于等于此数值则隐藏新增按钮 | number | - |
| add-text | 新增按钮的文本 | string | '新增' |
| columns | 自定义列头 | FormListColumn[] | - |
| draggable | 是否可拖拽排序 | boolean | false |
| layout | 渲染形态 | 'default' | 'table' | 'default' |
| table-props | 透传给内部 el-table 的属性 | Partial<TableProps> | - |
FormListColumn
| 属性 | 描述 | 类型 | 默认值 |
|---|---|---|---|
| label | 列头文本 | string | - |
| width | 列宽,表格布局下同时支持数字与 xs~xl 预设 | FormItemWidth | - |
| required | 列头是否显示必填星号 | boolean | false |
| prop | 列对应的字段名,缺省取表单项 prop 的末段 | string | - |
| align | 单元格对齐,仅表格布局生效 | 'left' | 'center' | 'right' | - |
| headerAlign | 列头对齐,仅表格布局生效,缺省跟随 align | 'left' | 'center' | 'right' | - |
FormListSlots<T>
继承 FormProps ,并有以下复写或额外属性。
| 插槽 | 描述 | 属性 |
|---|---|---|
| default | 默认结构的内容 | { row: T; index: number; getProp: (...args: (string | number)[]) => string[]; } |
| custom | 自定义结构的内容 | { list: T[]; getKey: (row: T) => string; getProp: (...args: (string | number)[]) => string[]; } & FormListAction<T> |
FormListEmits<T>
| 事件 | 描述 | 类型 |
|---|---|---|
| update:modelValue | 列表数据改变时触发 | (value: T[]) => void |
FormListAction<T>
| 属性 | 描述 | 类型 |
|---|---|---|
| add | 新增一行数据,默认根据默认值新增到列表最后 | (row?: T, index?: number) => void |
| remove | 删除指定下标的一行数据 | (index: number) => void |
| move | 移动一行数据 | (fromIndex: number, toIndex: number) => void |
FormListExpose<T>
继承 FormItemExpose 和 FormListAction<T>。