@xeplr/ui-factory 1.0.1
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/AUTHORING.md +133 -0
- package/LICENSE +21 -0
- package/README.md +184 -0
- package/bin/xeplr-factory.js +98 -0
- package/package.json +59 -0
- package/src/controls.js +238 -0
- package/src/designs/BuilderSample.jsx +117 -0
- package/src/designs/ControlView.jsx +192 -0
- package/src/designs/Palette.jsx +34 -0
- package/src/designs/PropertyPanel.jsx +228 -0
- package/src/designs/ScreenSample.jsx +65 -0
- package/src/designs/factory.css +177 -0
- package/src/designs/index.js +7 -0
- package/src/document.js +215 -0
- package/src/generate.js +167 -0
- package/src/index.js +19 -0
- package/src/model.js +15 -0
- package/src/pages.jsx +42 -0
- package/src/propertyPath.js +35 -0
- package/src/useFactoryBuilder.js +209 -0
- package/src/useFactoryScreen.js +155 -0
- package/src/validateDocument.js +202 -0
- package/src/values.js +148 -0
package/src/values.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// A screen's VALUES: what it starts with, what a keystroke becomes, and whether
|
|
2
|
+
// what was entered is acceptable.
|
|
3
|
+
//
|
|
4
|
+
// The rules come from the controls' props, and formSchema() turns them into a
|
|
5
|
+
// @xeplr/schema-handler schema. That is the point of it: the consuming app's
|
|
6
|
+
// server can check a submission with
|
|
7
|
+
//
|
|
8
|
+
// applySchema(formSchema(document), body)
|
|
9
|
+
//
|
|
10
|
+
// against the very rules the screen enforced, instead of a second copy of
|
|
11
|
+
// them. validateValues() below applies the same rules in the browser, with
|
|
12
|
+
// messages written for the person filling the form in rather than for a log.
|
|
13
|
+
|
|
14
|
+
import { CONTROLS } from './controls.js'
|
|
15
|
+
import { inputNodes } from './document.js'
|
|
16
|
+
|
|
17
|
+
/** schema-handler field schema for the screen's inputs, in reading order. */
|
|
18
|
+
export function formSchema(doc, controls = CONTROLS) {
|
|
19
|
+
return inputNodes(doc, controls).map((node) => {
|
|
20
|
+
const def = controls[node.type]
|
|
21
|
+
const p = node.props
|
|
22
|
+
const field = { name: p.name }
|
|
23
|
+
if (def.valueType) field.type = def.valueType
|
|
24
|
+
if (p.label) field.description = p.label
|
|
25
|
+
if (p.required) field.required = true
|
|
26
|
+
if (p.default !== undefined) field.default = p.default
|
|
27
|
+
if (node.type === 'dropdown' && p.data?.source === 'static') {
|
|
28
|
+
field.options = p.data.options.map((o) => o.id)
|
|
29
|
+
}
|
|
30
|
+
const v = p.validation && { ...p.validation }
|
|
31
|
+
if (v) {
|
|
32
|
+
delete v.patternMessage
|
|
33
|
+
if (Object.keys(v).length) field.validation = v
|
|
34
|
+
}
|
|
35
|
+
return field
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Each input's starting value: the caller's, else the control's default. */
|
|
40
|
+
export function initialValues(doc, provided = {}, controls = CONTROLS) {
|
|
41
|
+
const out = {}
|
|
42
|
+
inputNodes(doc, controls).forEach((node) => {
|
|
43
|
+
const name = node.props.name
|
|
44
|
+
if (provided[name] !== undefined) out[name] = provided[name]
|
|
45
|
+
else if (node.props.default !== undefined) out[name] = node.props.default
|
|
46
|
+
else if (node.type === 'checkbox') out[name] = false
|
|
47
|
+
})
|
|
48
|
+
return out
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* What the raw value from an input element becomes. An emptied box is
|
|
53
|
+
* `undefined` — not '' and not 0 — so "required" means the same thing for
|
|
54
|
+
* every type, and a cleared number is not silently saved as zero.
|
|
55
|
+
*/
|
|
56
|
+
export function parseInput(node, raw) {
|
|
57
|
+
if (node.type === 'checkbox') return Boolean(raw)
|
|
58
|
+
if (raw === undefined || raw === null || raw === '') return undefined
|
|
59
|
+
if (node.type === 'number') {
|
|
60
|
+
const n = typeof raw === 'number' ? raw : Number(String(raw).trim())
|
|
61
|
+
return Number.isFinite(n) ? n : raw // left as typed; validation says why
|
|
62
|
+
}
|
|
63
|
+
if (node.type === 'dropdown') return raw
|
|
64
|
+
return String(raw)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @returns {Object<string, string>} field name → message, for the fields that
|
|
69
|
+
* fail. Empty when everything passes.
|
|
70
|
+
*/
|
|
71
|
+
export function validateValues(doc, values, controls = CONTROLS, options = {}) {
|
|
72
|
+
const errors = {}
|
|
73
|
+
inputNodes(doc, controls).forEach((node) => {
|
|
74
|
+
const msg = fieldError(node, values ? values[node.props.name] : undefined, options[node.id])
|
|
75
|
+
if (msg) errors[node.props.name] = msg
|
|
76
|
+
})
|
|
77
|
+
return errors
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* One field's message, or null.
|
|
82
|
+
* @param loadedOptions for a dropdown whose options are loaded (a table), the
|
|
83
|
+
* rows — so a stale id is caught here rather than by the server
|
|
84
|
+
*/
|
|
85
|
+
export function fieldError(node, value, loadedOptions) {
|
|
86
|
+
const p = node.props
|
|
87
|
+
const label = p.label || p.name
|
|
88
|
+
const v = p.validation || {}
|
|
89
|
+
|
|
90
|
+
if (node.type === 'checkbox') {
|
|
91
|
+
return p.required && value !== true ? `${label} must be ticked` : null
|
|
92
|
+
}
|
|
93
|
+
if (value === undefined || value === null || value === '') {
|
|
94
|
+
return p.required ? `${label} is required` : null
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
switch (node.type) {
|
|
98
|
+
case 'text':
|
|
99
|
+
case 'textarea': {
|
|
100
|
+
const s = String(value)
|
|
101
|
+
if (v.minLength != null && s.length < v.minLength) return `${label} must be at least ${v.minLength} character${v.minLength === 1 ? '' : 's'}`
|
|
102
|
+
if (v.maxLength != null && s.length > v.maxLength) return `${label} must be at most ${v.maxLength} character${v.maxLength === 1 ? '' : 's'}`
|
|
103
|
+
if (v.pattern && !safeRegExp(v.pattern).test(s)) return v.patternMessage || `${label} is not in the expected format`
|
|
104
|
+
return null
|
|
105
|
+
}
|
|
106
|
+
case 'number': {
|
|
107
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) return `${label} must be a number`
|
|
108
|
+
if (v.integer && !Number.isInteger(value)) return `${label} must be a whole number`
|
|
109
|
+
if (v.min != null && value < v.min) return `${label} must be at least ${v.min}`
|
|
110
|
+
if (v.max != null && value > v.max) return `${label} must be at most ${v.max}`
|
|
111
|
+
return null
|
|
112
|
+
}
|
|
113
|
+
case 'date': {
|
|
114
|
+
if (typeof value !== 'string' || Number.isNaN(Date.parse(value))) return `${label} must be a date`
|
|
115
|
+
// ISO dates compare correctly as strings, and comparing strings avoids a
|
|
116
|
+
// timezone shifting "2026-01-01" into the previous day.
|
|
117
|
+
const day = value.slice(0, 10)
|
|
118
|
+
if (v.min && day < v.min) return `${label} must be on or after ${v.min}`
|
|
119
|
+
if (v.max && day > v.max) return `${label} must be on or before ${v.max}`
|
|
120
|
+
return null
|
|
121
|
+
}
|
|
122
|
+
case 'dropdown': {
|
|
123
|
+
const opts = p.data?.source === 'static' ? p.data.options : loadedOptions
|
|
124
|
+
if (Array.isArray(opts) && !opts.some((o) => sameId(o.id, value))) return `${label} must be one of the options`
|
|
125
|
+
return null
|
|
126
|
+
}
|
|
127
|
+
default:
|
|
128
|
+
return null
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* <select> hands back a string, while a table's ids may be numbers. The id is
|
|
134
|
+
* matched loosely and then saved in the OPTION's own type — see optionValue.
|
|
135
|
+
*/
|
|
136
|
+
export function sameId(a, b) {
|
|
137
|
+
return a === b || String(a) === String(b)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** The option's own id for what a <select> reported. */
|
|
141
|
+
export function optionValue(options, raw) {
|
|
142
|
+
const hit = (options || []).find((o) => sameId(o.id, raw))
|
|
143
|
+
return hit ? hit.id : raw
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function safeRegExp(pattern) {
|
|
147
|
+
try { return new RegExp(pattern) } catch (_) { return { test: () => true } }
|
|
148
|
+
}
|