@rokkit/forms 1.0.0-next.122
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/dist/src/forms-old/input/types.d.ts +7 -0
- package/dist/src/forms-old/lib/form.d.ts +95 -0
- package/dist/src/forms-old/lib/index.d.ts +1 -0
- package/dist/src/index.d.ts +5 -0
- package/dist/src/input/index.d.ts +18 -0
- package/dist/src/lib/builder.svelte.d.ts +140 -0
- package/dist/src/lib/deprecated/nested.d.ts +48 -0
- package/dist/src/lib/deprecated/nested.spec.d.ts +1 -0
- package/dist/src/lib/deprecated/validator.d.ts +30 -0
- package/dist/src/lib/deprecated/validator.spec.d.ts +1 -0
- package/dist/src/lib/fields.d.ts +16 -0
- package/dist/src/lib/fields.spec.d.ts +1 -0
- package/dist/src/lib/index.d.ts +7 -0
- package/dist/src/lib/layout.d.ts +7 -0
- package/dist/src/lib/schema.d.ts +7 -0
- package/dist/src/lib/validation.d.ts +41 -0
- package/dist/src/types.d.ts +5 -0
- package/package.json +38 -0
- package/src/DataEditor.svelte +30 -0
- package/src/FieldLayout.svelte +48 -0
- package/src/FormRenderer.svelte +118 -0
- package/src/Input.svelte +75 -0
- package/src/InputField.svelte +55 -0
- package/src/ListEditor.svelte +44 -0
- package/src/NestedEditor.svelte +85 -0
- package/src/forms-old/CheckBox.svelte +56 -0
- package/src/forms-old/DataEditor.svelte +30 -0
- package/src/forms-old/FieldLayout.svelte +48 -0
- package/src/forms-old/Form.svelte +17 -0
- package/src/forms-old/Icon.svelte +76 -0
- package/src/forms-old/Item.svelte +25 -0
- package/src/forms-old/ListEditor.svelte +44 -0
- package/src/forms-old/Tabs.svelte +57 -0
- package/src/forms-old/Wrapper.svelte +12 -0
- package/src/forms-old/input/Input.svelte +17 -0
- package/src/forms-old/input/InputField.svelte +70 -0
- package/src/forms-old/input/InputSelect.svelte +23 -0
- package/src/forms-old/input/InputSwitch.svelte +19 -0
- package/src/forms-old/input/types.js +29 -0
- package/src/forms-old/lib/form.js +72 -0
- package/src/forms-old/lib/index.js +12 -0
- package/src/forms-old/mocks/CustomField.svelte +7 -0
- package/src/forms-old/mocks/CustomWrapper.svelte +8 -0
- package/src/forms-old/mocks/Register.svelte +25 -0
- package/src/index.js +7 -0
- package/src/inp/Input.svelte +17 -0
- package/src/inp/InputField.svelte +69 -0
- package/src/inp/InputSelect.svelte +23 -0
- package/src/inp/InputSwitch.svelte +19 -0
- package/src/input/InputCheckbox.svelte +74 -0
- package/src/input/InputColor.svelte +42 -0
- package/src/input/InputDate.svelte +54 -0
- package/src/input/InputDateTime.svelte +54 -0
- package/src/input/InputEmail.svelte +63 -0
- package/src/input/InputFile.svelte +45 -0
- package/src/input/InputMonth.svelte +54 -0
- package/src/input/InputNumber.svelte +57 -0
- package/src/input/InputPassword.svelte +60 -0
- package/src/input/InputRadio.svelte +60 -0
- package/src/input/InputRange.svelte +51 -0
- package/src/input/InputSelect.svelte +71 -0
- package/src/input/InputSwitch.svelte +29 -0
- package/src/input/InputTel.svelte +60 -0
- package/src/input/InputText.svelte +60 -0
- package/src/input/InputTextArea.svelte +59 -0
- package/src/input/InputTime.svelte +54 -0
- package/src/input/InputUrl.svelte +60 -0
- package/src/input/InputWeek.svelte +54 -0
- package/src/input/index.js +23 -0
- package/src/lib/Input.svelte +291 -0
- package/src/lib/builder.svelte.js +359 -0
- package/src/lib/deprecated/Form.svelte +17 -0
- package/src/lib/deprecated/FormRenderer.svelte +121 -0
- package/src/lib/deprecated/nested.js +192 -0
- package/src/lib/deprecated/nested.spec.js +512 -0
- package/src/lib/deprecated/validator.js +137 -0
- package/src/lib/deprecated/validator.spec.js +348 -0
- package/src/lib/fields.js +119 -0
- package/src/lib/fields.spec.js +250 -0
- package/src/lib/index.js +7 -0
- package/src/lib/layout.js +63 -0
- package/src/lib/schema.js +32 -0
- package/src/lib/validation.js +273 -0
- package/src/types.js +29 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { deriveSchemaFromValue } from './schema.js'
|
|
3
|
+
import { deriveLayoutFromValue } from './layout.js'
|
|
4
|
+
import { findAttributeByPath, getSchemaWithLayout } from './fields.js'
|
|
5
|
+
|
|
6
|
+
import inputLayout from './fixtures/input-layout.json'
|
|
7
|
+
import inputSchema from './fixtures/input-schema.json'
|
|
8
|
+
import resultWithGeneratedSchema from './fixtures/result-with-generated-schema.json'
|
|
9
|
+
import resultWithGeneratedLayout from './fixtures/result-with-generated-layout.json'
|
|
10
|
+
import combinedSchemaLayout from './fixtures/combined-schema-layout.json'
|
|
11
|
+
import resultWithOnlyData from './fixtures/result-with-only-data.json'
|
|
12
|
+
|
|
13
|
+
describe('fields', () => {
|
|
14
|
+
describe('findAttributeByPath', () => {
|
|
15
|
+
it('should find attribute from path', () => {
|
|
16
|
+
const schema = {
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {
|
|
19
|
+
name: { type: 'string' },
|
|
20
|
+
age: { type: 'integer', min: 0, max: 100 }
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
let attribute = findAttributeByPath('#/name', schema)
|
|
24
|
+
expect(attribute).toEqual({
|
|
25
|
+
key: 'name',
|
|
26
|
+
props: {
|
|
27
|
+
type: 'string'
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
attribute = findAttributeByPath('#/age', schema)
|
|
32
|
+
expect(attribute).toEqual({
|
|
33
|
+
key: 'age',
|
|
34
|
+
props: {
|
|
35
|
+
type: 'integer',
|
|
36
|
+
min: 0,
|
|
37
|
+
max: 100
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('should find attribute from nested path', () => {
|
|
43
|
+
const schema = {
|
|
44
|
+
type: 'object',
|
|
45
|
+
properties: {
|
|
46
|
+
address: {
|
|
47
|
+
type: 'object',
|
|
48
|
+
properties: {
|
|
49
|
+
street: { type: 'string' },
|
|
50
|
+
city: { type: 'string' },
|
|
51
|
+
state: { type: 'string' },
|
|
52
|
+
zip: { type: 'integer' }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
let attribute = findAttributeByPath('#/address/street', schema)
|
|
58
|
+
expect(attribute).toEqual({
|
|
59
|
+
key: 'street',
|
|
60
|
+
props: {
|
|
61
|
+
type: 'string'
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
attribute = findAttributeByPath('#/address/city', schema)
|
|
66
|
+
expect(attribute).toEqual({
|
|
67
|
+
key: 'city',
|
|
68
|
+
props: {
|
|
69
|
+
type: 'string'
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
attribute = findAttributeByPath('#/address/state', schema)
|
|
74
|
+
expect(attribute).toEqual({
|
|
75
|
+
key: 'state',
|
|
76
|
+
props: {
|
|
77
|
+
type: 'string'
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
attribute = findAttributeByPath('#/address/zip', schema)
|
|
82
|
+
expect(attribute).toEqual({
|
|
83
|
+
key: 'zip',
|
|
84
|
+
props: {
|
|
85
|
+
type: 'integer'
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('should throw error if path is invalid', () => {
|
|
91
|
+
const schema = {
|
|
92
|
+
type: 'object',
|
|
93
|
+
properties: {
|
|
94
|
+
name: { type: 'string' },
|
|
95
|
+
age: { type: 'integer', min: 0, max: 100 }
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
expect(() => {
|
|
99
|
+
findAttributeByPath('#/invalid', schema)
|
|
100
|
+
}).toThrowError('Invalid scope: #/invalid')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('should return props if scope is missing', () => {
|
|
104
|
+
const schema = {
|
|
105
|
+
type: 'object',
|
|
106
|
+
properties: {
|
|
107
|
+
name: { type: 'string' },
|
|
108
|
+
age: { type: 'integer', min: 0, max: 100 }
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const attribute = findAttributeByPath(null, schema)
|
|
112
|
+
expect(attribute).toEqual({
|
|
113
|
+
props: schema
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
describe('getSchemaWithLayout', () => {
|
|
119
|
+
const value = {
|
|
120
|
+
name: 'John',
|
|
121
|
+
age: 30,
|
|
122
|
+
address: {
|
|
123
|
+
street: '123 Main St',
|
|
124
|
+
city: 'New York',
|
|
125
|
+
state: 'NY',
|
|
126
|
+
zip: 10001
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const schema = inputSchema
|
|
130
|
+
const layout = inputLayout
|
|
131
|
+
|
|
132
|
+
it('should combine generated schema and layout', () => {
|
|
133
|
+
const value = {
|
|
134
|
+
name: 'John',
|
|
135
|
+
age: 30
|
|
136
|
+
}
|
|
137
|
+
const schema = getSchemaWithLayout(deriveSchemaFromValue(value), deriveLayoutFromValue(value))
|
|
138
|
+
expect(schema).toEqual({
|
|
139
|
+
type: 'vertical',
|
|
140
|
+
elements: [
|
|
141
|
+
{
|
|
142
|
+
key: 'name',
|
|
143
|
+
|
|
144
|
+
props: {
|
|
145
|
+
label: 'name',
|
|
146
|
+
type: 'string'
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
key: 'age',
|
|
151
|
+
|
|
152
|
+
props: {
|
|
153
|
+
label: 'age',
|
|
154
|
+
type: 'integer'
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
]
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('should combine generated schema & layout for nested data', () => {
|
|
162
|
+
const schema = getSchemaWithLayout(deriveSchemaFromValue(value), deriveLayoutFromValue(value))
|
|
163
|
+
expect(schema).toEqual(resultWithOnlyData)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('should combine schema with generated layout', () => {
|
|
167
|
+
const combined = getSchemaWithLayout(schema, deriveLayoutFromValue(value))
|
|
168
|
+
expect(combined).toEqual(resultWithGeneratedLayout)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('should combine layout with generated schema', () => {
|
|
172
|
+
const combined = getSchemaWithLayout(deriveSchemaFromValue(value), layout)
|
|
173
|
+
expect(combined).toEqual(resultWithGeneratedSchema)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('should combine the schema and layout', () => {
|
|
177
|
+
const combined = getSchemaWithLayout(schema, layout)
|
|
178
|
+
expect(combined).toEqual(combinedSchemaLayout)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('should combine the schema and layout for nested array', () => {
|
|
182
|
+
const schema = {
|
|
183
|
+
type: 'object',
|
|
184
|
+
properties: {
|
|
185
|
+
users: {
|
|
186
|
+
type: 'array',
|
|
187
|
+
items: {
|
|
188
|
+
type: 'object',
|
|
189
|
+
properties: {
|
|
190
|
+
name: { type: 'string' },
|
|
191
|
+
age: { type: 'integer' }
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const layout = {
|
|
198
|
+
type: 'vertical',
|
|
199
|
+
elements: [
|
|
200
|
+
{
|
|
201
|
+
scope: '#/users',
|
|
202
|
+
schema: {
|
|
203
|
+
type: 'vertical',
|
|
204
|
+
|
|
205
|
+
elements: [
|
|
206
|
+
{
|
|
207
|
+
scope: '#/name',
|
|
208
|
+
label: 'name'
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
scope: '#/age',
|
|
212
|
+
label: 'age'
|
|
213
|
+
}
|
|
214
|
+
]
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
]
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const combined = getSchemaWithLayout(schema, layout)
|
|
221
|
+
expect(combined).toEqual({
|
|
222
|
+
type: 'vertical',
|
|
223
|
+
elements: [
|
|
224
|
+
{
|
|
225
|
+
key: 'users',
|
|
226
|
+
props: {
|
|
227
|
+
type: 'array',
|
|
228
|
+
schema: {
|
|
229
|
+
type: 'vertical',
|
|
230
|
+
elements: [
|
|
231
|
+
{
|
|
232
|
+
key: 'name',
|
|
233
|
+
props: {
|
|
234
|
+
label: 'name',
|
|
235
|
+
type: 'string'
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
key: 'age',
|
|
240
|
+
props: { label: 'age', type: 'integer' }
|
|
241
|
+
}
|
|
242
|
+
]
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
]
|
|
247
|
+
})
|
|
248
|
+
})
|
|
249
|
+
})
|
|
250
|
+
})
|
package/src/lib/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { FormBuilder } from './builder.svelte.js'
|
|
2
|
+
export { validateField, validateAll, createMessage, patterns } from './validation.js'
|
|
3
|
+
export { default as FormRenderer } from './FormRenderer.svelte'
|
|
4
|
+
export { default as Input } from './Input.svelte'
|
|
5
|
+
export * from './schema.js'
|
|
6
|
+
export * from './layout.js'
|
|
7
|
+
export * from './fields.js'
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { isObject } from '@rokkit/core'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Derives a layout from a given object value.
|
|
5
|
+
* @param {Object} val
|
|
6
|
+
* @param {string} scope
|
|
7
|
+
* @param {string} label
|
|
8
|
+
* @returns {import('../types').DataLayout}
|
|
9
|
+
*/
|
|
10
|
+
function deriveElementLayout(val, scope, label) {
|
|
11
|
+
const path = `${scope}/${label}`
|
|
12
|
+
if (isObject(val)) {
|
|
13
|
+
return {
|
|
14
|
+
title: label,
|
|
15
|
+
scope: path,
|
|
16
|
+
// eslint-disable-next-line no-use-before-define
|
|
17
|
+
...deriveLayoutFromValue(val, path)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return { label, scope: path }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Derives a layout from a given object value.
|
|
25
|
+
* @param {Object} value
|
|
26
|
+
* @param {string} scope
|
|
27
|
+
* @returns {import('../types').DataLayout}
|
|
28
|
+
*/
|
|
29
|
+
function deriveObjectLayout(value, scope) {
|
|
30
|
+
const elements = Object.entries(value).map(([label, val]) =>
|
|
31
|
+
deriveElementLayout(val, scope, label)
|
|
32
|
+
)
|
|
33
|
+
return { type: 'vertical', elements }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Derives a layout from a given array value.
|
|
38
|
+
* @param {Array<any>} value
|
|
39
|
+
* @param {string} scope
|
|
40
|
+
* @returns {import('../types').DataLayout}
|
|
41
|
+
*/
|
|
42
|
+
function deriveArrayLayout(value, scope) {
|
|
43
|
+
// eslint-disable-next-line no-use-before-define
|
|
44
|
+
const schema = deriveLayoutFromValue(value[0], '#')
|
|
45
|
+
return {
|
|
46
|
+
scope,
|
|
47
|
+
schema
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Derives a layout from a given value.
|
|
52
|
+
* @param {any} value
|
|
53
|
+
* @param {string} scope
|
|
54
|
+
* @returns {import('../types').DataLayout}
|
|
55
|
+
*/
|
|
56
|
+
export function deriveLayoutFromValue(value, scope = '#') {
|
|
57
|
+
if (Array.isArray(value)) {
|
|
58
|
+
return deriveArrayLayout(value, scope)
|
|
59
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
60
|
+
return deriveObjectLayout(value, scope)
|
|
61
|
+
}
|
|
62
|
+
return { type: 'vertical', elements: [{ scope }] }
|
|
63
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { typeOf } from '@rokkit/data'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Derives a schema for properties of an object.
|
|
5
|
+
*
|
|
6
|
+
* @param {Object} data
|
|
7
|
+
* @returns
|
|
8
|
+
*/
|
|
9
|
+
function deriveObjectProperties(data) {
|
|
10
|
+
const properties = {}
|
|
11
|
+
for (const [key, value] of Object.entries(data)) {
|
|
12
|
+
// eslint-disable-next-line no-use-before-define
|
|
13
|
+
properties[key] = deriveSchemaFromValue(value)
|
|
14
|
+
}
|
|
15
|
+
return properties
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Derives a schema from a given value.
|
|
20
|
+
*
|
|
21
|
+
* @param {any} data
|
|
22
|
+
* @returns {import('../types').DataSchema}
|
|
23
|
+
*/
|
|
24
|
+
export function deriveSchemaFromValue(data) {
|
|
25
|
+
const schema = { type: typeOf(data) }
|
|
26
|
+
if (schema.type === 'array') {
|
|
27
|
+
schema.items = deriveSchemaFromValue(data.length > 0 ? data[0] : {})
|
|
28
|
+
} else if (schema.type === 'object') {
|
|
29
|
+
schema.properties = deriveObjectProperties(data)
|
|
30
|
+
}
|
|
31
|
+
return schema
|
|
32
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation utility for FormBuilder
|
|
3
|
+
* Provides field validation functions that return message objects
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {Object} ValidationMessage
|
|
8
|
+
* @property {string} state - Message state: 'error', 'warning', 'info', 'success'
|
|
9
|
+
* @property {string} text - Message text content
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Helper function to check if a value is empty
|
|
14
|
+
* @private
|
|
15
|
+
* @param {any} value - Value to check
|
|
16
|
+
* @returns {boolean} True if empty
|
|
17
|
+
*/
|
|
18
|
+
function isEmpty(value) {
|
|
19
|
+
return value === undefined || value === null || value === ''
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Validate string field
|
|
24
|
+
* @private
|
|
25
|
+
* @param {string} value - String value
|
|
26
|
+
* @param {Object} schema - Field schema
|
|
27
|
+
* @param {string} label - Field label
|
|
28
|
+
* @returns {ValidationMessage|null}
|
|
29
|
+
*/
|
|
30
|
+
function validateString(value, schema, label) {
|
|
31
|
+
// Convert to string if not already
|
|
32
|
+
const stringValue = String(value)
|
|
33
|
+
|
|
34
|
+
// Pattern validation
|
|
35
|
+
if (schema.pattern) {
|
|
36
|
+
const regex = new RegExp(schema.pattern)
|
|
37
|
+
if (!regex.test(stringValue)) {
|
|
38
|
+
return {
|
|
39
|
+
state: 'error',
|
|
40
|
+
text: `${label} format is invalid`
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Length validations
|
|
46
|
+
if (schema.minLength !== undefined && stringValue.length < schema.minLength) {
|
|
47
|
+
return {
|
|
48
|
+
state: 'error',
|
|
49
|
+
text: `${label} must be at least ${schema.minLength} characters`
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (schema.maxLength !== undefined && stringValue.length > schema.maxLength) {
|
|
54
|
+
return {
|
|
55
|
+
state: 'error',
|
|
56
|
+
text: `${label} must be no more than ${schema.maxLength} characters`
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Enum validation
|
|
61
|
+
if (schema.enum && !schema.enum.includes(stringValue)) {
|
|
62
|
+
return {
|
|
63
|
+
state: 'error',
|
|
64
|
+
text: `${label} must be one of: ${schema.enum.join(', ')}`
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Validate number field
|
|
73
|
+
* @private
|
|
74
|
+
* @param {number} value - Number value
|
|
75
|
+
* @param {Object} schema - Field schema
|
|
76
|
+
* @param {string} label - Field label
|
|
77
|
+
* @returns {ValidationMessage|null}
|
|
78
|
+
*/
|
|
79
|
+
function validateNumber(value, schema, label) {
|
|
80
|
+
const numValue = Number(value)
|
|
81
|
+
|
|
82
|
+
// Check if it's a valid number
|
|
83
|
+
if (isNaN(numValue)) {
|
|
84
|
+
return {
|
|
85
|
+
state: 'error',
|
|
86
|
+
text: `${label} must be a valid number`
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Integer validation
|
|
91
|
+
if (schema.type === 'integer' && !Number.isInteger(numValue)) {
|
|
92
|
+
return {
|
|
93
|
+
state: 'error',
|
|
94
|
+
text: `${label} must be a whole number`
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Minimum validation (support both min and minimum)
|
|
99
|
+
const minimum = schema.min !== undefined ? schema.min : schema.minimum
|
|
100
|
+
if (minimum !== undefined && numValue < minimum) {
|
|
101
|
+
return {
|
|
102
|
+
state: 'error',
|
|
103
|
+
text: `${label} must be at least ${minimum}`
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Maximum validation (support both max and maximum)
|
|
108
|
+
const maximum = schema.max !== undefined ? schema.max : schema.maximum
|
|
109
|
+
if (maximum !== undefined && numValue > maximum) {
|
|
110
|
+
return {
|
|
111
|
+
state: 'error',
|
|
112
|
+
text: `${label} must be no more than ${maximum}`
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return null
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Validate boolean field
|
|
121
|
+
* @private
|
|
122
|
+
* @param {boolean} value - Boolean value
|
|
123
|
+
* @param {Object} schema - Field schema
|
|
124
|
+
* @param {string} label - Field label
|
|
125
|
+
* @returns {ValidationMessage|null}
|
|
126
|
+
*/
|
|
127
|
+
function validateBoolean(value, schema, label) {
|
|
128
|
+
// Convert to boolean if not already
|
|
129
|
+
const boolValue = Boolean(value)
|
|
130
|
+
|
|
131
|
+
// For required boolean fields, we might want to ensure it's explicitly true
|
|
132
|
+
if (schema.required && schema.mustBeTrue && !boolValue) {
|
|
133
|
+
return {
|
|
134
|
+
state: 'error',
|
|
135
|
+
text: `${label} must be accepted`
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return null
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Create validation messages for informational purposes
|
|
144
|
+
* @param {string} fieldPath - Field path
|
|
145
|
+
* @param {string} state - Message state ('info', 'warning', 'success')
|
|
146
|
+
* @param {string} text - Message text
|
|
147
|
+
* @returns {Object} Field path to message mapping
|
|
148
|
+
*/
|
|
149
|
+
export function createMessage(fieldPath, state, text) {
|
|
150
|
+
return {
|
|
151
|
+
[fieldPath]: { state, text }
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Common validation patterns
|
|
157
|
+
*/
|
|
158
|
+
export const patterns = {
|
|
159
|
+
email: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
|
|
160
|
+
phone: /^\+?[\d\s\-\(\)]{10,}$/,
|
|
161
|
+
url: /^https?:\/\/[^\s/$.?#].[^\s]*$/,
|
|
162
|
+
zipCode: /^\d{5}(-\d{4})?$/,
|
|
163
|
+
creditCard: /^\d{4}\s?\d{4}\s?\d{4}\s?\d{4}$/
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Get field schema by path
|
|
168
|
+
* @private
|
|
169
|
+
* @param {string} fieldPath - Field path
|
|
170
|
+
* @param {Object} schema - Schema object
|
|
171
|
+
* @returns {Object|null} Field schema
|
|
172
|
+
*/
|
|
173
|
+
function getFieldSchema(fieldPath, schema) {
|
|
174
|
+
if (!schema.properties) return null
|
|
175
|
+
|
|
176
|
+
const keys = fieldPath.split('/')
|
|
177
|
+
let current = schema.properties
|
|
178
|
+
|
|
179
|
+
for (const key of keys) {
|
|
180
|
+
if (current && current[key]) {
|
|
181
|
+
current = current[key]
|
|
182
|
+
} else {
|
|
183
|
+
return null
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return current
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Get value by path from data object
|
|
192
|
+
* @private
|
|
193
|
+
* @param {Object} data - Data object
|
|
194
|
+
* @param {string} path - Field path
|
|
195
|
+
* @returns {any} Field value
|
|
196
|
+
*/
|
|
197
|
+
function getValueByPath(data, path) {
|
|
198
|
+
const keys = path.split('/')
|
|
199
|
+
let current = data
|
|
200
|
+
|
|
201
|
+
for (const key of keys) {
|
|
202
|
+
if (current && typeof current === 'object') {
|
|
203
|
+
current = current[key]
|
|
204
|
+
} else {
|
|
205
|
+
return undefined
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return current
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Validate a single field value against its schema
|
|
214
|
+
* @param {any} value - Field value to validate
|
|
215
|
+
* @param {Object} fieldSchema - Field schema definition
|
|
216
|
+
* @param {string} [fieldLabel] - Field label for error messages
|
|
217
|
+
* @returns {ValidationMessage|null} Validation message or null if valid
|
|
218
|
+
*/
|
|
219
|
+
export function validateField(value, fieldSchema, fieldLabel = 'Field') {
|
|
220
|
+
if (!fieldSchema) return null
|
|
221
|
+
|
|
222
|
+
// Required field validation
|
|
223
|
+
if (fieldSchema.required && isEmpty(value)) {
|
|
224
|
+
return {
|
|
225
|
+
state: 'error',
|
|
226
|
+
text: `${fieldLabel} is required`
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Skip other validations if field is empty and not required
|
|
231
|
+
if (isEmpty(value)) return null
|
|
232
|
+
|
|
233
|
+
// Type-specific validations
|
|
234
|
+
switch (fieldSchema.type) {
|
|
235
|
+
case 'string':
|
|
236
|
+
return validateString(value, fieldSchema, fieldLabel)
|
|
237
|
+
case 'number':
|
|
238
|
+
case 'integer':
|
|
239
|
+
return validateNumber(value, fieldSchema, fieldLabel)
|
|
240
|
+
case 'boolean':
|
|
241
|
+
return validateBoolean(value, fieldSchema, fieldLabel)
|
|
242
|
+
default:
|
|
243
|
+
return null
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Validate all fields in a data object
|
|
248
|
+
* @param {Object} data - Data object to validate
|
|
249
|
+
* @param {Object} schema - Schema object with properties
|
|
250
|
+
* @param {Object} layout - Layout object with element definitions
|
|
251
|
+
* @returns {Object} Validation results keyed by field path
|
|
252
|
+
*/
|
|
253
|
+
export function validateAll(data, schema, layout) {
|
|
254
|
+
const validationResults = {}
|
|
255
|
+
|
|
256
|
+
if (!layout.elements || !schema.properties) return validationResults
|
|
257
|
+
|
|
258
|
+
for (const element of layout.elements) {
|
|
259
|
+
if (!element.scope) continue
|
|
260
|
+
|
|
261
|
+
const fieldPath = element.scope.replace(/^#\//, '')
|
|
262
|
+
const fieldSchema = getFieldSchema(fieldPath, schema)
|
|
263
|
+
const fieldLabel = element.label || element.title || fieldPath
|
|
264
|
+
const value = getValueByPath(data, fieldPath)
|
|
265
|
+
|
|
266
|
+
const result = validateField(value, fieldSchema, fieldLabel)
|
|
267
|
+
if (result) {
|
|
268
|
+
validationResults[fieldPath] = result
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return validationResults
|
|
273
|
+
}
|
package/src/types.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { toHyphenCase } from '@rokkit/core'
|
|
2
|
+
// skipcq: JS-C1003 - Importing all components from atoms
|
|
3
|
+
import * as NativeInput from '@rokkit/forms'
|
|
4
|
+
// skipcq: JS-C1003 - Importing all components from molecules
|
|
5
|
+
// import CheckBox from '@rokkit/ui'
|
|
6
|
+
|
|
7
|
+
// import InputSelect from './InputSelect.svelte'
|
|
8
|
+
// import InputSwitch from './InputSwitch.svelte'
|
|
9
|
+
|
|
10
|
+
function extractComponentMap(components, prefix = /^Input/) {
|
|
11
|
+
return Object.entries(components).reduce(
|
|
12
|
+
(acc, [name, component]) => ({
|
|
13
|
+
...acc,
|
|
14
|
+
[toHyphenCase(name.replace(prefix, ''))]: component
|
|
15
|
+
}),
|
|
16
|
+
{}
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
const native = extractComponentMap(NativeInput)
|
|
20
|
+
|
|
21
|
+
export const types = {
|
|
22
|
+
string: NativeInput.InputText,
|
|
23
|
+
integer: NativeInput.InputNumber,
|
|
24
|
+
// boolean: CheckBox,
|
|
25
|
+
// enum: InputSelect,
|
|
26
|
+
phone: NativeInput.InputTel,
|
|
27
|
+
...native
|
|
28
|
+
// ...extractComponentMap({ InputSelect, InputSwitch })
|
|
29
|
+
}
|