You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 03:12:05