@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
|
@@ -1,113 +1,113 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
DcsFormSubmitPayload,
|
|
3
|
-
DcsFormSubmitSuccess,
|
|
4
|
-
DcsFormSubmitError,
|
|
5
|
-
} from '../types'
|
|
6
|
-
|
|
7
|
-
export interface SubmitOptions {
|
|
8
|
-
apiBase: string
|
|
9
|
-
siteSlug: string
|
|
10
|
-
payload: DcsFormSubmitPayload
|
|
11
|
-
/** Number of retries on 5xx / network errors. Default 1. */
|
|
12
|
-
retries?: number
|
|
13
|
-
/** Optional fetch implementation override (tests). */
|
|
14
|
-
fetchImpl?: typeof fetch
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* POSTs a form submission to
|
|
19
|
-
* `${apiBase}/
|
|
20
|
-
*
|
|
21
|
-
* Uses JSON for plain values and `multipart/form-data` when any
|
|
22
|
-
* value is a `File` (file-upload fields).
|
|
23
|
-
*/
|
|
24
|
-
export async function submitFormValues(
|
|
25
|
-
opts: SubmitOptions,
|
|
26
|
-
): Promise<DcsFormSubmitSuccess> {
|
|
27
|
-
const { apiBase, siteSlug, payload } = opts
|
|
28
|
-
const retries = opts.retries ?? 1
|
|
29
|
-
const fetchImpl = opts.fetchImpl ?? fetch
|
|
30
|
-
const url = `${apiBase.replace(/\/$/, '')}/
|
|
31
|
-
siteSlug,
|
|
32
|
-
)}/
|
|
33
|
-
|
|
34
|
-
const hasFile = Object.values(payload.values).some(
|
|
35
|
-
(v) => typeof File !== 'undefined' && v instanceof File,
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
let lastError: Error | undefined
|
|
39
|
-
let lastStatus: number | undefined
|
|
40
|
-
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
41
|
-
try {
|
|
42
|
-
const init: RequestInit = hasFile
|
|
43
|
-
? { method: 'POST', body: buildFormData(payload) }
|
|
44
|
-
: {
|
|
45
|
-
method: 'POST',
|
|
46
|
-
headers: { 'Content-Type': 'application/json' },
|
|
47
|
-
body: JSON.stringify(payload),
|
|
48
|
-
}
|
|
49
|
-
const res = await fetchImpl(url, init)
|
|
50
|
-
lastStatus = res.status
|
|
51
|
-
if (res.ok) {
|
|
52
|
-
const body = await safeJson(res)
|
|
53
|
-
return { payload, response: body }
|
|
54
|
-
}
|
|
55
|
-
// Retry only on 5xx
|
|
56
|
-
if (res.status < 500) {
|
|
57
|
-
const body = await safeText(res)
|
|
58
|
-
const err: DcsFormSubmitError = {
|
|
59
|
-
payload,
|
|
60
|
-
status: res.status,
|
|
61
|
-
error: new Error(`Submission failed (${res.status}): ${body}`),
|
|
62
|
-
}
|
|
63
|
-
throw err
|
|
64
|
-
}
|
|
65
|
-
lastError = new Error(`Server error ${res.status}`)
|
|
66
|
-
} catch (e) {
|
|
67
|
-
// If `e` is already a DcsFormSubmitError-shaped object, rethrow.
|
|
68
|
-
if (e && typeof e === 'object' && 'payload' in e && 'error' in e) {
|
|
69
|
-
throw e
|
|
70
|
-
}
|
|
71
|
-
lastError = e as Error
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
throw {
|
|
76
|
-
payload,
|
|
77
|
-
status: lastStatus,
|
|
78
|
-
error: lastError ?? new Error('Submission failed'),
|
|
79
|
-
} satisfies DcsFormSubmitError
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function buildFormData(payload: DcsFormSubmitPayload): FormData {
|
|
83
|
-
const fd = new FormData()
|
|
84
|
-
fd.append('formId', payload.formId)
|
|
85
|
-
if (payload.captchaToken) fd.append('captchaToken', payload.captchaToken)
|
|
86
|
-
for (const [key, value] of Object.entries(payload.values)) {
|
|
87
|
-
if (value === undefined || value === null) continue
|
|
88
|
-
if (value instanceof File) {
|
|
89
|
-
fd.append(`values[${key}]`, value)
|
|
90
|
-
} else if (Array.isArray(value)) {
|
|
91
|
-
for (const item of value) fd.append(`values[${key}][]`, String(item))
|
|
92
|
-
} else {
|
|
93
|
-
fd.append(`values[${key}]`, String(value))
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
return fd
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
async function safeJson(res: Response): Promise<unknown> {
|
|
100
|
-
try {
|
|
101
|
-
return await res.json()
|
|
102
|
-
} catch {
|
|
103
|
-
return null
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
async function safeText(res: Response): Promise<string> {
|
|
108
|
-
try {
|
|
109
|
-
return await res.text()
|
|
110
|
-
} catch {
|
|
111
|
-
return ''
|
|
112
|
-
}
|
|
113
|
-
}
|
|
1
|
+
import type {
|
|
2
|
+
DcsFormSubmitPayload,
|
|
3
|
+
DcsFormSubmitSuccess,
|
|
4
|
+
DcsFormSubmitError,
|
|
5
|
+
} from '../types'
|
|
6
|
+
|
|
7
|
+
export interface SubmitOptions {
|
|
8
|
+
apiBase: string
|
|
9
|
+
siteSlug: string
|
|
10
|
+
payload: DcsFormSubmitPayload
|
|
11
|
+
/** Number of retries on 5xx / network errors. Default 1. */
|
|
12
|
+
retries?: number
|
|
13
|
+
/** Optional fetch implementation override (tests). */
|
|
14
|
+
fetchImpl?: typeof fetch
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* POSTs a form submission to
|
|
19
|
+
* `${apiBase}/sites/{siteSlug}/forms/{formId}/submissions`.
|
|
20
|
+
*
|
|
21
|
+
* Uses JSON for plain values and `multipart/form-data` when any
|
|
22
|
+
* value is a `File` (file-upload fields).
|
|
23
|
+
*/
|
|
24
|
+
export async function submitFormValues(
|
|
25
|
+
opts: SubmitOptions,
|
|
26
|
+
): Promise<DcsFormSubmitSuccess> {
|
|
27
|
+
const { apiBase, siteSlug, payload } = opts
|
|
28
|
+
const retries = opts.retries ?? 1
|
|
29
|
+
const fetchImpl = opts.fetchImpl ?? fetch
|
|
30
|
+
const url = `${apiBase.replace(/\/$/, '')}/sites/${encodeURIComponent(
|
|
31
|
+
siteSlug,
|
|
32
|
+
)}/forms/${encodeURIComponent(payload.formId)}/submissions`
|
|
33
|
+
|
|
34
|
+
const hasFile = Object.values(payload.values).some(
|
|
35
|
+
(v) => typeof File !== 'undefined' && v instanceof File,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
let lastError: Error | undefined
|
|
39
|
+
let lastStatus: number | undefined
|
|
40
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
41
|
+
try {
|
|
42
|
+
const init: RequestInit = hasFile
|
|
43
|
+
? { method: 'POST', body: buildFormData(payload) }
|
|
44
|
+
: {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
headers: { 'Content-Type': 'application/json' },
|
|
47
|
+
body: JSON.stringify(payload),
|
|
48
|
+
}
|
|
49
|
+
const res = await fetchImpl(url, init)
|
|
50
|
+
lastStatus = res.status
|
|
51
|
+
if (res.ok) {
|
|
52
|
+
const body = await safeJson(res)
|
|
53
|
+
return { payload, response: body }
|
|
54
|
+
}
|
|
55
|
+
// Retry only on 5xx
|
|
56
|
+
if (res.status < 500) {
|
|
57
|
+
const body = await safeText(res)
|
|
58
|
+
const err: DcsFormSubmitError = {
|
|
59
|
+
payload,
|
|
60
|
+
status: res.status,
|
|
61
|
+
error: new Error(`Submission failed (${res.status}): ${body}`),
|
|
62
|
+
}
|
|
63
|
+
throw err
|
|
64
|
+
}
|
|
65
|
+
lastError = new Error(`Server error ${res.status}`)
|
|
66
|
+
} catch (e) {
|
|
67
|
+
// If `e` is already a DcsFormSubmitError-shaped object, rethrow.
|
|
68
|
+
if (e && typeof e === 'object' && 'payload' in e && 'error' in e) {
|
|
69
|
+
throw e
|
|
70
|
+
}
|
|
71
|
+
lastError = e as Error
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
throw {
|
|
76
|
+
payload,
|
|
77
|
+
status: lastStatus,
|
|
78
|
+
error: lastError ?? new Error('Submission failed'),
|
|
79
|
+
} satisfies DcsFormSubmitError
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function buildFormData(payload: DcsFormSubmitPayload): FormData {
|
|
83
|
+
const fd = new FormData()
|
|
84
|
+
fd.append('formId', payload.formId)
|
|
85
|
+
if (payload.captchaToken) fd.append('captchaToken', payload.captchaToken)
|
|
86
|
+
for (const [key, value] of Object.entries(payload.values)) {
|
|
87
|
+
if (value === undefined || value === null) continue
|
|
88
|
+
if (value instanceof File) {
|
|
89
|
+
fd.append(`values[${key}]`, value)
|
|
90
|
+
} else if (Array.isArray(value)) {
|
|
91
|
+
for (const item of value) fd.append(`values[${key}][]`, String(item))
|
|
92
|
+
} else {
|
|
93
|
+
fd.append(`values[${key}]`, String(value))
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return fd
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function safeJson(res: Response): Promise<unknown> {
|
|
100
|
+
try {
|
|
101
|
+
return await res.json()
|
|
102
|
+
} catch {
|
|
103
|
+
return null
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function safeText(res: Response): Promise<string> {
|
|
108
|
+
try {
|
|
109
|
+
return await res.text()
|
|
110
|
+
} catch {
|
|
111
|
+
return ''
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -1,127 +1,128 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
PortalFormDefinition,
|
|
3
|
-
PortalFormField,
|
|
4
|
-
FormErrors,
|
|
5
|
-
FormValues,
|
|
6
|
-
} from '../types'
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Returns true iff `field` is currently visible given the form's
|
|
10
|
-
* other values. Hidden fields are never validated and never sent.
|
|
11
|
-
*/
|
|
12
|
-
export function isFieldVisible(
|
|
13
|
-
field: PortalFormField,
|
|
14
|
-
values: FormValues,
|
|
15
|
-
): boolean {
|
|
16
|
-
if (!field.visibleIf) return true
|
|
17
|
-
const sibling = values[field.visibleIf.fieldId]
|
|
18
|
-
return sibling === field.visibleIf.equals
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Validates a single field's value. Returns an error string or
|
|
23
|
-
* `undefined` if valid. Layout-only field types (section-heading,
|
|
24
|
-
* html-block) and hidden fields never produce errors.
|
|
25
|
-
*/
|
|
26
|
-
export function validateField(
|
|
27
|
-
field: PortalFormField,
|
|
28
|
-
value: unknown,
|
|
29
|
-
): string | undefined {
|
|
30
|
-
if (
|
|
31
|
-
field.type === 'section-heading' ||
|
|
32
|
-
field.type === 'html-block' ||
|
|
33
|
-
field.type === 'hidden'
|
|
34
|
-
) {
|
|
35
|
-
return undefined
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
value ===
|
|
41
|
-
value ===
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (!
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
1
|
+
import type {
|
|
2
|
+
PortalFormDefinition,
|
|
3
|
+
PortalFormField,
|
|
4
|
+
FormErrors,
|
|
5
|
+
FormValues,
|
|
6
|
+
} from '../types'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Returns true iff `field` is currently visible given the form's
|
|
10
|
+
* other values. Hidden fields are never validated and never sent.
|
|
11
|
+
*/
|
|
12
|
+
export function isFieldVisible(
|
|
13
|
+
field: PortalFormField,
|
|
14
|
+
values: FormValues,
|
|
15
|
+
): boolean {
|
|
16
|
+
if (!field.visibleIf) return true
|
|
17
|
+
const sibling = values[field.visibleIf.fieldId]
|
|
18
|
+
return sibling === field.visibleIf.equals
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Validates a single field's value. Returns an error string or
|
|
23
|
+
* `undefined` if valid. Layout-only field types (section-heading,
|
|
24
|
+
* html-block) and hidden fields never produce errors.
|
|
25
|
+
*/
|
|
26
|
+
export function validateField(
|
|
27
|
+
field: PortalFormField,
|
|
28
|
+
value: unknown,
|
|
29
|
+
): string | undefined {
|
|
30
|
+
if (
|
|
31
|
+
field.type === 'section-heading' ||
|
|
32
|
+
field.type === 'html-block' ||
|
|
33
|
+
field.type === 'hidden'
|
|
34
|
+
) {
|
|
35
|
+
return undefined
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const normalizedValue = typeof value === 'string' ? value.trim() : value
|
|
39
|
+
const isEmpty =
|
|
40
|
+
value === undefined ||
|
|
41
|
+
value === null ||
|
|
42
|
+
normalizedValue === '' ||
|
|
43
|
+
(Array.isArray(value) && value.length === 0)
|
|
44
|
+
|
|
45
|
+
if (field.required && isEmpty) {
|
|
46
|
+
return `${field.label} is required`
|
|
47
|
+
}
|
|
48
|
+
if (isEmpty) return undefined
|
|
49
|
+
|
|
50
|
+
// Type-specific built-ins
|
|
51
|
+
if (field.type === 'email' && typeof normalizedValue === 'string') {
|
|
52
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizedValue)) {
|
|
53
|
+
return `${field.label} must be a valid email address`
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const v = field.validation
|
|
58
|
+
if (!v) return undefined
|
|
59
|
+
|
|
60
|
+
if (typeof normalizedValue === 'string') {
|
|
61
|
+
if (v.minLength != null && normalizedValue.length < v.minLength) {
|
|
62
|
+
return `${field.label} must be at least ${v.minLength} characters`
|
|
63
|
+
}
|
|
64
|
+
if (v.maxLength != null && normalizedValue.length > v.maxLength) {
|
|
65
|
+
return `${field.label} must be at most ${v.maxLength} characters`
|
|
66
|
+
}
|
|
67
|
+
if (v.regex) {
|
|
68
|
+
try {
|
|
69
|
+
if (!new RegExp(v.regex).test(normalizedValue)) {
|
|
70
|
+
return `${field.label} is invalid`
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
// bad regex in the definition — treat as no rule
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (typeof value === 'number') {
|
|
79
|
+
if (v.min != null && value < v.min) {
|
|
80
|
+
return `${field.label} must be at least ${v.min}`
|
|
81
|
+
}
|
|
82
|
+
if (v.max != null && value > v.max) {
|
|
83
|
+
return `${field.label} must be at most ${v.max}`
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (field.type === 'file' && v.accept && v.accept.length > 0) {
|
|
88
|
+
const file = value as File | null
|
|
89
|
+
if (file && typeof File !== 'undefined' && file instanceof File) {
|
|
90
|
+
const accepted = v.accept.some((a) => {
|
|
91
|
+
if (a.startsWith('.')) {
|
|
92
|
+
return file.name.toLowerCase().endsWith(a.toLowerCase())
|
|
93
|
+
}
|
|
94
|
+
return file.type === a
|
|
95
|
+
})
|
|
96
|
+
if (!accepted) {
|
|
97
|
+
return `${field.label} must be one of: ${v.accept.join(', ')}`
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return undefined
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Validates an entire form. Skips fields that are not visible.
|
|
107
|
+
* If `fieldIds` is supplied, only those fields are validated
|
|
108
|
+
* (used for per-step validation in multi-step forms).
|
|
109
|
+
*/
|
|
110
|
+
export function validateForm(
|
|
111
|
+
def: PortalFormDefinition,
|
|
112
|
+
values: FormValues,
|
|
113
|
+
fieldIds?: string[],
|
|
114
|
+
): FormErrors {
|
|
115
|
+
const errors: FormErrors = {}
|
|
116
|
+
const ids = fieldIds ? new Set(fieldIds) : null
|
|
117
|
+
for (const field of def.fields) {
|
|
118
|
+
if (ids && !ids.has(field.id)) continue
|
|
119
|
+
if (!isFieldVisible(field, values)) continue
|
|
120
|
+
const err = validateField(field, values[field.id])
|
|
121
|
+
if (err) errors[field.id] = err
|
|
122
|
+
}
|
|
123
|
+
return errors
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function hasErrors(errors: FormErrors): boolean {
|
|
127
|
+
return Object.values(errors).some((e) => !!e)
|
|
128
|
+
}
|
|
@@ -1,35 +1,35 @@
|
|
|
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: boolean | undefined
|
|
9
|
-
error?: string
|
|
10
|
-
}>()
|
|
11
|
-
|
|
12
|
-
const emit = defineEmits<{
|
|
13
|
-
'update:modelValue': [value: boolean]
|
|
14
|
-
}>()
|
|
15
|
-
|
|
16
|
-
const inputId = computed(() => `field-${props.field.id}`)
|
|
17
|
-
</script>
|
|
18
|
-
|
|
19
|
-
<template>
|
|
20
|
-
<DcsFormFieldWrapper :field="field" :error="error" :input-id="inputId">
|
|
21
|
-
<template #label><span class="sr-only">{{ field.label }}</span></template>
|
|
22
|
-
<label class="dcs-form-checkbox-single" :for="inputId">
|
|
23
|
-
<input
|
|
24
|
-
:id="inputId"
|
|
25
|
-
type="checkbox"
|
|
26
|
-
:name="field.id"
|
|
27
|
-
:checked="!!modelValue"
|
|
28
|
-
:required="field.required"
|
|
29
|
-
:aria-invalid="!!error"
|
|
30
|
-
@change="emit('update:modelValue', ($event.target as HTMLInputElement).checked)"
|
|
31
|
-
/>
|
|
32
|
-
<span>{{ field.label }}<span v-if="field.required" aria-hidden="true"> *</span></span>
|
|
33
|
-
</label>
|
|
34
|
-
</DcsFormFieldWrapper>
|
|
35
|
-
</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: boolean | undefined
|
|
9
|
+
error?: string
|
|
10
|
+
}>()
|
|
11
|
+
|
|
12
|
+
const emit = defineEmits<{
|
|
13
|
+
'update:modelValue': [value: boolean]
|
|
14
|
+
}>()
|
|
15
|
+
|
|
16
|
+
const inputId = computed(() => `field-${props.field.id}`)
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<template>
|
|
20
|
+
<DcsFormFieldWrapper :field="field" :error="error" :input-id="inputId">
|
|
21
|
+
<template #label><span class="sr-only">{{ field.label }}</span></template>
|
|
22
|
+
<label class="dcs-form-checkbox-single" :for="inputId">
|
|
23
|
+
<input
|
|
24
|
+
:id="inputId"
|
|
25
|
+
type="checkbox"
|
|
26
|
+
:name="field.id"
|
|
27
|
+
:checked="!!modelValue"
|
|
28
|
+
:required="field.required"
|
|
29
|
+
:aria-invalid="!!error"
|
|
30
|
+
@change="emit('update:modelValue', ($event.target as HTMLInputElement).checked)"
|
|
31
|
+
/>
|
|
32
|
+
<span>{{ field.label }}<span v-if="field.required" aria-hidden="true"> *</span></span>
|
|
33
|
+
</label>
|
|
34
|
+
</DcsFormFieldWrapper>
|
|
35
|
+
</template>
|
|
@@ -1,52 +1,52 @@
|
|
|
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
|
-
const current = computed(() => props.modelValue ?? [])
|
|
19
|
-
|
|
20
|
-
function toggle(value: string, checked: boolean): void {
|
|
21
|
-
const set = new Set(current.value)
|
|
22
|
-
if (checked) set.add(value)
|
|
23
|
-
else set.delete(value)
|
|
24
|
-
emit('update:modelValue', Array.from(set))
|
|
25
|
-
}
|
|
26
|
-
</script>
|
|
27
|
-
|
|
28
|
-
<template>
|
|
29
|
-
<DcsFormFieldWrapper :field="field" :error="error" :input-id="inputId">
|
|
30
|
-
<fieldset
|
|
31
|
-
class="dcs-form-checkbox-group"
|
|
32
|
-
:aria-required="field.required"
|
|
33
|
-
:aria-invalid="!!error"
|
|
34
|
-
>
|
|
35
|
-
<legend class="sr-only">{{ field.label }}</legend>
|
|
36
|
-
<label
|
|
37
|
-
v-for="opt in options"
|
|
38
|
-
:key="opt.value"
|
|
39
|
-
class="dcs-form-checkbox"
|
|
40
|
-
>
|
|
41
|
-
<input
|
|
42
|
-
type="checkbox"
|
|
43
|
-
:name="`${field.id}[]`"
|
|
44
|
-
:value="opt.value"
|
|
45
|
-
:checked="current.includes(opt.value)"
|
|
46
|
-
@change="toggle(opt.value, ($event.target as HTMLInputElement).checked)"
|
|
47
|
-
/>
|
|
48
|
-
<span>{{ opt.label }}</span>
|
|
49
|
-
</label>
|
|
50
|
-
</fieldset>
|
|
51
|
-
</DcsFormFieldWrapper>
|
|
52
|
-
</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
|
+
const current = computed(() => props.modelValue ?? [])
|
|
19
|
+
|
|
20
|
+
function toggle(value: string, checked: boolean): void {
|
|
21
|
+
const set = new Set(current.value)
|
|
22
|
+
if (checked) set.add(value)
|
|
23
|
+
else set.delete(value)
|
|
24
|
+
emit('update:modelValue', Array.from(set))
|
|
25
|
+
}
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<template>
|
|
29
|
+
<DcsFormFieldWrapper :field="field" :error="error" :input-id="inputId">
|
|
30
|
+
<fieldset
|
|
31
|
+
class="dcs-form-checkbox-group"
|
|
32
|
+
:aria-required="field.required"
|
|
33
|
+
:aria-invalid="!!error"
|
|
34
|
+
>
|
|
35
|
+
<legend class="sr-only">{{ field.label }}</legend>
|
|
36
|
+
<label
|
|
37
|
+
v-for="opt in options"
|
|
38
|
+
:key="opt.value"
|
|
39
|
+
class="dcs-form-checkbox"
|
|
40
|
+
>
|
|
41
|
+
<input
|
|
42
|
+
type="checkbox"
|
|
43
|
+
:name="`${field.id}[]`"
|
|
44
|
+
:value="opt.value"
|
|
45
|
+
:checked="current.includes(opt.value)"
|
|
46
|
+
@change="toggle(opt.value, ($event.target as HTMLInputElement).checked)"
|
|
47
|
+
/>
|
|
48
|
+
<span>{{ opt.label }}</span>
|
|
49
|
+
</label>
|
|
50
|
+
</fieldset>
|
|
51
|
+
</DcsFormFieldWrapper>
|
|
52
|
+
</template>
|