| 1 |
<template> |
| 2 |
<div |
| 3 |
ref="formWrapperRef" |
| 4 |
:class="['ivyforms-form-wrapper', 'ivyforms-form-wrapper-' + formData.id]" |
| 5 |
> |
| 6 |
<div v-if="showSuccessMessage" class="ivyforms-success-message"> |
| 7 |
{{ successMessage || getLabel('thank_you') }} |
| 8 |
</div> |
| 9 |
<!-- Form title--> |
| 10 |
<template v-else> |
| 11 |
<h2 |
| 12 |
v-if="formData.showTitle" |
| 13 |
:class="['ivyforms-form-title', 'ivyforms-form-title-' + formData.id]" |
| 14 |
> |
| 15 |
{{ formData.name }} |
| 16 |
</h2> |
| 17 |
<!-- Form description--> |
| 18 |
<p |
| 19 |
v-if="formData.showDescription" |
| 20 |
:class="['ivyforms-form-description', 'ivyforms-form-description-' + formData.id]" |
| 21 |
> |
| 22 |
{{ formData.description }} |
| 23 |
</p> |
| 24 |
|
| 25 |
<IvyForm |
| 26 |
:id="'ivyforms-form-' + formData.id" |
| 27 |
ref="formRef" |
| 28 |
:class="'ivyforms-form ivyforms-form-' + formData.id" |
| 29 |
:data-form-id="formData.id" |
| 30 |
:rules="formRules" |
| 31 |
:model="formValues" |
| 32 |
@submit.prevent="submitForm" |
| 33 |
> |
| 34 |
<template v-for="(field, index) in formFields" :key="index"> |
| 35 |
<component |
| 36 |
:is="formFieldsTemplates[field.type]" |
| 37 |
ref="formFieldsCollectorRefs" |
| 38 |
v-model="formValues[field.type + '_' + field.fieldIndex]" |
| 39 |
:field="field" |
| 40 |
:error="fieldErrors[field.type + '_' + field.fieldIndex]" |
| 41 |
></component> |
| 42 |
</template> |
| 43 |
<!-- Add hidden inputs fields for referer, post id ... --> |
| 44 |
<div class="ivyforms-form-submit"> |
| 45 |
<IvyButtonAction |
| 46 |
:id="'ivyforms-form-submit-button-' + formData.id" |
| 47 |
priority="tertiary" |
| 48 |
type="border" |
| 49 |
@click.prevent.stop="validateAndSubmit" |
| 50 |
> |
| 51 |
{{ getLabel('submit') }} |
| 52 |
</IvyButtonAction> |
| 53 |
</div> |
| 54 |
</IvyForm> |
| 55 |
</template> |
| 56 |
</div> |
| 57 |
</template> |
| 58 |
|
| 59 |
<script setup lang="ts"> |
| 60 |
import { ref, onMounted, markRaw, reactive } from 'vue' |
| 61 |
import axios from 'axios' |
| 62 |
|
| 63 |
// * Form Field templates |
| 64 |
import IvyTextFormField from '../public/fields/TextFormField.vue' |
| 65 |
import IvyParagraphFormField from '../public/fields/ParagraphFormField.vue' |
| 66 |
import IvyEmailFormField from '../public/fields/EmailFormField.vue' |
| 67 |
import IvyNumberFormField from '../public/fields/NumberFormField.vue' |
| 68 |
import IvyPhoneFormField from '../public/fields/PhoneFormField.vue' |
| 69 |
import type { Field } from '@/types/field' |
| 70 |
import type { Confirmation } from '@/types/confirmations' |
| 71 |
import { useLabels } from '@/composables/useLabels' |
| 72 |
const { getLabel } = useLabels() |
| 73 |
|
| 74 |
// Form data |
| 75 |
const formFields = ref([]) |
| 76 |
const formValues = ref({}) |
| 77 |
const formRules = ref({}) |
| 78 |
const fieldErrors = ref({}) |
| 79 |
const isLoading = ref(true) |
| 80 |
const formRef = ref(null) |
| 81 |
const formWrapperRef = ref(null) |
| 82 |
const formNonce = ref(null) |
| 83 |
const formConfirmation = ref([]) |
| 84 |
const showSuccessMessage = ref(false) |
| 85 |
const successMessage = ref('') |
| 86 |
|
| 87 |
interface FormData { |
| 88 |
id: string | number |
| 89 |
name?: string |
| 90 |
description?: string |
| 91 |
showTitle?: boolean |
| 92 |
showDescription?: boolean |
| 93 |
defaultValue?: string |
| 94 |
counter?: number |
| 95 |
fields?: Field[] |
| 96 |
confirmations?: Confirmation[] |
| 97 |
} |
| 98 |
// Update your refs with proper typing |
| 99 |
const formData = ref<FormData>({ |
| 100 |
id: '', |
| 101 |
name: '', |
| 102 |
description: '', |
| 103 |
showTitle: true, |
| 104 |
showDescription: false, |
| 105 |
defaultValue: '', |
| 106 |
counter: 0, |
| 107 |
fields: [], |
| 108 |
confirmations: [], |
| 109 |
}) |
| 110 |
// * Form Fields Object |
| 111 |
const formFieldsTemplates = reactive({ |
| 112 |
text: markRaw(IvyTextFormField), |
| 113 |
email: markRaw(IvyEmailFormField), |
| 114 |
textarea: markRaw(IvyParagraphFormField), |
| 115 |
number: markRaw(IvyNumberFormField), |
| 116 |
phone: markRaw(IvyPhoneFormField), |
| 117 |
}) |
| 118 |
|
| 119 |
// Initialize from global variable |
| 120 |
onMounted(async () => { |
| 121 |
if (typeof window !== 'undefined' && window.wpIvyFormDataList) { |
| 122 |
const counter = formWrapperRef.value.closest('.ivyforms-frontend').dataset.counter |
| 123 |
const formsData = window.wpIvyFormDataList[counter] |
| 124 |
// Check if formData is an object and has the required properties |
| 125 |
if (typeof formsData === 'object' && formsData !== null && formsData.id) { |
| 126 |
// Assign the form data to the reactive variable |
| 127 |
formData.value = formsData.form |
| 128 |
formNonce.value = formsData.nonce |
| 129 |
formFields.value = formsData.fields || [] |
| 130 |
formConfirmation.value = formsData.confirmations || [] |
| 131 |
|
| 132 |
// Initialize form values with defaults or empty strings |
| 133 |
formFields.value.forEach((field) => { |
| 134 |
// Initialize field errors |
| 135 |
const key = field.type + '_' + field.fieldIndex |
| 136 |
formValues.value[key] = field.defaultValue || '' |
| 137 |
|
| 138 |
const rules = [] |
| 139 |
|
| 140 |
// Required rule |
| 141 |
if (field.required) { |
| 142 |
rules.push({ |
| 143 |
required: true, |
| 144 |
message: getRequiredMessage(field), |
| 145 |
trigger: 'submit', |
| 146 |
}) |
| 147 |
} |
| 148 |
|
| 149 |
// Format validations |
| 150 |
if (field.type === 'email') { |
| 151 |
rules.push({ |
| 152 |
type: 'email', |
| 153 |
message: getLabel('enter_email_address'), |
| 154 |
trigger: ['submit'], |
| 155 |
}) |
| 156 |
} |
| 157 |
|
| 158 |
if (field.type === 'phone') { |
| 159 |
rules.push({ |
| 160 |
pattern: /^[+\d]?(?:[\d\s.-]{7,15})$/, |
| 161 |
message: getLabel('enter_phone_number'), |
| 162 |
trigger: ['submit'], |
| 163 |
}) |
| 164 |
} |
| 165 |
|
| 166 |
formRules.value[key] = rules |
| 167 |
}) |
| 168 |
// Form is loaded, remove loader |
| 169 |
isLoading.value = false |
| 170 |
// add vanila js remove class from loader |
| 171 |
const loader = document.querySelector('.ivyforms-skeleton-' + counter) |
| 172 |
if (loader) { |
| 173 |
loader.remove() |
| 174 |
} |
| 175 |
} else { |
| 176 |
console.error(getLabel('invalid_form_data'), formData) |
| 177 |
} |
| 178 |
} else { |
| 179 |
console.error(getLabel('form_data_not_found')) |
| 180 |
// Even with error, don't show loader anymore |
| 181 |
isLoading.value = false |
| 182 |
} |
| 183 |
}) |
| 184 |
|
| 185 |
function getRequiredMessage(field: Field): string { |
| 186 |
return field.label?.trim() ? `${field.label} ${getLabel('this_field')}` : getLabel('required') |
| 187 |
} |
| 188 |
|
| 189 |
// Validate form and submit |
| 190 |
const validateAndSubmit = async () => { |
| 191 |
if (!formRef.value) return |
| 192 |
|
| 193 |
await formRef.value.validate((valid: boolean, errors: Record<string, { message: string }[]>) => { |
| 194 |
if (valid) { |
| 195 |
submitForm() |
| 196 |
} else { |
| 197 |
fieldErrors.value = {} // Clear previous |
| 198 |
Object.keys(errors).forEach((fieldKey) => { |
| 199 |
fieldErrors.value[fieldKey] = errors[fieldKey][0]?.message || getLabel('invalid_input') |
| 200 |
}) |
| 201 |
} |
| 202 |
}) |
| 203 |
} |
| 204 |
|
| 205 |
// Submit form |
| 206 |
const submitForm = async () => { |
| 207 |
try { |
| 208 |
const response = await axios.post( |
| 209 |
// eslint-disable-next-line no-undef |
| 210 |
`${wpIvyApiSettings.root}ivyforms/v1/form/submission/`, |
| 211 |
{ |
| 212 |
formId: formData.value.id, |
| 213 |
values: formValues.value, |
| 214 |
nonce: formNonce.value, |
| 215 |
}, |
| 216 |
{ |
| 217 |
headers: { |
| 218 |
'Content-Type': 'application/json', |
| 219 |
// eslint-disable-next-line no-undef |
| 220 |
'X-WP-Nonce': wpIvyApiSettings.nonce, |
| 221 |
}, |
| 222 |
}, |
| 223 |
) |
| 224 |
// Handle response |
| 225 |
if (response.status !== 200) { |
| 226 |
console.error(getLabel('error_submitting_form'), response) |
| 227 |
} |
| 228 |
|
| 229 |
console.log(getLabel('form_submitted')) |
| 230 |
// Reset form or show success message |
| 231 |
formValues.value = {} |
| 232 |
const type = formConfirmation.value[0].type |
| 233 |
const status = formConfirmation.value[0].status |
| 234 |
|
| 235 |
if (!status) return |
| 236 |
|
| 237 |
if (type === 'successMessage') { |
| 238 |
showSuccessMessage.value = true |
| 239 |
successMessage.value = formConfirmation.value[0].message || getLabel('thank_you') |
| 240 |
return |
| 241 |
} else if (type === 'redirectToPage' && formConfirmation.value[0].page) { |
| 242 |
const pageId = formConfirmation.value[0].page |
| 243 |
if (pageId) { |
| 244 |
const pageUrl = `${window.location.origin}/?p=${pageId}` |
| 245 |
window.location.href = pageUrl |
| 246 |
} |
| 247 |
} else if (type === 'redirectToCustomUrl' && formConfirmation.value[0].url) { |
| 248 |
window.location.href = formConfirmation.value[0].url |
| 249 |
} |
| 250 |
} catch (error) { |
| 251 |
console.error(getLabel('error_submitting_form'), error) |
| 252 |
// Show error message |
| 253 |
} |
| 254 |
} |
| 255 |
</script> |
| 256 |
|
| 257 |
<style lang="scss" scoped> |
| 258 |
.ivyforms-form-wrapper { |
| 259 |
width: 100%; |
| 260 |
max-width: 720px; |
| 261 |
margin: 0 auto; |
| 262 |
|
| 263 |
.ivyforms-form-title { |
| 264 |
font-size: 24px; |
| 265 |
font-weight: 600; |
| 266 |
margin-bottom: 12px; |
| 267 |
color: var(--map-base-text-0); |
| 268 |
} |
| 269 |
|
| 270 |
.ivyforms-form-description { |
| 271 |
font-size: 16px; |
| 272 |
margin-bottom: 24px; |
| 273 |
color: var(--map-base-text-1); |
| 274 |
} |
| 275 |
|
| 276 |
.ivyforms-form-field { |
| 277 |
margin-bottom: 16px; |
| 278 |
} |
| 279 |
|
| 280 |
.ivyforms-form-submit { |
| 281 |
margin-top: 24px; |
| 282 |
} |
| 283 |
::v-deep(.ivyforms-success-message) { |
| 284 |
font-size: 20px; |
| 285 |
color: var(--map-base-brand-symbol-0); |
| 286 |
padding: 20px; |
| 287 |
background-color: var(--map-base-brand-o05); |
| 288 |
border: 1px solid var(--map-base-brand-symbol-0); |
| 289 |
border-radius: 8px; |
| 290 |
text-align: center; |
| 291 |
} |
| 292 |
} |
| 293 |
</style> |
| 294 |
|