@duffcloudservices/site-forms 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +274 -260
- package/dist/composables/useFormSubmission.d.ts +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +554 -398
- package/dist/index.js.map +1 -1
- package/dist/presets.d.ts +13 -0
- package/dist/site-forms.css +1 -0
- package/dist/types.d.ts +12 -2
- package/package.json +72 -73
- package/src/DcsForm.vue +295 -303
- package/src/__tests__/fields.test.ts +81 -82
- package/src/__tests__/multi-step.test.ts +45 -46
- package/src/__tests__/presets.test.ts +64 -0
- package/src/__tests__/schema.test.ts +41 -42
- package/src/__tests__/style-import.test.ts +9 -0
- package/src/__tests__/submission.test.ts +115 -77
- package/src/__tests__/validation.test.ts +29 -0
- package/src/__tests__/visible-if.test.ts +110 -111
- package/src/composables/useDcsForm.ts +201 -201
- package/src/composables/useFormSubmission.ts +113 -113
- package/src/composables/useFormValidation.ts +128 -127
- package/src/fields/DcsFormCheckbox.vue +35 -35
- package/src/fields/DcsFormCheckboxGroup.vue +52 -52
- package/src/fields/DcsFormDate.vue +34 -34
- package/src/fields/DcsFormFieldWrapper.vue +39 -39
- package/src/fields/DcsFormFile.vue +38 -38
- package/src/fields/DcsFormHidden.vue +17 -17
- package/src/fields/DcsFormHtmlBlock.vue +19 -19
- package/src/fields/DcsFormRadio.vue +45 -45
- package/src/fields/DcsFormSection.vue +19 -19
- package/src/fields/DcsFormSelect.vue +62 -62
- package/src/fields/DcsFormText.vue +54 -54
- package/src/fields/DcsFormTextarea.vue +43 -43
- package/src/index.ts +64 -51
- package/src/loaders/yaml.ts +51 -51
- package/src/presets.ts +192 -0
- package/src/schema/form-definition.schema.json +410 -45
- package/src/schema/validate.ts +58 -58
- package/src/shims.d.ts +10 -10
- package/src/style.css +256 -0
- package/src/types.ts +164 -140
package/src/DcsForm.vue
CHANGED
|
@@ -1,303 +1,295 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { computed, onMounted, ref, watch } from 'vue'
|
|
3
|
-
import type {
|
|
4
|
-
DcsFormSubmitError,
|
|
5
|
-
DcsFormSubmitSuccess,
|
|
6
|
-
PortalFormDefinition,
|
|
7
|
-
PortalFormField,
|
|
8
|
-
} from './types'
|
|
9
|
-
import { useDcsForm } from './composables/useDcsForm'
|
|
10
|
-
import { submitFormValues } from './composables/useFormSubmission'
|
|
11
|
-
import { warnIfInvalid } from './schema/validate'
|
|
12
|
-
import { loadFormDefinitions } from './loaders/yaml'
|
|
13
|
-
|
|
14
|
-
import DcsFormText from './fields/DcsFormText.vue'
|
|
15
|
-
import DcsFormTextarea from './fields/DcsFormTextarea.vue'
|
|
16
|
-
import DcsFormSelect from './fields/DcsFormSelect.vue'
|
|
17
|
-
import DcsFormRadio from './fields/DcsFormRadio.vue'
|
|
18
|
-
import DcsFormCheckboxGroup from './fields/DcsFormCheckboxGroup.vue'
|
|
19
|
-
import DcsFormCheckbox from './fields/DcsFormCheckbox.vue'
|
|
20
|
-
import DcsFormDate from './fields/DcsFormDate.vue'
|
|
21
|
-
import DcsFormFile from './fields/DcsFormFile.vue'
|
|
22
|
-
import DcsFormHidden from './fields/DcsFormHidden.vue'
|
|
23
|
-
import DcsFormSection from './fields/DcsFormSection.vue'
|
|
24
|
-
import DcsFormHtmlBlock from './fields/DcsFormHtmlBlock.vue'
|
|
25
|
-
|
|
26
|
-
interface Props {
|
|
27
|
-
/** Kebab-case form id; matches `.dcs/forms/<formId>.yaml`. */
|
|
28
|
-
formId: string
|
|
29
|
-
/** Site slug used for the submission endpoint path. */
|
|
30
|
-
siteSlug?: string
|
|
31
|
-
/** Override the loaded definition (used by the portal preview iframe). */
|
|
32
|
-
definitionOverride?: PortalFormDefinition
|
|
33
|
-
/** Override the API base URL; defaults to `VITE_DCS_PUBLIC_API`. */
|
|
34
|
-
apiBase?: string
|
|
35
|
-
/** Optional captcha token, attached to the submission payload. */
|
|
36
|
-
captchaToken?: string
|
|
37
|
-
/**
|
|
38
|
-
* Optional override for the YAML modules map. Mostly for tests; in
|
|
39
|
-
* real apps the build-time `import.meta.glob` call below is used.
|
|
40
|
-
*/
|
|
41
|
-
formsModules?: Record<string, unknown>
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const props = defineProps<Props>()
|
|
45
|
-
|
|
46
|
-
const emit = defineEmits<{
|
|
47
|
-
'submit-success': [event: DcsFormSubmitSuccess]
|
|
48
|
-
'submit-error': [event: DcsFormSubmitError]
|
|
49
|
-
'validation-error': [errors: Record<string, string | undefined>]
|
|
50
|
-
}>()
|
|
51
|
-
|
|
52
|
-
// Eager glob — Vite inlines every site form at build time. Customer
|
|
53
|
-
// sites should configure `vite-plugin-yaml` so YAML is parsed to an
|
|
54
|
-
// object; without it the loader falls back to parsing raw strings.
|
|
55
|
-
const eagerFormsModules = (import.meta as unknown as {
|
|
56
|
-
glob: (
|
|
57
|
-
pattern: string,
|
|
58
|
-
opts: { eager: true; import: string; query?: string },
|
|
59
|
-
) => Record<string, unknown>
|
|
60
|
-
}).glob('/.dcs/forms/*.yaml', { eager: true, import: 'default' })
|
|
61
|
-
|
|
62
|
-
const definitions = computed(() =>
|
|
63
|
-
loadFormDefinitions(props.formsModules ?? eagerFormsModules),
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
const definition = computed<PortalFormDefinition | null>(() => {
|
|
67
|
-
if (props.definitionOverride) return props.definitionOverride
|
|
68
|
-
return definitions.value[props.formId] ?? null
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
const isDev = !!(import.meta as unknown as { env?: { DEV?: boolean } }).env?.DEV
|
|
72
|
-
|
|
73
|
-
watch(
|
|
74
|
-
definition,
|
|
75
|
-
(def) => {
|
|
76
|
-
if (def) warnIfInvalid(props.formId, def, isDev)
|
|
77
|
-
},
|
|
78
|
-
{ immediate: true },
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
// We always call useDcsForm with *some* definition so hooks render
|
|
82
|
-
// stably; if the form is missing we'll show an error state instead.
|
|
83
|
-
const safeDefinition = computed<PortalFormDefinition>(
|
|
84
|
-
() =>
|
|
85
|
-
definition.value ?? {
|
|
86
|
-
formId: props.formId,
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
() =>
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
case '
|
|
125
|
-
case '
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
case '
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
form.
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
`
|
|
194
|
-
`
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
<
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
>
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
<button
|
|
277
|
-
v-if="form.steps.value && !form.
|
|
278
|
-
type="button"
|
|
279
|
-
class="dcs-form__btn dcs-form__btn--
|
|
280
|
-
@click="form.
|
|
281
|
-
>
|
|
282
|
-
|
|
283
|
-
</button>
|
|
284
|
-
<button
|
|
285
|
-
v-if="form.steps.value
|
|
286
|
-
type="
|
|
287
|
-
class="dcs-form__btn dcs-form__btn--
|
|
288
|
-
|
|
289
|
-
>
|
|
290
|
-
|
|
291
|
-
</button>
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
:disabled="form.submitting.value"
|
|
297
|
-
>
|
|
298
|
-
{{ form.submitting.value ? 'Sending…' : submitLabel }}
|
|
299
|
-
</button>
|
|
300
|
-
</slot>
|
|
301
|
-
</div>
|
|
302
|
-
</form>
|
|
303
|
-
</template>
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, onMounted, ref, watch } from 'vue'
|
|
3
|
+
import type {
|
|
4
|
+
DcsFormSubmitError,
|
|
5
|
+
DcsFormSubmitSuccess,
|
|
6
|
+
PortalFormDefinition,
|
|
7
|
+
PortalFormField,
|
|
8
|
+
} from './types'
|
|
9
|
+
import { useDcsForm } from './composables/useDcsForm'
|
|
10
|
+
import { submitFormValues } from './composables/useFormSubmission'
|
|
11
|
+
import { warnIfInvalid } from './schema/validate'
|
|
12
|
+
import { loadFormDefinitions } from './loaders/yaml'
|
|
13
|
+
|
|
14
|
+
import DcsFormText from './fields/DcsFormText.vue'
|
|
15
|
+
import DcsFormTextarea from './fields/DcsFormTextarea.vue'
|
|
16
|
+
import DcsFormSelect from './fields/DcsFormSelect.vue'
|
|
17
|
+
import DcsFormRadio from './fields/DcsFormRadio.vue'
|
|
18
|
+
import DcsFormCheckboxGroup from './fields/DcsFormCheckboxGroup.vue'
|
|
19
|
+
import DcsFormCheckbox from './fields/DcsFormCheckbox.vue'
|
|
20
|
+
import DcsFormDate from './fields/DcsFormDate.vue'
|
|
21
|
+
import DcsFormFile from './fields/DcsFormFile.vue'
|
|
22
|
+
import DcsFormHidden from './fields/DcsFormHidden.vue'
|
|
23
|
+
import DcsFormSection from './fields/DcsFormSection.vue'
|
|
24
|
+
import DcsFormHtmlBlock from './fields/DcsFormHtmlBlock.vue'
|
|
25
|
+
|
|
26
|
+
interface Props {
|
|
27
|
+
/** Kebab-case form id; matches `.dcs/forms/<formId>.yaml`. */
|
|
28
|
+
formId: string
|
|
29
|
+
/** Site slug used for the submission endpoint path. */
|
|
30
|
+
siteSlug?: string
|
|
31
|
+
/** Override the loaded definition (used by the portal preview iframe). */
|
|
32
|
+
definitionOverride?: PortalFormDefinition
|
|
33
|
+
/** Override the API base URL; defaults to `VITE_DCS_PUBLIC_API`. */
|
|
34
|
+
apiBase?: string
|
|
35
|
+
/** Optional captcha token, attached to the submission payload. */
|
|
36
|
+
captchaToken?: string
|
|
37
|
+
/**
|
|
38
|
+
* Optional override for the YAML modules map. Mostly for tests; in
|
|
39
|
+
* real apps the build-time `import.meta.glob` call below is used.
|
|
40
|
+
*/
|
|
41
|
+
formsModules?: Record<string, unknown>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const props = defineProps<Props>()
|
|
45
|
+
|
|
46
|
+
const emit = defineEmits<{
|
|
47
|
+
'submit-success': [event: DcsFormSubmitSuccess]
|
|
48
|
+
'submit-error': [event: DcsFormSubmitError]
|
|
49
|
+
'validation-error': [errors: Record<string, string | undefined>]
|
|
50
|
+
}>()
|
|
51
|
+
|
|
52
|
+
// Eager glob — Vite inlines every site form at build time. Customer
|
|
53
|
+
// sites should configure `vite-plugin-yaml` so YAML is parsed to an
|
|
54
|
+
// object; without it the loader falls back to parsing raw strings.
|
|
55
|
+
const eagerFormsModules = (import.meta as unknown as {
|
|
56
|
+
glob: (
|
|
57
|
+
pattern: string,
|
|
58
|
+
opts: { eager: true; import: string; query?: string },
|
|
59
|
+
) => Record<string, unknown>
|
|
60
|
+
}).glob('/.dcs/forms/*.yaml', { eager: true, import: 'default' })
|
|
61
|
+
|
|
62
|
+
const definitions = computed(() =>
|
|
63
|
+
loadFormDefinitions(props.formsModules ?? eagerFormsModules),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
const definition = computed<PortalFormDefinition | null>(() => {
|
|
67
|
+
if (props.definitionOverride) return props.definitionOverride
|
|
68
|
+
return definitions.value[props.formId] ?? null
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const isDev = !!(import.meta as unknown as { env?: { DEV?: boolean } }).env?.DEV
|
|
72
|
+
|
|
73
|
+
watch(
|
|
74
|
+
definition,
|
|
75
|
+
(def) => {
|
|
76
|
+
if (def) warnIfInvalid(props.formId, def, isDev)
|
|
77
|
+
},
|
|
78
|
+
{ immediate: true },
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
// We always call useDcsForm with *some* definition so hooks render
|
|
82
|
+
// stably; if the form is missing we'll show an error state instead.
|
|
83
|
+
const safeDefinition = computed<PortalFormDefinition>(
|
|
84
|
+
() =>
|
|
85
|
+
definition.value ?? {
|
|
86
|
+
formId: props.formId,
|
|
87
|
+
submission: { kind: 'lead' },
|
|
88
|
+
fields: [],
|
|
89
|
+
},
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
const form = useDcsForm({ definition: safeDefinition.value })
|
|
93
|
+
|
|
94
|
+
// If the resolved definition changes (e.g. preview iframe updates),
|
|
95
|
+
// re-create derived state by resetting.
|
|
96
|
+
watch(
|
|
97
|
+
() => safeDefinition.value,
|
|
98
|
+
() => form.reset(),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
const apiBase = computed(
|
|
102
|
+
() =>
|
|
103
|
+
props.apiBase ??
|
|
104
|
+
(import.meta as unknown as { env?: { VITE_DCS_PUBLIC_API?: string } }).env
|
|
105
|
+
?.VITE_DCS_PUBLIC_API ??
|
|
106
|
+
'',
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
const resolvedSiteSlug = computed(
|
|
110
|
+
() =>
|
|
111
|
+
props.siteSlug ??
|
|
112
|
+
(import.meta as unknown as { env?: { VITE_DCS_SITE_SLUG?: string } }).env
|
|
113
|
+
?.VITE_DCS_SITE_SLUG ??
|
|
114
|
+
'',
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
const submitLabel = computed(
|
|
118
|
+
() => safeDefinition.value.submitLabel ?? 'Send',
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
function fieldComponent(field: PortalFormField) {
|
|
122
|
+
switch (field.type) {
|
|
123
|
+
case 'text':
|
|
124
|
+
case 'email':
|
|
125
|
+
case 'tel':
|
|
126
|
+
return DcsFormText
|
|
127
|
+
case 'textarea':
|
|
128
|
+
return DcsFormTextarea
|
|
129
|
+
case 'select':
|
|
130
|
+
case 'multiselect':
|
|
131
|
+
return DcsFormSelect
|
|
132
|
+
case 'radio':
|
|
133
|
+
return DcsFormRadio
|
|
134
|
+
case 'checkbox-group':
|
|
135
|
+
return DcsFormCheckboxGroup
|
|
136
|
+
case 'checkbox':
|
|
137
|
+
return DcsFormCheckbox
|
|
138
|
+
case 'date':
|
|
139
|
+
return DcsFormDate
|
|
140
|
+
case 'file':
|
|
141
|
+
return DcsFormFile
|
|
142
|
+
case 'hidden':
|
|
143
|
+
return DcsFormHidden
|
|
144
|
+
case 'section-heading':
|
|
145
|
+
return DcsFormSection
|
|
146
|
+
case 'html-block':
|
|
147
|
+
return DcsFormHtmlBlock
|
|
148
|
+
default:
|
|
149
|
+
return DcsFormText
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const formEl = ref<HTMLFormElement | null>(null)
|
|
154
|
+
|
|
155
|
+
async function onSubmit(e: Event): Promise<void> {
|
|
156
|
+
e.preventDefault()
|
|
157
|
+
if (!definition.value) return
|
|
158
|
+
const ok = form.validateAll()
|
|
159
|
+
if (!ok) {
|
|
160
|
+
emit('validation-error', form.errors.value)
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
form.submitting.value = true
|
|
164
|
+
form.submitError.value = null
|
|
165
|
+
const payload = {
|
|
166
|
+
formId: props.formId,
|
|
167
|
+
values: form.collectSubmissionValues(),
|
|
168
|
+
captchaToken: props.captchaToken,
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
const result = await submitFormValues({
|
|
172
|
+
apiBase: apiBase.value,
|
|
173
|
+
siteSlug: resolvedSiteSlug.value,
|
|
174
|
+
payload,
|
|
175
|
+
})
|
|
176
|
+
form.submitted.value = true
|
|
177
|
+
emit('submit-success', result)
|
|
178
|
+
} catch (err) {
|
|
179
|
+
const e2 = err as DcsFormSubmitError
|
|
180
|
+
form.submitError.value = e2.error?.message ?? 'Submission failed'
|
|
181
|
+
emit('submit-error', e2)
|
|
182
|
+
} finally {
|
|
183
|
+
form.submitting.value = false
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
onMounted(() => {
|
|
188
|
+
if (!definition.value) {
|
|
189
|
+
// eslint-disable-next-line no-console
|
|
190
|
+
console.warn(
|
|
191
|
+
`[@duffcloudservices/site-forms] No form definition found for "${props.formId}". ` +
|
|
192
|
+
`Expected a YAML at .dcs/forms/${props.formId}.yaml. ` +
|
|
193
|
+
`In customer-site repos the YAML lives above the Vite root, so the ` +
|
|
194
|
+
`internal auto-glob will not find it — pass formsModules explicitly: ` +
|
|
195
|
+
`<DcsForm form-id="${props.formId}" :forms-modules="dcsFormsModules" />. ` +
|
|
196
|
+
`See @duffcloudservices/site-forms README "Vite setup" section.`,
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
})
|
|
200
|
+
</script>
|
|
201
|
+
|
|
202
|
+
<template>
|
|
203
|
+
<div v-if="!definition" class="dcs-form dcs-form--missing" :data-form-key="formId">
|
|
204
|
+
<slot name="missing" :form-id="formId">
|
|
205
|
+
<p>Form “{{ formId }}” is not configured.</p>
|
|
206
|
+
</slot>
|
|
207
|
+
</div>
|
|
208
|
+
|
|
209
|
+
<div v-else-if="form.submitted.value" class="dcs-form dcs-form--success" :data-form-key="formId">
|
|
210
|
+
<slot name="success" :definition="definition">
|
|
211
|
+
<p>{{ definition.successMessage ?? 'Thanks — we received your message.' }}</p>
|
|
212
|
+
</slot>
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
<form
|
|
216
|
+
v-else
|
|
217
|
+
ref="formEl"
|
|
218
|
+
class="dcs-form"
|
|
219
|
+
:data-form-key="formId"
|
|
220
|
+
novalidate
|
|
221
|
+
@submit="onSubmit"
|
|
222
|
+
>
|
|
223
|
+
<slot name="header" :definition="definition" />
|
|
224
|
+
|
|
225
|
+
<div v-if="form.steps.value" class="dcs-form__progress" aria-live="polite">
|
|
226
|
+
<slot
|
|
227
|
+
name="progress"
|
|
228
|
+
:current="form.currentStepIndex.value"
|
|
229
|
+
:total="form.steps.value.length"
|
|
230
|
+
:step="form.currentStep.value"
|
|
231
|
+
>
|
|
232
|
+
<p>
|
|
233
|
+
Step {{ form.currentStepIndex.value + 1 }} of
|
|
234
|
+
{{ form.steps.value.length }}
|
|
235
|
+
<template v-if="form.currentStep.value">
|
|
236
|
+
— {{ form.currentStep.value.title }}
|
|
237
|
+
</template>
|
|
238
|
+
</p>
|
|
239
|
+
</slot>
|
|
240
|
+
</div>
|
|
241
|
+
|
|
242
|
+
<div class="dcs-form__fields">
|
|
243
|
+
<component
|
|
244
|
+
:is="fieldComponent(field)"
|
|
245
|
+
v-for="field in form.visibleFields.value"
|
|
246
|
+
:key="field.id"
|
|
247
|
+
:field="field"
|
|
248
|
+
:model-value="form.values[field.id]"
|
|
249
|
+
:error="form.errors.value[field.id]"
|
|
250
|
+
@update:model-value="(v: unknown) => form.setValue(field.id, v)"
|
|
251
|
+
@blur="form.touch(field.id)"
|
|
252
|
+
/>
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
<div v-if="form.submitError.value" class="dcs-form__submit-error" role="alert">
|
|
256
|
+
{{ form.submitError.value }}
|
|
257
|
+
</div>
|
|
258
|
+
|
|
259
|
+
<div class="dcs-form__actions">
|
|
260
|
+
<slot
|
|
261
|
+
name="actions"
|
|
262
|
+
:is-first-step="form.isFirstStep.value"
|
|
263
|
+
:is-last-step="form.isLastStep.value"
|
|
264
|
+
:submitting="form.submitting.value"
|
|
265
|
+
:prev="form.prev"
|
|
266
|
+
:next="form.next"
|
|
267
|
+
>
|
|
268
|
+
<button
|
|
269
|
+
v-if="form.steps.value && !form.isFirstStep.value"
|
|
270
|
+
type="button"
|
|
271
|
+
class="dcs-form__btn dcs-form__btn--prev"
|
|
272
|
+
@click="form.prev()"
|
|
273
|
+
>
|
|
274
|
+
Previous
|
|
275
|
+
</button>
|
|
276
|
+
<button
|
|
277
|
+
v-if="form.steps.value && !form.isLastStep.value"
|
|
278
|
+
type="button"
|
|
279
|
+
class="dcs-form__btn dcs-form__btn--next"
|
|
280
|
+
@click="form.next()"
|
|
281
|
+
>
|
|
282
|
+
Next
|
|
283
|
+
</button>
|
|
284
|
+
<button
|
|
285
|
+
v-if="!form.steps.value || form.isLastStep.value"
|
|
286
|
+
type="submit"
|
|
287
|
+
class="dcs-form__btn dcs-form__btn--submit"
|
|
288
|
+
:disabled="form.submitting.value"
|
|
289
|
+
>
|
|
290
|
+
{{ form.submitting.value ? 'Sending…' : submitLabel }}
|
|
291
|
+
</button>
|
|
292
|
+
</slot>
|
|
293
|
+
</div>
|
|
294
|
+
</form>
|
|
295
|
+
</template>
|