PluginProbe
The Innovative Form Builder – IvyForms / 0.3
The Innovative Form Builder – IvyForms v0.3
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, at frontend/src/views/public/FormRender.vue

396 lines 12.0 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, 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 let fieldMessage: string
226
227 // Set field type based on field type
228 fieldMessage = ucFirst(field.label || field.type)
229 fieldMessage += ` ${getLabel('required')}`
230
231 return fieldMessage
232 }
233
234 const ucFirst = (str: string) => (str ? str[0]?.toLocaleUpperCase() + str.slice(1) : '')
235
236 // Validate form and submit
237 const validateAndSubmit = async () => {
238 if (!formRef.value) return
239
240 await formRef.value.validate(
241 async (valid: boolean, errors: Record<string, { message: string }[]>) => {
242 if (valid) {
243 // Handle reCAPTCHA v3 execution before form submission
244 const hasRecaptchaField = formFields.value.some((field) => field.type === 'recaptcha')
245
246 if (hasRecaptchaField) {
247 // Find reCAPTCHA field component ref
248 const recaptchaFieldRef = formFieldsCollectorRefs.value?.find(
249 (ref: ComponentPublicInstance): ref is RecaptchaFieldComponent =>
250 ref &&
251 (ref as RecaptchaFieldComponent).$props &&
252 (ref as RecaptchaFieldComponent).$props.field &&
253 (ref as RecaptchaFieldComponent).$props.field.type === 'recaptcha',
254 )
255
256 if (recaptchaFieldRef && recaptchaFieldRef.executeRecaptcha) {
257 try {
258 // For v3, this will execute the reCAPTCHA and get a token
259 // For v2, this will return the current token
260 const token = await recaptchaFieldRef.executeRecaptcha()
261
262 // Update the form value with the token
263 const fieldKey = `recaptcha_${recaptchaFieldRef.$props.field.fieldIndex}`
264 formValues.value[fieldKey] = token
265 } catch (error) {
266 console.error('reCAPTCHA execution failed:', error)
267 // You might want to show an error message to the user here
268 return
269 }
270 }
271 }
272
273 submitForm()
274 } else {
275 fieldErrors.value = {} // Clear previous
276 Object.keys(errors).forEach((fieldKey) => {
277 fieldErrors.value[fieldKey] = errors[fieldKey][0]?.message || getLabel('invalid_input')
278 })
279 }
280 },
281 )
282 }
283
284 // Submit form
285 const submitForm = async () => {
286 try {
287 const { data, error, status } = await request('form/submission/', {
288 method: 'POST',
289 headers: {
290 'Content-Type': 'application/json',
291 },
292 data: {
293 formId: formData.value.id,
294 values: formValues.value,
295 nonce: formNonce.value,
296 },
297 })
298 // Handle response
299 if (status !== 200 || error) {
300 IvyMessage({
301 type: 'error',
302 message: getLabel('error_submitting_form') + (error ? error.message : data),
303 })
304 }
305
306 // Reset form or show success message
307 formValues.value = {}
308 const type = formConfirmation.value[0].type
309 const enabled = formConfirmation.value[0].enabled
310 const showForm = formConfirmation.value[0].showForm
311
312 if (!enabled) return
313
314 if (type === 'successMessage') {
315 if (!showForm) {
316 showSuccessMessage.value = true
317 successMessage.value = formConfirmation.value[0].message || getLabel('thank_you')
318 return
319 } else {
320 showSuccessMessageReset.value = true
321 successMessage.value = formConfirmation.value[0].message || getLabel('thank_you')
322
323 // Hide the success message after 5 seconds
324 setTimeout(() => {
325 const messageElement = document.querySelector('.ivyforms-success-message')
326 if (messageElement) {
327 messageElement.classList.add('ivyforms-fade-out')
328 }
329 setTimeout(() => {
330 showSuccessMessageReset.value = false
331 }, 500) // Match the CSS transition duration
332 }, 5000)
333 }
334 } else if (type === 'redirectToPage' && formConfirmation.value[0].page) {
335 const pageId = formConfirmation.value[0].page
336 if (pageId) {
337 if (router) {
338 await router.push({ path: '/', query: { p: pageId } })
339 } else {
340 redirectToUrl(`/?p=${pageId}`)
341 }
342 }
343 } else if (type === 'redirectToCustomUrl' && formConfirmation.value[0].url) {
344 redirectToUrl(formConfirmation.value[0].url)
345 }
346 } catch (error) {
347 console.error(getLabel('error_submitting_form'), error)
348 // Show error message
349 }
350 }
351 </script>
352
353 <style lang="scss" scoped>
354 .ivyforms-form-wrapper {
355 width: 100%;
356 max-width: 720px;
357 margin: 0 auto;
358
359 .ivyforms-form-title {
360 font-size: 24px;
361 font-weight: 600;
362 margin-bottom: 12px;
363 color: var(--map-base-text-0);
364 }
365
366 .ivyforms-form-description {
367 font-size: 16px;
368 margin-bottom: 24px;
369 color: var(--map-base-text-1);
370 }
371
372 .ivyforms-form-field {
373 margin-bottom: 16px;
374 }
375
376 .ivyforms-form-submit {
377 margin-top: 24px;
378 }
379 ::v-deep(.ivyforms-success-message) {
380 font-size: 20px;
381 color: var(--map-base-brand-symbol-0);
382 padding: 20px;
383 background-color: var(--map-base-brand-o05);
384 border: 1px solid var(--map-base-brand-symbol-0);
385 border-radius: 8px;
386 text-align: center;
387 transition: opacity 0.5s ease-in-out;
388 opacity: 1;
389
390 &.ivyforms-fade-out {
391 opacity: 0;
392 }
393 }
394 }
395 </style>
396