# ivyforms/0.6/frontend/src/stores/useFormBuilder.ts

The Innovative Form Builder – IvyForms, version 0.6. 456 lines.

- Page: https://pluginprobe.com/plugins/ivyforms/0.6/code/frontend/src/stores/useFormBuilder.ts
- Raw: https://pluginprobe.com/plugins/ivyforms/0.6/raw/frontend/src/stores/useFormBuilder.ts
- Modified: 2025-11-19T18:42:10+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/ivyforms/0.6/code/frontend/src/stores/useFormBuilder.ts#L10-L20`.

```typescript
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { useLabels } from '@/composables/useLabels'
import IvyMessage from '@/views/_components/message/ivyMessage.ts'
import { useRouter } from 'vue-router'
import { useUnsavedChangesStore } from '@/stores/useUnsavedChangesStore'
import { useApiClient } from '@/composables/useApiClient'
import { useGlobalState } from '@/stores/useGlobalState'
import type { Field } from '@/types/field'
import { normalizeFieldsForBuilder, denormalizeFieldsForApi } from '@/utils/fieldsNormalization'
import { buildNewField } from '@/utils/addFieldUtil.ts'
import { duplicateFieldForStore } from '@/utils/fieldUtils'

export const useFormBuilder = defineStore('formBuilderData', () => {
  const formId = ref<string | null>(null)
  const isEditing = ref(false)
  const fields = ref<Field[]>([])
  const counterFields = ref<number>(0)
  const selectedField = ref<Field | null>(null)
  const activeTab = ref<string>('addField')
  const description = ref<string | null>(null)
  const name = ref<string | null>('Blank form')
  const published = ref(true)
  const showTitle = ref(false)
  const showDescription = ref(false)
  const storeEntries = ref(false)
  const integrationSettings = ref<{
    wpdatatables: { enabled: boolean }
    [key: string]: Record<string, unknown>
  }>({
    wpdatatables: { enabled: true },
  })
  const isEditingSettingGeneral = ref(false)
  const confirmationId = ref<string | null>(null)
  const router = useRouter()
  const isFormLoading = ref(false)

  const { getLabel } = useLabels()
  const { request } = useApiClient()
  const unsavedChangesStore = useUnsavedChangesStore()

  const selectField = (fieldIndex: number) => {
    const field = fields.value.find((field) => field.fieldIndex === fieldIndex) || null

    // If selecting the same field, still ensure the tab is set to options
    if (selectedField.value?.fieldIndex === fieldIndex) {
      activeTab.value = 'options'
      return
    }

    selectedField.value = field
    activeTab.value = 'options'
  }
  const entityManager = unsavedChangesStore.createEntityManager('formBuilder', {
    name,
    description,
    published,
    showTitle,
    showDescription,
    storeEntries,
    integrationSettings,
    fields,
  })

  function deselectField(event: Event) {
    event.stopPropagation()
    const clickedElement = event.target as HTMLElement
    const fieldElements = document.querySelectorAll('.ivyforms-form-builder__list-group__item')
    let clickedInsideField = false
    fieldElements.forEach((el) => {
      if (el.contains(clickedElement)) {
        clickedInsideField = true
      }
    })
    if (!clickedInsideField) {
      selectedField.value = null
    }
  }

  const isSelected = (fieldIndex: number) => {
    return selectedField.value?.fieldIndex === fieldIndex
  }

  // Only update the selectedField object and the canonical entry in fields[]
  const updateSelectedField = (key: string, value: unknown) => {
    const sel = selectedField.value as Record<string, unknown> | null
    if (sel) {
      sel[key] = value
    }
    const index = fields.value.findIndex((f) => f.fieldIndex === selectedField.value?.fieldIndex)
    if (index !== -1) {
      const target = fields.value[index] as Record<string, unknown>
      if (target !== sel) {
        target[key] = value
      }
    }
  }

  function addField(fieldData: Partial<Field> & { type: string }) {
    fields.value.push(buildNewField(fieldData, counterFields.value, getLabel))
    updateFieldPositions()
  }

  const updateFieldPositions = () => {
    fields.value.forEach((field, fieldIndex) => {
      field.position = fieldIndex + 1
    })
  }

  const increaseCounter = () => {
    counterFields.value++
    return counterFields.value
  }

  /**
   * Apply beforeSaveFields hooks to allow Pro or other extensions to modify fields
   * @param fieldsArray - The fields array to process
   * @returns Processed fields array after all hooks have been applied
   */
  const applyBeforeSaveFieldsHooks = (fieldsArray: Field[]): Field[] => {
    let fieldsToSave = [...fieldsArray]

    if (typeof window !== 'undefined') {
      const w = window as Window & {
        IvyForms?: {
          hooks?: {
            beforeSaveFields?: Array<(fields: Field[]) => Field[]>
          }
        }
      }
      const hooks = w.IvyForms?.hooks?.beforeSaveFields || []
      hooks.forEach((hook) => {
        try {
          fieldsToSave = hook(fieldsToSave) as Field[]
        } catch (error) {
          console.error(getLabel('error_in_before_save_field_hook'), error)
        }
      })
    }

    return fieldsToSave
  }

  // Load existing form data if formId is provided
  const loadForm = async (id: string) => {
    isFormLoading.value = true
    const globalState = useGlobalState()

    try {
      const { data, error, status } = await request(`form/${id}/`, {
        method: 'GET',
        headers: {
          'Content-Type': 'application/json',
        },
      })
      if (!error && status === 200 && data?.data?.data) {
        formId.value = id
        isEditing.value = true
        isEditingSettingGeneral.value = true
        const responseData = data.data.data
        name.value = responseData.name || getLabel('blank_form')
        description.value = responseData.description || ''
        published.value = responseData.published || false
        showTitle.value = responseData.showTitle || false
        showDescription.value = responseData.showDescription || false
        storeEntries.value = responseData.storeEntries

        // Load integration settings from the response
        if (responseData.integrationSettings) {
          integrationSettings.value = responseData.integrationSettings
        }

        confirmationId.value = responseData.confirmationId || null

        const rawFields = (responseData.fields || []) as Field[]
        fields.value = normalizeFieldsForBuilder(rawFields)

        if (name.value) {
          globalState.setPageTitle(name.value)
        }
        entityManager.resetUnsavedChanges()
        counterFields.value = fields.value.length
          ? Math.max(...fields.value.map((f) => f.fieldIndex))
          : 0
      } else {
        IvyMessage({
          message: getLabel('failed_to_tab_form'),
          type: 'error',
        })
        console.error(getLabel('failed_to_tab_form'), data?.message)
      }
    } catch (error) {
      IvyMessage({
        message: getLabel('error_loading_form'),
        type: 'error',
      })
      console.error(getLabel('error_loading_form'), error)
    } finally {
      isFormLoading.value = false
    }
  }

  const updateForm = async () => {
    if (!formId.value) return

    // Allow Pro or other extensions to modify fields before saving
    const fieldsToSave = applyBeforeSaveFieldsHooks(fields.value)

    const denormalizedFields = denormalizeFieldsForApi(fieldsToSave)

    const formData = {
      name: name.value || getLabel('updated_form'),
      description: description.value || '',
      published: published.value || false,
      showTitle: showTitle.value || false,
      showDescription: showDescription.value || false,
      storeEntries: storeEntries.value,
      integrationSettings: integrationSettings.value,
      fields: denormalizedFields,
    }

    try {
      const { data, error, status } = await request(`form/update/${formId.value}/`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        data: formData,
      })
      if (!error && status === 200) {
        IvyMessage({
          message: getLabel('form_updated'),
          type: 'success',
        })
        entityManager.resetUnsavedChanges()
      } else {
        IvyMessage({
          message: getLabel('failed_to_update_form'),
          type: 'error',
        })
        console.error(getLabel('failed_to_update_form'), data?.message)
      }
    } catch (error) {
      IvyMessage({
        message: getLabel('error_updating_form'),
        type: 'error',
      })
      console.error(getLabel('error_updating_form'), error)
    }
  }

  // Save form using WordPress REST API
  const saveForm = async (options?: { template_id?: string }) => {
    // Allow Pro or other extensions to modify fields before saving
    const fieldsToSave = applyBeforeSaveFieldsHooks(fields.value)

    const denormalizedFields = denormalizeFieldsForApi(fieldsToSave)
    const formData = {
      name: name.value || getLabel('blank_form'),
      description: description.value || '',
      showTitle: showTitle.value || false,
      published: published.value || false,
      showDescription: showDescription.value || false,
      storeEntries: storeEntries.value,
      integrationSettings: integrationSettings.value,
      fields: denormalizedFields,
      ...(options?.template_id && { template_id: options.template_id }),
    }

    try {
      const { data, error, status } = await request('form/add/', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        data: formData,
      })
      if (!error && status === 200 && data?.data?.data?.id) {
        const responseData = data.data.data
        formId.value = responseData.id
        confirmationId.value = responseData.confirmationId
        isEditing.value = true
        isEditingSettingGeneral.value = true
        await router.push({ hash: `manage/${formId.value}` })
        IvyMessage({
          message: getLabel('form_saved'),
          type: 'success',
        })
        entityManager.resetUnsavedChanges()
      } else {
        IvyMessage({
          message: getLabel('failed_to_save_form'),
          type: 'error',
        })
      }
    } catch (error) {
      IvyMessage({
        message: `${getLabel('error_save_form')} ${error}`,
        type: 'error',
      })
    }
  }

  const saveFormSettings = async () => {
    // Allow Pro or other extensions to modify fields before saving
    const fieldsToSave = applyBeforeSaveFieldsHooks(fields.value)

    const formData = {
      name: name.value || getLabel('blank_form'),
      description: description.value || '',
      published: published.value || false,
      showTitle: showTitle.value || false,
      showDescription: showDescription.value || false,
      storeEntries: storeEntries.value,
      fields: denormalizeFieldsForApi(fieldsToSave),
    }
    try {
      const { data, error, status } = await request('form/add/', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        data: formData,
      })
      if (!error && status === 200 && data?.data?.data?.id) {
        formId.value = data.data.data.id
        isEditing.value = true
        isEditingSettingGeneral.value = true
        await router.push({ hash: `/settings/general/manage/${formId.value}` })
        IvyMessage({
          message: getLabel('form_saved'),
          type: 'success',
        })
        entityManager.resetUnsavedChanges()
      } else {
        IvyMessage({
          message: getLabel('failed_to_save_form'),
          type: 'error',
        })
      }
    } catch (error) {
      IvyMessage({
        message: `${getLabel('error_save_form')} ${error}`,
        type: 'error',
      })
    }
  }

  const moveFieldUp = (fieldIndex: number) => {
    const currentIndex = fields.value.findIndex((f) => f.fieldIndex === fieldIndex)
    if (currentIndex > 0) {
      const temp = fields.value[currentIndex]
      fields.value[currentIndex] = fields.value[currentIndex - 1]
      fields.value[currentIndex - 1] = temp
      updateFieldPositions()
    }
  }

  const moveFieldDown = (fieldIndex: number) => {
    const currentIndex = fields.value.findIndex((f) => f.fieldIndex === fieldIndex)
    if (currentIndex < fields.value.length - 1) {
      const temp = fields.value[currentIndex]
      fields.value[currentIndex] = fields.value[currentIndex + 1]
      fields.value[currentIndex + 1] = temp
      updateFieldPositions()
    }
  }

  const deleteField = (fieldIndex: number) => {
    const i = fields.value.findIndex((f) => f.fieldIndex === fieldIndex)
    if (i !== -1) {
      fields.value.splice(i, 1)
      if (selectedField.value?.fieldIndex === fieldIndex) {
        selectedField.value = null
      }
      updateFieldPositions()
    }
  }

  const duplicateField = (fieldIndex: number) => {
    const original = fields.value.find((f) => f.fieldIndex === fieldIndex)
    if (!original) return
    const newIndex = increaseCounter()
    const copy = duplicateFieldForStore(original, newIndex, getLabel)
    const insertAt = fields.value.findIndex((f) => f.fieldIndex === fieldIndex)
    fields.value.splice(insertAt + 1, 0, copy)
    updateFieldPositions()
  }

  const resetForm = () => {
    formId.value = null
    isEditing.value = false
    fields.value = []
    counterFields.value = 0
    selectedField.value = null
    activeTab.value = 'addField'
    description.value = null
    name.value = 'Blank form'
    published.value = true
    showTitle.value = true
    showDescription.value = false
    storeEntries.value = true
    isEditingSettingGeneral.value = false
    confirmationId.value = null
    isFormLoading.value = false
  }

  const updateFieldOptions = (key: string, value: unknown) => {
    if (!selectedField.value) return
    const index = fields.value.findIndex((f) => f.fieldIndex === selectedField.value?.fieldIndex)
    if (index !== -1) {
      // Mutate the existing object instead of creating a new one
      // This keeps the reference intact for selectedField
      const target = fields.value[index] as Record<string, unknown>
      target[key] = value
    }
  }

  return {
    formId,
    fields,
    isEditing,
    selectedField,
    activeTab,
    counterFields,
    description,
    name,
    published,
    showTitle,
    showDescription,
    storeEntries,
    integrationSettings,
    isEditingSettingGeneral,
    confirmationId,
    isFormLoading,
    getCurrentState: entityManager.getCurrentState,
    saveForm,
    loadForm,
    updateForm,
    increaseCounter,
    addField,
    selectField,
    deselectField,
    isSelected,
    updateSelectedField,
    updateFieldOptions,
    updateFieldPositions,
    moveFieldUp,
    moveFieldDown,
    deleteField,
    duplicateField,
    saveFormSettings,
    resetForm,
  }
})

```
