Vue.js 3 how to handle Formvuelate nested forms?

22 Views Asked by At

I am using Formvuelate with the vee-validate plugin https://formvuelate.js.org/

I had to make a custom form build service that takes in data and returns a schema of the components to generate a form with validation using the SchemaFormWithValidation.

Main form builder

<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>

One of these form components is a table component that has a add button to open a modal that contains a form to add a row. This is a problem as you can't have a form nested inside a form.

You will see in the code above the commented out teleport is what I used first. By having the modal outside the from I can handle it as a separate form and this worked fine if I extract the table components and render them outside the SchemaFormWithValidation inorder to emit from it. The problem I have however is some of the form components have children which is a list of more form components that only appear when the parent has a value. Unfortunately this can include a table which means the table will have to be rendered inside SchemaFormWithValidation and I can't emit from it. At least I don't know how to.

If it is generated in the main form then I can't submit the modal form to trigger validation as it is seen as part of the main form and will trigger validation for the main form too. I need to either manually trigger validation for only the modal form part or some how emit an action from the main form through the table component or somehow decouple the modal from the parent so it is not seen as part of the parent as teleport does not do this.

Table component

<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>

Modal

<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>

0

There are 0 best solutions below