PluginProbe
The Innovative Form Builder – IvyForms / 0.3.1
The Innovative Form Builder – IvyForms v0.3.1
1.5.1 1.5 1.4.1 1.4 trunk 0.1.2 0.2 0.2.1 0.3 0.3.1 0.4 0.5 0.6 0.6.1 0.6.1-backup 0.6.1.1 0.7 0.8 0.8.1 0.8.2 0.9 0.9.1 1.0 1.1 1.1.1 All 27 releases
ivyforms / frontend / src / views / public / FormRender.vue

FormRender.vue in The Innovative Form Builder – IvyForms 0.3.1, at frontend/src/views/public/FormRender.vue

410 lines 12.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 <div v-if="showSuccessMessageReset" class="ivyforms-success-message ivyforms-mb-24">
12 {{ successMessage || getLabel('thank_you') }}
13 </div>
14
15 <h2
16 v-if="formData.showTitle"
17 :class="['ivyforms-form-title', 'ivyforms-form-title-' + formData.id]"
18 >
19 {{ formData.name }}
20 </h2>
21 <!-- Form description-->
22 <p
23 v-if="formData.showDescription"
24 :class="['ivyforms-form-description', 'ivyforms-form-description-' + formData.id]"
25 >
26 {{ formData.description }}
27 </p>
28
29 <IvyForm
30 :id="'ivyforms-form-' + formData.id"
31 ref="formRef"
32 :class="'ivyforms-form ivyforms-form-' + formData.id"
33 :data-form-id="formData.id"
34 :rules="formRules"
35 :model="formValues"
36 @submit.prevent="submitForm"
37 >
38 <template v-for="field in formFields" :key="field.type + '_' + field.fieldIndex">
39 <component
40 :is="formFieldsTemplates[field.type]"
41 ref="formFieldsCollectorRefs"
42 v-model="formValues[field.type + '_' + field.fieldIndex]"
43 :field="field"
44 :error="fieldErrors[field.type + '_' + field.fieldIndex]"
45 ></component>
46 </template>
47 <!-- Add hidden inputs fields for referer, post id ... -->
48 <div class="ivyforms-form-submit">
49 <IvyButtonAction
50 :id="'ivyforms-form-submit-button-' + formData.id"
51 priority="tertiary"
52 type="border"
53 @click.prevent.stop="validateAndSubmit"
54 >
55 {{ getLabel('submit') }}
56 </IvyButtonAction>
57 </div>
58 </IvyForm>
59 </template>
60 </div>
61 </template>
62
63 <script setup lang="ts">
64 import { ref, onMounted, markRaw, reactive, provide, type ComponentPublicInstance } from 'vue'
65 import IvyForm from '@/views/_components/form/IvyForm.vue'
66 import IvyButtonAction from '@/views/_components/button/IvyButtonAction.vue'
67 import { useApiClient } from '@/composables/useApiClient'
68
69 interface RecaptchaFieldComponent extends ComponentPublicInstance {
70 $props: {
71 field: {
72 type: string
73 fieldIndex: number
74 }
75 }
76 executeRecaptcha?: () => Promise<string>
77 }
78
79 // * Form Field templates
80 import IvyTextFormField from '../public/fields/TextFormField.vue'
81 import IvyParagraphFormField from '../public/fields/ParagraphFormField.vue'
82 import IvyEmailFormField from '../public/fields/EmailFormField.vue'
83 import IvyNumberFormField from '../public/fields/NumberFormField.vue'
84 import IvyPhoneFormField from '../public/fields/PhoneFormField.vue'
85 import IvyWebsiteFormField from '../public/fields/WebsiteFormField.vue'
86 import RecaptchaFormField from '../public/fields/RecaptchaFormField.vue'
87 import type { Field } from '@/types/field'
88 import type { Confirmation } from '@/types/confirmations'
89 import { useLabels } from '@/composables/useLabels'
90 import IvyMessage from '@/views/_components/message/ivyMessage.ts'
91 import utilRedirect from '@/utils/utilRedirects'
92 import { useRouter } from 'vue-router'
93
94 const { getLabel } = useLabels()
95 const { request } = useApiClient()
96 const { redirectToUrl } = utilRedirect()
97 const router = useRouter()
98
99 // Form data
100 const formFields = ref([])
101 const formValues = ref({})
102 const formRules = ref({})
103 const fieldErrors = ref({})
104 const isLoading = ref(true)
105 const formRef = ref(null)
106 const formWrapperRef = ref(null)
107 const formFieldsCollectorRefs = ref([])
108 const formNonce = ref(null)
109 const formConfirmation = ref([])
110 const showSuccessMessage = ref(false)
111 const showSuccessMessageReset = ref(false)
112 const successMessage = ref('')
113
114 interface FormData {
115 id: string | number
116 name?: string
117 description?: string
118 showTitle?: boolean
119 showDescription?: boolean
120 storeEntries?: boolean
121 defaultValue?: string
122 counter?: number
123 fields?: Field[]
124 confirmations?: Confirmation[]
125 }
126 // Update your refs with proper typing
127 const formData = ref<FormData>({
128 id: '',
129 name: '',
130 description: '',
131 showTitle: true,
132 showDescription: false,
133 storeEntries: true,
134 defaultValue: '',
135 counter: 0,
136 fields: [],
137 confirmations: [],
138 })
139 // * Form Fields Object
140 const formFieldsTemplates = reactive({
141 text: markRaw(IvyTextFormField),
142 email: markRaw(IvyEmailFormField),
143 textarea: markRaw(IvyParagraphFormField),
144 number: markRaw(IvyNumberFormField),
145 phone: markRaw(IvyPhoneFormField),
146 website: markRaw(IvyWebsiteFormField),
147 recaptcha: markRaw(RecaptchaFormField),
148 })
149
150 // Initialize from global variable
151 onMounted(async () => {
152 if (typeof window !== 'undefined' && window.wpIvyFormDataList) {
153 const counter = formWrapperRef.value.closest('.ivyforms-frontend').dataset.counter
154 const formsData = window.wpIvyFormDataList[counter]
155 // Check if formData is an object and has the required properties
156 if (typeof formsData === 'object' && formsData !== null && formsData.id) {
157 // Assign the form data to the reactive variable
158 formData.value = formsData.form
159 formNonce.value = formsData.nonce
160 formFields.value = formsData.fields || []
161 formConfirmation.value = formsData.confirmations || []
162
163 // Initialize form values with defaults or empty strings
164 formFields.value.forEach((field) => {
165 // Initialize field errors
166 const key = field.type + '_' + field.fieldIndex
167 formValues.value[key] = field.defaultValue || ''
168
169 const rules = []
170
171 // Required rule
172 if (field.required) {
173 rules.push({
174 required: true,
175 message: getRequiredMessage(field),
176 trigger: 'submit',
177 })
178 }
179
180 // Format validations
181 if (field.type === 'email') {
182 rules.push({
183 type: 'email',
184 message: getLabel('enter_email_address'),
185 trigger: ['submit'],
186 })
187 }
188
189 if (field.type === 'phone') {
190 rules.push({
191 pattern: /^[+\d]?(?:[\d\s.-]{7,15})$/,
192 message: getLabel('enter_phone_number'),
193 trigger: ['submit'],
194 })
195 }
196
197 if (field.type === 'website') {
198 rules.push({
199 pattern: /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([/\w .-]*)*\/?$/,
200 message: getLabel('enter_valid_website'),
201 trigger: ['submit'],
202 })
203 }
204
205 formRules.value[key] = rules
206 })
207 // Form is loaded, remove loader
208 isLoading.value = false
209 // add vanila js remove class from loader
210 const loader = document.querySelector('.ivyforms-skeleton-' + counter)
211 if (loader) {
212 loader.remove()
213 }
214 } else {
215 console.error(getLabel('invalid_form_data'), formData)
216 }
217 } else {
218 console.error(getLabel('form_data_not_found'))
219 // Even with error, don't show loader anymore
220 isLoading.value = false
221 }
222 })
223
224 function getRequiredMessage(field: Field): string {
225 // If field has custom required message, use it
226 if (field.requiredMessage && field.requiredMessage.trim() !== '') {
227 return field.requiredMessage
228 }
229
230 // Otherwise use default message
231 let fieldMessage: string
232 fieldMessage = ucFirst(field.label || field.type)
233 fieldMessage += ` ${getLabel('required')}`
234
235 return fieldMessage
236 }
237
238 const ucFirst = (str: string) => (str ? str[0]?.toLocaleUpperCase() + str.slice(1) : '')
239
240 // Validate form and submit
241 const validateAndSubmit = async () => {
242 if (!formRef.value) return
243
244 await formRef.value.validate(
245 async (valid: boolean, errors: Record<string, { message: string }[]>) => {
246 if (valid) {
247 // Handle reCAPTCHA v3 execution before form submission
248 const hasRecaptchaField = formFields.value.some((field) => field.type === 'recaptcha')
249
250 if (hasRecaptchaField) {
251 // Find reCAPTCHA field component ref
252 const recaptchaFieldRef = formFieldsCollectorRefs.value?.find(
253 (ref: ComponentPublicInstance): ref is RecaptchaFieldComponent =>
254 ref &&
255 (ref as RecaptchaFieldComponent).$props &&
256 (ref as RecaptchaFieldComponent).$props.field &&
257 (ref as RecaptchaFieldComponent).$props.field.type === 'recaptcha',
258 )
259
260 if (recaptchaFieldRef && recaptchaFieldRef.executeRecaptcha) {
261 try {
262 // For v3, this will execute the reCAPTCHA and get a token
263 // For v2, this will return the current token
264 const token = await recaptchaFieldRef.executeRecaptcha()
265
266 // Update the form value with the token
267 const fieldKey = `recaptcha_${recaptchaFieldRef.$props.field.fieldIndex}`
268 formValues.value[fieldKey] = token
269 } catch (error) {
270 console.error('reCAPTCHA execution failed:', error)
271 // You might want to show an error message to the user here
272 return
273 }
274 }
275 }
276
277 submitForm()
278 } else {
279 fieldErrors.value = {} // Clear previous
280 Object.keys(errors).forEach((fieldKey) => {
281 fieldErrors.value[fieldKey] = errors[fieldKey][0]?.message || getLabel('invalid_input')
282 })
283 }
284 },
285 )
286 }
287
288 // Clear error when user starts typing
289 const clearFieldError = (fieldKey: string) => {
290 if (fieldErrors.value[fieldKey]) {
291 delete fieldErrors.value[fieldKey]
292 }
293 }
294
295 // Provide the clearFieldError function to child components
296 provide('clearFieldError', clearFieldError)
297
298 // Submit form
299 const submitForm = async () => {
300 try {
301 const { data, error, status } = await request('form/submission/', {
302 method: 'POST',
303 headers: {
304 'Content-Type': 'application/json',
305 },
306 data: {
307 formId: formData.value.id,
308 values: formValues.value,
309 nonce: formNonce.value,
310 },
311 })
312 // Handle response
313 if (status !== 200 || error) {
314 IvyMessage({
315 type: 'error',
316 message: getLabel('error_submitting_form') + (error ? error.message : data),
317 })
318 }
319
320 // Reset form or show success message
321 formValues.value = {}
322 const type = formConfirmation.value[0].type
323 const enabled = formConfirmation.value[0].enabled
324 const showForm = formConfirmation.value[0].showForm
325
326 if (!enabled) return
327
328 if (type === 'successMessage') {
329 if (!showForm) {
330 showSuccessMessage.value = true
331 successMessage.value = formConfirmation.value[0].message || getLabel('thank_you')
332 return
333 } else {
334 showSuccessMessageReset.value = true
335 successMessage.value = formConfirmation.value[0].message || getLabel('thank_you')
336
337 // Hide the success message after 5 seconds
338 setTimeout(() => {
339 const messageElement = document.querySelector('.ivyforms-success-message')
340 if (messageElement) {
341 messageElement.classList.add('ivyforms-fade-out')
342 }
343 setTimeout(() => {
344 showSuccessMessageReset.value = false
345 }, 500) // Match the CSS transition duration
346 }, 5000)
347 }
348 } else if (type === 'redirectToPage' && formConfirmation.value[0].page) {
349 const pageId = formConfirmation.value[0].page
350 if (pageId) {
351 if (router) {
352 await router.push({ path: '/', query: { p: pageId } })
353 } else {
354 redirectToUrl(`/?p=${pageId}`)
355 }
356 }
357 } else if (type === 'redirectToCustomUrl' && formConfirmation.value[0].url) {
358 redirectToUrl(formConfirmation.value[0].url)
359 }
360 } catch (error) {
361 console.error(getLabel('error_submitting_form'), error)
362 // Show error message
363 }
364 }
365 </script>
366
367 <style lang="scss" scoped>
368 .ivyforms-form-wrapper {
369 width: 100%;
370 max-width: 720px;
371 margin: 0 auto;
372
373 .ivyforms-form-title {
374 font-size: 24px;
375 font-weight: 600;
376 margin-bottom: 12px;
377 color: var(--map-base-text-0);
378 }
379
380 .ivyforms-form-description {
381 font-size: 16px;
382 margin-bottom: 24px;
383 color: var(--map-base-text-1);
384 }
385
386 .ivyforms-form-field {
387 margin-bottom: 16px;
388 }
389
390 .ivyforms-form-submit {
391 margin-top: 24px;
392 }
393 ::v-deep(.ivyforms-success-message) {
394 font-size: 20px;
395 color: var(--map-base-brand-symbol-0);
396 padding: 20px;
397 background-color: var(--map-base-brand-o05);
398 border: 1px solid var(--map-base-brand-symbol-0);
399 border-radius: 8px;
400 text-align: center;
401 transition: opacity 0.5s ease-in-out;
402 opacity: 1;
403
404 &.ivyforms-fade-out {
405 opacity: 0;
406 }
407 }
408 }
409 </style>
410