Vue.js 3中Formvuelate嵌套表单处理方案咨询
问题场景
使用Formvuelate搭配vee-validate插件开发自定义表单构建服务,服务接收数据后返回组件Schema,通过SchemaFormWithValidation生成带验证的表单。
其中表格组件点击“添加”按钮会打开包含表单的模态框以添加行,但HTML不允许表单嵌套。尝试过用Teleport将模态框移到body外,但部分表格组件依赖父组件的值才会显示,必须在SchemaFormWithValidation内渲染,无法移到外部。当前问题是:表格在主表单内时,提交模态框表单会触发主表单整体验证,而非仅模态框部分,需要实现仅触发模态框表单验证、从主表单向表格组件传递动作,或者彻底将模态框与父表单解耦。
主表单构建器
<template> <div v-if="setupFinished" class="form-container"> <SchemaFormWithValidation :schema="formSchema" schemaRowClasses="row" @submit="submitForm" > <template #afterForm> <template v-for="pageSchema in schemaPages" :key="pageIndex"> <!-- Main form --> <SchemaFormWithValidation :schema="pageSchema[0]" schemaRowClasses="row" /> </template> <button ref="submitBtn" v-show="false"></button> </template> </SchemaFormWithValidation> <div v-if="currentPage < schemaPages.length - 1"> <FilledButton :style="'secondary'" @click="back">Back</FilledButton> <FilledButton :style="'secondary'" @click="next">Next</FilledButton> </div> <div> <FilledButton :style="'secondary'" @click="submit">Submit</FilledButton> </div> <!-- <Teleport to="body"> <GridTableModal v-if="showAddRowModal" :editElements="tableElement?.editElements" @close="closeTableAddRow" @submitRow="rowSubmitted" /> </Teleport> --> </div> </template> <script lang="ts"> const SchemaFormWithValidation = SchemaFormFactory([ VeeValidatePlugin({ mapProps(validation: any) { return { validation, }; }, }), ]); export default defineComponent({ name: "FormBuilder", props: { formData: { type: Object as PropType<FormBuilderForm>, required: true, }, }, components: { GridTableSchemaWrapper, SchemaFormWithValidation, FilledButton, GridTableModal, }, ..... </script>
表格组件
<template> <label v-if="label" class="text-input-label"> {{ label }} <template v-if="required === 'true'">*</template> <span class="errorMessage" v-if="validation?.errorMessage">{{ validation?.errorMessage }}</span> </label> <p v-show="tableIsOverflow" class="scroll-text">Scroll Horizontal</p> <div class="table-responsive" ref="tableContainerId"> <table class="table" ref="tableId"> <thead> <tr> <th v-for="column of columns" :key="column.datafield" scope="col"> {{ column.label }} </th> <th></th> </tr> </thead> <tbody> <tr v-for="(item, itemIndex) of data" :key="itemIndex"> <td v-for="column of columns" :key="column.datafield"> {{ item[column.datafield] }} </td> <td class="actions-col"> <button class="table-action-btn" @click="deleteItem(itemIndex)"> Delete </button> </td> </tr> </tbody> </table> </div> <BorderedButton :style="'secondary'" @click="openAddRowModal" type="button"> Add New </BorderedButton> <Teleport to="body"> <GridTableModal v-if="showAddRowModal" :editElements="element?.editElements" @submitRow="rowSubmitted" /> </Teleport> </template> <script lang="ts"> import { defineComponent, type PropType, ref } from "vue"; import { BorderedButton, FilledButton } from "@components"; import type { FormElement, TableHeader } from "@interfaces"; import GridTableModal from "../../grid-table/modal/GridTableModal.vue"; import { useSchemaForm } from "formvuelate"; export default defineComponent({ name: "GridTable", components: { BorderedButton, FilledButton, GridTableModal, }, props: { element: { required: true, type: Object as PropType<FormElement>, }, label: { required: false, type: String, default: "", }, required: { type: String, required: false, default: false, }, columns: { required: true, type: Array as PropType<TableHeader[]>, }, modelValue: { required: true, type: Array as PropType<any[]>, default: [], }, validation: { type: Object, default: () => ({}), }, }, emit: ["update:modelValue"], data() { const form = useSchemaForm(); const data = ref<any[]>(this.modelValue); const showAddRowModal = ref(false); return { data, showAddRowModal, form }; }, methods: { update() { this.$emit("update:modelValue", this.data); }, deleteItem(rowIndex: number) { this.data.splice(rowIndex, 1); this.update(); }, openAddRowModal() { this.showAddRowModal = true; }, rowSubmitted(form: any) { if (this.element?.datafield != undefined) { const rows = this.form.formModel.value[this.element?.datafield] || []; rows.push(form); this.form.formModel.value[this.element?.datafield] = rows; } }, }, watch: { modelValue() { this.data = this.modelValue; }, }, }); </script>
模态框组件
<template> <div class="backdrop modal fade show" id="common_modal_id" tabindex="-1" role="dialog" aria-labelledby="common_modal" aria-hidden="true" > <div class="modal-dialog modal-dialog-centered modal-lg" :class="{ fullscreen: isMobile }" role="document" > <div class="modal-content"> <div class="modal-body"> <SchemaFormWithValidation :schema="schema" schemaRowClasses="row" @submit="submitRow" > <template #afterForm> <button ref="submitBtn" v-show="false"></button> </template> </SchemaFormWithValidation> </div> <div class="modal-footer"> <BorderedButton :style="'secondary'" @click="close"> Cancel </BorderedButton> <FilledButton :style="'primary'" @click="addRow">Save</FilledButton> </div> </div> </div> </div> </template> <script lang="ts"> import { type PropType, defineComponent, ref, shallowRef } from "vue"; import type { FormElement } from "@interfaces"; import formBuilderService from "@/service/form-builder/form-builder-service"; import { SchemaFormFactory, useSchemaForm } from "formvuelate"; import VeeValidatePlugin from "@formvuelate/plugin-vee-validate"; import { FilledButton, BorderedButton } from "@components"; const SchemaFormWithValidation = SchemaFormFactory([ VeeValidatePlugin({ mapProps(validation: any) { return { validation, }; }, }), ]); export default defineComponent({ name: "GridTableModal", props: { editElements: { required: true, type: Array as PropType<FormElement[]>, default: [], }, }, components: { FilledButton, BorderedButton, SchemaFormWithValidation, }, setup() { let rowForm: any = useSchemaForm(); const submitBtn = ref(); return { rowForm, submitBtn, }; }, data() { this.resizeEventHandler(); const schema: any = shallowRef( formBuilderService.handleElements(this.editElements) ); return { schema }; }, methods: { close() { this.$emit("close"); }, addRow() { this.submitBtn.click(); }, submitRow() { this.$emit("submitRow", this.rowForm.formModel.value); }, }, }); </script>
解决方案
1. 阻止提交事件冒泡
修改模态框组件的submitRow方法,阻止事件冒泡避免触发主表单验证:
submitRow(event: Event) { event.stopPropagation(); this.$emit("submitRow", this.rowForm.formModel.value); }
同时确保模态框内的隐藏提交按钮类型为button:
<button ref="submitBtn" v-show="false" type="button"></button>
2. 独立控制模态框表单验证
利用vee-validate的独立表单实例,手动控制模态框的验证流程,不依赖主表单上下文:
修改模态框组件的setup和方法:
import { useForm } from 'vee-validate'; setup() { const rowForm: any = useSchemaForm(); const submitBtn = ref(); // 创建独立的vee-validate表单实例 const { validate, handleSubmit } = useForm(); // 自定义提交逻辑,仅验证当前模态框表单 const handleModalSubmit = handleSubmit((values) => { this.$emit("submitRow", values); this.close(); }); const addRow = async () => { // 仅验证模态框内的表单 const isValid = await validate(); if (isValid) { handleModalSubmit(); } }; return { rowForm, submitBtn, addRow }; }
同时删除模板中SchemaFormWithValidation的@submit绑定,因为已改用手动触发。
3. 全局状态管理解耦
通过全局状态(如Pinia/Vuex)管理模态框的显示状态和表单配置,让模态框完全独立于主表单:
- 在全局状态中定义
modalVisible、modalSchema等状态 - 表格组件点击“添加”时更新全局状态,触发模态框显示
- 模态框提交后更新表格数据,通过全局状态或事件通知表格组件刷新
内容的提问来源于stack exchange,提问作者Dean Strydom
相关产品推荐
相关产品推荐

