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

The Innovative Form Builder – IvyForms, version 0.3. 457 lines.

- Page: https://pluginprobe.com/plugins/ivyforms/0.3/code/frontend/src/stores/useFormBuilder.ts
- Raw: https://pluginprobe.com/plugins/ivyforms/0.3/raw/frontend/src/stores/useFormBuilder.ts
- Modified: 2025-08-20T10:20:12+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.3/code/frontend/src/stores/useFormBuilder.ts#L10-L20`.

```typescript
import { defineStore } from 'pinia'
import { ref } from 'vue'
import type { Field } from '@/types/field'
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'

export const useFormBuilder = defineStore('formBuilderData', () => {
  const formId = ref<string | null>(null)
  const isEditing = ref(false)
  const fields = ref([])
  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 isEditingSettingGeneral = ref(false)
  const confirmationId = ref<string | null>(null)
  const router = useRouter()

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

  // Use unsaved changes store for state management
  const unsavedChangesStore = useUnsavedChangesStore()

  // Set selected field and update the active tab
  const selectField = (fieldIndex: number) => {
    selectedField.value =
      fields.value.find((field: Field) => field.fieldIndex === fieldIndex) || null
    activeTab.value = 'options'
  }

  // Create complete entity manager for form builder
  const entityManager = unsavedChangesStore.createEntityManager('formBuilder', {
    name,
    description,
    published,
    showTitle,
    showDescription,
    storeEntries,
    fields,
  })

  // Deselect field when clicking outside of it
  function deselectField(event: Event) {
    event.stopPropagation() // Prevent event from bubbling up to the document
    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
      }
    })

    // Only deselect if clicking outside a field
    if (!clickedInsideField) {
      selectedField.value = null
    }
  }

  // Check if the field is selected based on its index
  const isSelected = (fieldIndex: number) => {
    return selectedField.value?.fieldIndex === fieldIndex
  }

  // Update the selected field and the store
  const updateSelectedField = (key: string, value: number) => {
    // Find the index of the field in the store and update it
    const index = fields.value.findIndex((f) => f.fieldIndex === selectedField.value?.fieldIndex)
    if (index !== -1) {
      fields.value[index][key] = value
    }
  }

  // Add a new field when an item is dropped or clicked
  const addField = (fieldData: {
    id: number
    fieldIndex: number
    type: string
    label: string
    defaultValue?: string
    required?: boolean
    placeholder?: string | number
    position?: number
  }) => {
    const newPosition = fieldData.position ?? fields.value.length + 1

    fields.value.push({
      id: fieldData.id,
      fieldIndex: fieldData.fieldIndex,
      type: fieldData.type,
      label: fieldData.label,
      defaultValue: '',
      required: fieldData.required || false,
      placeholder: fieldData.placeholder || '',
      position: newPosition,
    })

    updateFieldPositions() // Ensure positions remain sequential
  }

  // Function to update positions after reordering
  const updateFieldPositions = () => {
    fields.value.forEach((field, fieldIndex) => {
      field.position = fieldIndex + 1
    })
  }

  // Increase the counter for new fields
  const increaseCounter = () => {
    counterFields.value++
    return counterFields.value
  }
  // TODO add Internalization for all strings in notifications
  // Load existing form data if formId is provided
  const loadForm = async (id: string, options?: { normalize?: boolean; silent?: boolean }) => {
    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
        confirmationId.value = responseData.confirmationId || null

        // Set page title to the loaded form name
        if (name.value) {
          globalState.setPageTitle(name.value)
        }

        const fieldsArray = responseData.fields
        fields.value = []
        counterFields.value = 0
        fields.value = fieldsArray.map((field: Field, arrayIndex: number) => {
          const newFieldIndex = ++counterFields.value
          return {
            id: field.id,
            fieldIndex: newFieldIndex, // Use regenerated fieldIndex instead of database value
            type: field.type,
            label: field.label,
            defaultValue: field.defaultValue || '',
            required: field.required || false,
            placeholder: field.placeholder || '',
            position: field.position || arrayIndex + 1,
          }
        })

        entityManager.resetUnsavedChanges()

        // Set counterFields to the highest fieldIndex value for future fields
        counterFields.value = Math.max(...fields.value.map((field: Field) => field.fieldIndex))

        // Optionally persist normalized state silently
        if (options?.normalize) {
          await updateForm({ silent: options.silent ?? true })
        }
      } 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)
    }
  }

  // Update form after clicking update button
  const updateForm = async (options?: { silent?: boolean }) => {
    if (!formId.value) return
    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,
      fields: fields.value ? fields.value : [],
    }

    try {
      const { data, error, status } = await request(`form/update/${formId.value}/`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        data: formData,
      })
      if (!error && status === 200) {
        if (!options?.silent) {
          IvyMessage({
            message: getLabel('form_updated'),
            type: 'success',
          })
        }
        entityManager.resetUnsavedChanges()
      } else {
        if (!options?.silent) {
          IvyMessage({
            message: getLabel('failed_to_update_form'),
            type: 'error',
          })
        }
        console.error(getLabel('failed_to_update_form'), data?.message)
      }
    } catch (error) {
      if (!options?.silent) {
        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 }) => {
    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,
      fields: fields.value.map((field) => ({
        id: field.id,
        fieldIndex: field.fieldIndex,
        type: field.type,
        label: field.label,
        required: field.required,
        defaultValue: field.defaultValue,
        placeholder: field.placeholder,
        position: field.position,
      })),
      ...(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',
      })
    }
  }

  // Save form settings
  const saveFormSettings = async () => {
    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: fields.value.map((field) => ({
        id: field.id,
        fieldIndex: field.fieldIndex,
        type: field.type,
        label: field.label,
        required: field.required,
        defaultValue: field.defaultValue,
        placeholder: field.placeholder,
        position: field.position,
      })),
    }

    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()
    }
  }

  // Delete a field by index
  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()
    }
  }

  // Duplicate a field
  const duplicateField = (fieldIndex: number) => {
    const original = fields.value.find((f) => f.fieldIndex === fieldIndex)
    if (!original) return

    const newIndex = increaseCounter()

    const copy = {
      ...JSON.parse(JSON.stringify(original)),
      id: 0,
      fieldIndex: newIndex,
      label: original.label + ' (copy)',
    }

    const insertAt = fields.value.findIndex((f) => f.fieldIndex === fieldIndex)
    fields.value.splice(insertAt + 1, 0, copy)
    updateFieldPositions()
  }

  // Reset form to default values
  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
  }

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

```
