@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.
Files changed (41) hide show
  1. package/README.md +274 -260
  2. package/dist/composables/useFormSubmission.d.ts +1 -1
  3. package/dist/index.d.ts +3 -1
  4. package/dist/index.js +554 -398
  5. package/dist/index.js.map +1 -1
  6. package/dist/presets.d.ts +13 -0
  7. package/dist/site-forms.css +1 -0
  8. package/dist/types.d.ts +12 -2
  9. package/package.json +72 -73
  10. package/src/DcsForm.vue +295 -303
  11. package/src/__tests__/fields.test.ts +81 -82
  12. package/src/__tests__/multi-step.test.ts +45 -46
  13. package/src/__tests__/presets.test.ts +64 -0
  14. package/src/__tests__/schema.test.ts +41 -42
  15. package/src/__tests__/style-import.test.ts +9 -0
  16. package/src/__tests__/submission.test.ts +115 -77
  17. package/src/__tests__/validation.test.ts +29 -0
  18. package/src/__tests__/visible-if.test.ts +110 -111
  19. package/src/composables/useDcsForm.ts +201 -201
  20. package/src/composables/useFormSubmission.ts +113 -113
  21. package/src/composables/useFormValidation.ts +128 -127
  22. package/src/fields/DcsFormCheckbox.vue +35 -35
  23. package/src/fields/DcsFormCheckboxGroup.vue +52 -52
  24. package/src/fields/DcsFormDate.vue +34 -34
  25. package/src/fields/DcsFormFieldWrapper.vue +39 -39
  26. package/src/fields/DcsFormFile.vue +38 -38
  27. package/src/fields/DcsFormHidden.vue +17 -17
  28. package/src/fields/DcsFormHtmlBlock.vue +19 -19
  29. package/src/fields/DcsFormRadio.vue +45 -45
  30. package/src/fields/DcsFormSection.vue +19 -19
  31. package/src/fields/DcsFormSelect.vue +62 -62
  32. package/src/fields/DcsFormText.vue +54 -54
  33. package/src/fields/DcsFormTextarea.vue +43 -43
  34. package/src/index.ts +64 -51
  35. package/src/loaders/yaml.ts +51 -51
  36. package/src/presets.ts +192 -0
  37. package/src/schema/form-definition.schema.json +410 -45
  38. package/src/schema/validate.ts +58 -58
  39. package/src/shims.d.ts +10 -10
  40. package/src/style.css +256 -0
  41. package/src/types.ts +164 -140
@@ -1,34 +1,34 @@
1
- <script setup lang="ts">
2
- import { computed } from 'vue'
3
- import type { PortalFormField } from '../types'
4
- import DcsFormFieldWrapper from './DcsFormFieldWrapper.vue'
5
-
6
- const props = defineProps<{
7
- field: PortalFormField
8
- modelValue: string | undefined
9
- error?: string
10
- }>()
11
-
12
- const emit = defineEmits<{
13
- 'update:modelValue': [value: string]
14
- blur: []
15
- }>()
16
-
17
- const inputId = computed(() => `field-${props.field.id}`)
18
- </script>
19
-
20
- <template>
21
- <DcsFormFieldWrapper :field="field" :error="error" :input-id="inputId">
22
- <input
23
- :id="inputId"
24
- class="dcs-form-input dcs-form-date"
25
- type="date"
26
- :name="field.id"
27
- :required="field.required"
28
- :aria-invalid="!!error"
29
- :value="modelValue ?? ''"
30
- @input="emit('update:modelValue', ($event.target as HTMLInputElement).value)"
31
- @blur="emit('blur')"
32
- />
33
- </DcsFormFieldWrapper>
34
- </template>
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import type { PortalFormField } from '../types'
4
+ import DcsFormFieldWrapper from './DcsFormFieldWrapper.vue'
5
+
6
+ const props = defineProps<{
7
+ field: PortalFormField
8
+ modelValue: string | undefined
9
+ error?: string
10
+ }>()
11
+
12
+ const emit = defineEmits<{
13
+ 'update:modelValue': [value: string]
14
+ blur: []
15
+ }>()
16
+
17
+ const inputId = computed(() => `field-${props.field.id}`)
18
+ </script>
19
+
20
+ <template>
21
+ <DcsFormFieldWrapper :field="field" :error="error" :input-id="inputId">
22
+ <input
23
+ :id="inputId"
24
+ class="dcs-form-input dcs-form-date"
25
+ type="date"
26
+ :name="field.id"
27
+ :required="field.required"
28
+ :aria-invalid="!!error"
29
+ :value="modelValue ?? ''"
30
+ @input="emit('update:modelValue', ($event.target as HTMLInputElement).value)"
31
+ @blur="emit('blur')"
32
+ />
33
+ </DcsFormFieldWrapper>
34
+ </template>
@@ -1,39 +1,39 @@
1
- <script setup lang="ts">
2
- import type { PortalFormField } from '../types'
3
-
4
- defineProps<{
5
- field: PortalFormField
6
- error?: string
7
- /** Optional id to use for the input — wrappers use it for label `for=`. */
8
- inputId?: string
9
- }>()
10
- </script>
11
-
12
- <template>
13
- <div
14
- class="dcs-form-field"
15
- :class="[`dcs-form-field--${field.type}`, `dcs-form-field--width-${field.width ?? 'full'}`]"
16
- :data-form-field-key="field.id"
17
- >
18
- <slot name="label">
19
- <label
20
- v-if="field.type !== 'checkbox' && field.type !== 'hidden' && field.type !== 'section-heading' && field.type !== 'html-block'"
21
- :for="inputId ?? `field-${field.id}`"
22
- class="dcs-form-field__label"
23
- >
24
- {{ field.label }}
25
- <span v-if="field.required" aria-hidden="true" class="dcs-form-field__required">*</span>
26
- </label>
27
- </slot>
28
-
29
- <slot />
30
-
31
- <slot name="help">
32
- <p v-if="field.helpText" class="dcs-form-field__help">{{ field.helpText }}</p>
33
- </slot>
34
-
35
- <slot name="error">
36
- <p v-if="error" class="dcs-form-field__error" role="alert">{{ error }}</p>
37
- </slot>
38
- </div>
39
- </template>
1
+ <script setup lang="ts">
2
+ import type { PortalFormField } from '../types'
3
+
4
+ defineProps<{
5
+ field: PortalFormField
6
+ error?: string
7
+ /** Optional id to use for the input — wrappers use it for label `for=`. */
8
+ inputId?: string
9
+ }>()
10
+ </script>
11
+
12
+ <template>
13
+ <div
14
+ class="dcs-form-field"
15
+ :class="[`dcs-form-field--${field.type}`, `dcs-form-field--width-${field.width ?? 'full'}`]"
16
+ :data-form-field-key="field.id"
17
+ >
18
+ <slot name="label">
19
+ <label
20
+ v-if="field.type !== 'checkbox' && field.type !== 'hidden' && field.type !== 'section-heading' && field.type !== 'html-block'"
21
+ :for="inputId ?? `field-${field.id}`"
22
+ class="dcs-form-field__label"
23
+ >
24
+ {{ field.label }}
25
+ <span v-if="field.required" aria-hidden="true" class="dcs-form-field__required">*</span>
26
+ </label>
27
+ </slot>
28
+
29
+ <slot />
30
+
31
+ <slot name="help">
32
+ <p v-if="field.helpText" class="dcs-form-field__help">{{ field.helpText }}</p>
33
+ </slot>
34
+
35
+ <slot name="error">
36
+ <p v-if="error" class="dcs-form-field__error" role="alert">{{ error }}</p>
37
+ </slot>
38
+ </div>
39
+ </template>
@@ -1,38 +1,38 @@
1
- <script setup lang="ts">
2
- import { computed } from 'vue'
3
- import type { PortalFormField } from '../types'
4
- import DcsFormFieldWrapper from './DcsFormFieldWrapper.vue'
5
-
6
- const props = defineProps<{
7
- field: PortalFormField
8
- modelValue: File | undefined
9
- error?: string
10
- }>()
11
-
12
- const emit = defineEmits<{
13
- 'update:modelValue': [value: File | undefined]
14
- }>()
15
-
16
- const inputId = computed(() => `field-${props.field.id}`)
17
- const accept = computed(() => (props.field.validation?.accept ?? []).join(','))
18
-
19
- function onChange(e: Event): void {
20
- const target = e.target as HTMLInputElement
21
- emit('update:modelValue', target.files?.[0])
22
- }
23
- </script>
24
-
25
- <template>
26
- <DcsFormFieldWrapper :field="field" :error="error" :input-id="inputId">
27
- <input
28
- :id="inputId"
29
- class="dcs-form-input dcs-form-file"
30
- type="file"
31
- :name="field.id"
32
- :required="field.required"
33
- :aria-invalid="!!error"
34
- :accept="accept || undefined"
35
- @change="onChange"
36
- />
37
- </DcsFormFieldWrapper>
38
- </template>
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import type { PortalFormField } from '../types'
4
+ import DcsFormFieldWrapper from './DcsFormFieldWrapper.vue'
5
+
6
+ const props = defineProps<{
7
+ field: PortalFormField
8
+ modelValue: File | undefined
9
+ error?: string
10
+ }>()
11
+
12
+ const emit = defineEmits<{
13
+ 'update:modelValue': [value: File | undefined]
14
+ }>()
15
+
16
+ const inputId = computed(() => `field-${props.field.id}`)
17
+ const accept = computed(() => (props.field.validation?.accept ?? []).join(','))
18
+
19
+ function onChange(e: Event): void {
20
+ const target = e.target as HTMLInputElement
21
+ emit('update:modelValue', target.files?.[0])
22
+ }
23
+ </script>
24
+
25
+ <template>
26
+ <DcsFormFieldWrapper :field="field" :error="error" :input-id="inputId">
27
+ <input
28
+ :id="inputId"
29
+ class="dcs-form-input dcs-form-file"
30
+ type="file"
31
+ :name="field.id"
32
+ :required="field.required"
33
+ :aria-invalid="!!error"
34
+ :accept="accept || undefined"
35
+ @change="onChange"
36
+ />
37
+ </DcsFormFieldWrapper>
38
+ </template>
@@ -1,17 +1,17 @@
1
- <script setup lang="ts">
2
- import type { PortalFormField } from '../types'
3
-
4
- defineProps<{
5
- field: PortalFormField
6
- modelValue: string | number | undefined
7
- }>()
8
- </script>
9
-
10
- <template>
11
- <input
12
- type="hidden"
13
- :name="field.id"
14
- :value="modelValue ?? (field.defaultValue as string | number | undefined) ?? ''"
15
- :data-form-field-key="field.id"
16
- />
17
- </template>
1
+ <script setup lang="ts">
2
+ import type { PortalFormField } from '../types'
3
+
4
+ defineProps<{
5
+ field: PortalFormField
6
+ modelValue: string | number | undefined
7
+ }>()
8
+ </script>
9
+
10
+ <template>
11
+ <input
12
+ type="hidden"
13
+ :name="field.id"
14
+ :value="modelValue ?? (field.defaultValue as string | number | undefined) ?? ''"
15
+ :data-form-field-key="field.id"
16
+ />
17
+ </template>
@@ -1,19 +1,19 @@
1
- <script setup lang="ts">
2
- import type { PortalFormField } from '../types'
3
-
4
- defineProps<{
5
- field: PortalFormField
6
- }>()
7
- </script>
8
-
9
- <template>
10
- <!--
11
- `html` is sanitized server-side per the schema; rendering with v-html
12
- is intentional. Site authors must keep portal-side sanitization on.
13
- -->
14
- <div
15
- class="dcs-form-html-block"
16
- :data-form-field-key="field.id"
17
- v-html="field.html ?? ''"
18
- />
19
- </template>
1
+ <script setup lang="ts">
2
+ import type { PortalFormField } from '../types'
3
+
4
+ defineProps<{
5
+ field: PortalFormField
6
+ }>()
7
+ </script>
8
+
9
+ <template>
10
+ <!--
11
+ `html` is sanitized server-side per the schema; rendering with v-html
12
+ is intentional. Site authors must keep portal-side sanitization on.
13
+ -->
14
+ <div
15
+ class="dcs-form-html-block"
16
+ :data-form-field-key="field.id"
17
+ v-html="field.html ?? ''"
18
+ />
19
+ </template>
@@ -1,45 +1,45 @@
1
- <script setup lang="ts">
2
- import { computed } from 'vue'
3
- import type { PortalFormField } from '../types'
4
- import DcsFormFieldWrapper from './DcsFormFieldWrapper.vue'
5
-
6
- const props = defineProps<{
7
- field: PortalFormField
8
- modelValue: string | undefined
9
- error?: string
10
- }>()
11
-
12
- const emit = defineEmits<{
13
- 'update:modelValue': [value: string]
14
- }>()
15
-
16
- const inputId = computed(() => `field-${props.field.id}`)
17
- const options = computed(() => props.field.options ?? [])
18
- </script>
19
-
20
- <template>
21
- <DcsFormFieldWrapper :field="field" :error="error" :input-id="inputId">
22
- <fieldset
23
- class="dcs-form-radio-group"
24
- role="radiogroup"
25
- :aria-required="field.required"
26
- :aria-invalid="!!error"
27
- >
28
- <legend class="sr-only">{{ field.label }}</legend>
29
- <label
30
- v-for="opt in options"
31
- :key="opt.value"
32
- class="dcs-form-radio"
33
- >
34
- <input
35
- type="radio"
36
- :name="field.id"
37
- :value="opt.value"
38
- :checked="modelValue === opt.value"
39
- @change="emit('update:modelValue', opt.value)"
40
- />
41
- <span>{{ opt.label }}</span>
42
- </label>
43
- </fieldset>
44
- </DcsFormFieldWrapper>
45
- </template>
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import type { PortalFormField } from '../types'
4
+ import DcsFormFieldWrapper from './DcsFormFieldWrapper.vue'
5
+
6
+ const props = defineProps<{
7
+ field: PortalFormField
8
+ modelValue: string | undefined
9
+ error?: string
10
+ }>()
11
+
12
+ const emit = defineEmits<{
13
+ 'update:modelValue': [value: string]
14
+ }>()
15
+
16
+ const inputId = computed(() => `field-${props.field.id}`)
17
+ const options = computed(() => props.field.options ?? [])
18
+ </script>
19
+
20
+ <template>
21
+ <DcsFormFieldWrapper :field="field" :error="error" :input-id="inputId">
22
+ <fieldset
23
+ class="dcs-form-radio-group"
24
+ role="radiogroup"
25
+ :aria-required="field.required"
26
+ :aria-invalid="!!error"
27
+ >
28
+ <legend class="sr-only">{{ field.label }}</legend>
29
+ <label
30
+ v-for="opt in options"
31
+ :key="opt.value"
32
+ class="dcs-form-radio"
33
+ >
34
+ <input
35
+ type="radio"
36
+ :name="field.id"
37
+ :value="opt.value"
38
+ :checked="modelValue === opt.value"
39
+ @change="emit('update:modelValue', opt.value)"
40
+ />
41
+ <span>{{ opt.label }}</span>
42
+ </label>
43
+ </fieldset>
44
+ </DcsFormFieldWrapper>
45
+ </template>
@@ -1,19 +1,19 @@
1
- <script setup lang="ts">
2
- import type { PortalFormField } from '../types'
3
-
4
- defineProps<{
5
- field: PortalFormField
6
- }>()
7
- </script>
8
-
9
- <template>
10
- <div
11
- class="dcs-form-section"
12
- :data-form-field-key="field.id"
13
- >
14
- <slot>
15
- <h3 class="dcs-form-section__heading">{{ field.label }}</h3>
16
- <p v-if="field.helpText" class="dcs-form-section__help">{{ field.helpText }}</p>
17
- </slot>
18
- </div>
19
- </template>
1
+ <script setup lang="ts">
2
+ import type { PortalFormField } from '../types'
3
+
4
+ defineProps<{
5
+ field: PortalFormField
6
+ }>()
7
+ </script>
8
+
9
+ <template>
10
+ <div
11
+ class="dcs-form-section"
12
+ :data-form-field-key="field.id"
13
+ >
14
+ <slot>
15
+ <h3 class="dcs-form-section__heading">{{ field.label }}</h3>
16
+ <p v-if="field.helpText" class="dcs-form-section__help">{{ field.helpText }}</p>
17
+ </slot>
18
+ </div>
19
+ </template>
@@ -1,62 +1,62 @@
1
- <script setup lang="ts">
2
- import { computed } from 'vue'
3
- import type { PortalFormField } from '../types'
4
- import DcsFormFieldWrapper from './DcsFormFieldWrapper.vue'
5
-
6
- const props = defineProps<{
7
- field: PortalFormField
8
- modelValue: string | string[] | undefined
9
- error?: string
10
- }>()
11
-
12
- const emit = defineEmits<{
13
- 'update:modelValue': [value: string | string[]]
14
- blur: []
15
- }>()
16
-
17
- const inputId = computed(() => `field-${props.field.id}`)
18
- const isMulti = computed(() => props.field.type === 'multiselect')
19
- const options = computed(() => props.field.options ?? [])
20
-
21
- function onChange(e: Event): void {
22
- const sel = e.target as HTMLSelectElement
23
- if (isMulti.value) {
24
- const values: string[] = []
25
- for (const opt of Array.from(sel.selectedOptions)) values.push(opt.value)
26
- emit('update:modelValue', values)
27
- } else {
28
- emit('update:modelValue', sel.value)
29
- }
30
- }
31
- </script>
32
-
33
- <template>
34
- <DcsFormFieldWrapper :field="field" :error="error" :input-id="inputId">
35
- <slot
36
- name="input"
37
- :id="inputId"
38
- :value="modelValue"
39
- :options="options"
40
- :on-change="onChange"
41
- >
42
- <select
43
- :id="inputId"
44
- class="dcs-form-input dcs-form-select"
45
- :name="field.id"
46
- :required="field.required"
47
- :multiple="isMulti"
48
- :aria-invalid="!!error"
49
- :value="modelValue ?? (isMulti ? [] : '')"
50
- @change="onChange"
51
- @blur="emit('blur')"
52
- >
53
- <option v-if="!isMulti" value="" disabled>
54
- {{ field.placeholder ?? 'Select…' }}
55
- </option>
56
- <option v-for="opt in options" :key="opt.value" :value="opt.value">
57
- {{ opt.label }}
58
- </option>
59
- </select>
60
- </slot>
61
- </DcsFormFieldWrapper>
62
- </template>
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import type { PortalFormField } from '../types'
4
+ import DcsFormFieldWrapper from './DcsFormFieldWrapper.vue'
5
+
6
+ const props = defineProps<{
7
+ field: PortalFormField
8
+ modelValue: string | string[] | undefined
9
+ error?: string
10
+ }>()
11
+
12
+ const emit = defineEmits<{
13
+ 'update:modelValue': [value: string | string[]]
14
+ blur: []
15
+ }>()
16
+
17
+ const inputId = computed(() => `field-${props.field.id}`)
18
+ const isMulti = computed(() => props.field.type === 'multiselect')
19
+ const options = computed(() => props.field.options ?? [])
20
+
21
+ function onChange(e: Event): void {
22
+ const sel = e.target as HTMLSelectElement
23
+ if (isMulti.value) {
24
+ const values: string[] = []
25
+ for (const opt of Array.from(sel.selectedOptions)) values.push(opt.value)
26
+ emit('update:modelValue', values)
27
+ } else {
28
+ emit('update:modelValue', sel.value)
29
+ }
30
+ }
31
+ </script>
32
+
33
+ <template>
34
+ <DcsFormFieldWrapper :field="field" :error="error" :input-id="inputId">
35
+ <slot
36
+ name="input"
37
+ :id="inputId"
38
+ :value="modelValue"
39
+ :options="options"
40
+ :on-change="onChange"
41
+ >
42
+ <select
43
+ :id="inputId"
44
+ class="dcs-form-input dcs-form-select"
45
+ :name="field.id"
46
+ :required="field.required"
47
+ :multiple="isMulti"
48
+ :aria-invalid="!!error"
49
+ :value="modelValue ?? (isMulti ? [] : '')"
50
+ @change="onChange"
51
+ @blur="emit('blur')"
52
+ >
53
+ <option v-if="!isMulti" value="" disabled>
54
+ {{ field.placeholder ?? 'Select…' }}
55
+ </option>
56
+ <option v-for="opt in options" :key="opt.value" :value="opt.value">
57
+ {{ opt.label }}
58
+ </option>
59
+ </select>
60
+ </slot>
61
+ </DcsFormFieldWrapper>
62
+ </template>
@@ -1,54 +1,54 @@
1
- <script setup lang="ts">
2
- import { computed } from 'vue'
3
- import type { PortalFormField } from '../types'
4
- import DcsFormFieldWrapper from './DcsFormFieldWrapper.vue'
5
-
6
- const props = defineProps<{
7
- field: PortalFormField
8
- modelValue: string | number | undefined
9
- error?: string
10
- }>()
11
-
12
- const emit = defineEmits<{
13
- 'update:modelValue': [value: string]
14
- blur: []
15
- }>()
16
-
17
- const inputId = computed(() => `field-${props.field.id}`)
18
- const inputType = computed(() => {
19
- switch (props.field.type) {
20
- case 'email':
21
- return 'email'
22
- case 'tel':
23
- return 'tel'
24
- default:
25
- return 'text'
26
- }
27
- })
28
- </script>
29
-
30
- <template>
31
- <DcsFormFieldWrapper :field="field" :error="error" :input-id="inputId">
32
- <slot
33
- name="input"
34
- :id="inputId"
35
- :value="modelValue"
36
- :on-input="(e: Event) => emit('update:modelValue', (e.target as HTMLInputElement).value)"
37
- :on-blur="() => emit('blur')"
38
- >
39
- <input
40
- :id="inputId"
41
- class="dcs-form-input"
42
- :type="inputType"
43
- :name="field.id"
44
- :value="modelValue ?? ''"
45
- :placeholder="field.placeholder"
46
- :required="field.required"
47
- :aria-invalid="!!error"
48
- :aria-describedby="error ? `${inputId}-error` : undefined"
49
- @input="emit('update:modelValue', ($event.target as HTMLInputElement).value)"
50
- @blur="emit('blur')"
51
- />
52
- </slot>
53
- </DcsFormFieldWrapper>
54
- </template>
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import type { PortalFormField } from '../types'
4
+ import DcsFormFieldWrapper from './DcsFormFieldWrapper.vue'
5
+
6
+ const props = defineProps<{
7
+ field: PortalFormField
8
+ modelValue: string | number | undefined
9
+ error?: string
10
+ }>()
11
+
12
+ const emit = defineEmits<{
13
+ 'update:modelValue': [value: string]
14
+ blur: []
15
+ }>()
16
+
17
+ const inputId = computed(() => `field-${props.field.id}`)
18
+ const inputType = computed(() => {
19
+ switch (props.field.type) {
20
+ case 'email':
21
+ return 'email'
22
+ case 'tel':
23
+ return 'tel'
24
+ default:
25
+ return 'text'
26
+ }
27
+ })
28
+ </script>
29
+
30
+ <template>
31
+ <DcsFormFieldWrapper :field="field" :error="error" :input-id="inputId">
32
+ <slot
33
+ name="input"
34
+ :id="inputId"
35
+ :value="modelValue"
36
+ :on-input="(e: Event) => emit('update:modelValue', (e.target as HTMLInputElement).value)"
37
+ :on-blur="() => emit('blur')"
38
+ >
39
+ <input
40
+ :id="inputId"
41
+ class="dcs-form-input"
42
+ :type="inputType"
43
+ :name="field.id"
44
+ :value="modelValue ?? ''"
45
+ :placeholder="field.placeholder"
46
+ :required="field.required"
47
+ :aria-invalid="!!error"
48
+ :aria-describedby="error ? `${inputId}-error` : undefined"
49
+ @input="emit('update:modelValue', ($event.target as HTMLInputElement).value)"
50
+ @blur="emit('blur')"
51
+ />
52
+ </slot>
53
+ </DcsFormFieldWrapper>
54
+ </template>