@rokkit/forms 1.7.0 → 1.8.0
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 +4 -2
- package/dist/lib/builder.svelte.d.ts +100 -248
- package/dist/lib/builder.svelte.js +182 -1047
- package/dist/lib/elements.d.ts +81 -0
- package/dist/lib/elements.js +196 -0
- package/dist/lib/schema.d.ts +13 -0
- package/dist/lib/schema.js +27 -0
- package/dist/lib/state/FormDefinition.svelte.d.ts +32 -0
- package/dist/lib/state/FormDefinition.svelte.js +83 -0
- package/dist/lib/state/FormLookups.svelte.d.ts +39 -0
- package/dist/lib/state/FormLookups.svelte.js +93 -0
- package/dist/lib/state/FormSteps.svelte.d.ts +32 -0
- package/dist/lib/state/FormSteps.svelte.js +80 -0
- package/dist/lib/state/FormValidation.svelte.d.ts +52 -0
- package/dist/lib/state/FormValidation.svelte.js +140 -0
- package/dist/lib/state/FormValues.svelte.d.ts +29 -0
- package/dist/lib/state/FormValues.svelte.js +69 -0
- package/dist/lib/validation.js +5 -28
- package/dist/lib/values.d.ts +42 -0
- package/dist/lib/values.js +90 -0
- package/package.json +1 -1
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The form's validation messages, keyed by field path, and the ways of producing them.
|
|
3
|
+
*
|
|
4
|
+
* Which fields are VISIBLE is the element builder's knowledge (a hidden `showWhen` field has
|
|
5
|
+
* no element), so it arrives as a function rather than a dependency on the elements — which in
|
|
6
|
+
* turn read these messages.
|
|
7
|
+
*/
|
|
8
|
+
export class FormValidation {
|
|
9
|
+
/**
|
|
10
|
+
* @param {{
|
|
11
|
+
* values: import('./FormValues.svelte.js').FormValues,
|
|
12
|
+
* definition: import('./FormDefinition.svelte.js').FormDefinition,
|
|
13
|
+
* steps: import('./FormSteps.svelte.js').FormSteps,
|
|
14
|
+
* visiblePaths: () => string[]
|
|
15
|
+
* }} parts
|
|
16
|
+
*/
|
|
17
|
+
constructor({ values, definition, steps, visiblePaths }: {
|
|
18
|
+
values: import("./FormValues.svelte.js").FormValues;
|
|
19
|
+
definition: import("./FormDefinition.svelte.js").FormDefinition;
|
|
20
|
+
steps: import("./FormSteps.svelte.js").FormSteps;
|
|
21
|
+
visiblePaths: () => string[];
|
|
22
|
+
});
|
|
23
|
+
set map(value: {});
|
|
24
|
+
/** Every message, by field path. */
|
|
25
|
+
get map(): {};
|
|
26
|
+
/** One field's message, or null. */
|
|
27
|
+
message(path: any): any;
|
|
28
|
+
/** Set a field's message; a null message removes it. */
|
|
29
|
+
set(path: any, message: any): void;
|
|
30
|
+
clear(): void;
|
|
31
|
+
/** No message is an error. */
|
|
32
|
+
get isValid(): boolean;
|
|
33
|
+
/** Error messages, with their paths. */
|
|
34
|
+
get errors(): any[];
|
|
35
|
+
/** Every message with its path, most severe first. */
|
|
36
|
+
get sorted(): any[];
|
|
37
|
+
/** Validate one field against its schema, and record the result. Null without a schema. */
|
|
38
|
+
validateField(path: any): import("../validation.js").ValidationMessage | null;
|
|
39
|
+
/**
|
|
40
|
+
* Validate the whole form. A multi-step form validates every step at once; a flat one keeps
|
|
41
|
+
* only the results for fields currently visible.
|
|
42
|
+
*/
|
|
43
|
+
validateAll(): Object;
|
|
44
|
+
/**
|
|
45
|
+
* Validate one step's fields, replacing their messages and leaving every other field's alone.
|
|
46
|
+
* A non-step index is trivially valid.
|
|
47
|
+
*/
|
|
48
|
+
validateStep(index: any): boolean;
|
|
49
|
+
/** Drop the messages of fields that are no longer visible (a `showWhen` just hid them). */
|
|
50
|
+
clearHidden(): void;
|
|
51
|
+
#private;
|
|
52
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { SvelteSet } from 'svelte/reactivity'
|
|
2
|
+
import { validateField as validateOne, validateAll } from '../validation.js'
|
|
3
|
+
import { stepPaths } from './FormSteps.svelte.js'
|
|
4
|
+
|
|
5
|
+
/** Severity order for `sorted`; anything unlisted sorts last. */
|
|
6
|
+
const SEVERITY = { error: 0, warning: 1, info: 2, success: 3 }
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The form's validation messages, keyed by field path, and the ways of producing them.
|
|
10
|
+
*
|
|
11
|
+
* Which fields are VISIBLE is the element builder's knowledge (a hidden `showWhen` field has
|
|
12
|
+
* no element), so it arrives as a function rather than a dependency on the elements — which in
|
|
13
|
+
* turn read these messages.
|
|
14
|
+
*/
|
|
15
|
+
export class FormValidation {
|
|
16
|
+
#values
|
|
17
|
+
#definition
|
|
18
|
+
#steps
|
|
19
|
+
#visiblePaths
|
|
20
|
+
#map = $state({})
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {{
|
|
24
|
+
* values: import('./FormValues.svelte.js').FormValues,
|
|
25
|
+
* definition: import('./FormDefinition.svelte.js').FormDefinition,
|
|
26
|
+
* steps: import('./FormSteps.svelte.js').FormSteps,
|
|
27
|
+
* visiblePaths: () => string[]
|
|
28
|
+
* }} parts
|
|
29
|
+
*/
|
|
30
|
+
constructor({ values, definition, steps, visiblePaths }) {
|
|
31
|
+
this.#values = values
|
|
32
|
+
this.#definition = definition
|
|
33
|
+
this.#steps = steps
|
|
34
|
+
this.#visiblePaths = visiblePaths
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Every message, by field path. */
|
|
38
|
+
get map() {
|
|
39
|
+
return this.#map
|
|
40
|
+
}
|
|
41
|
+
set map(value) {
|
|
42
|
+
this.#map = value
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** One field's message, or null. */
|
|
46
|
+
message(path) {
|
|
47
|
+
return this.#map[path] || null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Set a field's message; a null message removes it. */
|
|
51
|
+
set(path, message) {
|
|
52
|
+
if (message) {
|
|
53
|
+
this.#map = { ...this.#map, [path]: message }
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
const { [path]: _removed, ...rest } = this.#map
|
|
57
|
+
this.#map = rest
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
clear() {
|
|
61
|
+
this.#map = {}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** No message is an error. */
|
|
65
|
+
get isValid() {
|
|
66
|
+
return Object.values(this.#map).every((msg) => msg.state !== 'error')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Error messages, with their paths. */
|
|
70
|
+
get errors() {
|
|
71
|
+
return Object.entries(this.#map)
|
|
72
|
+
.filter(([, msg]) => msg.state === 'error')
|
|
73
|
+
.map(([path, msg]) => ({ path, ...msg }))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Every message with its path, most severe first. */
|
|
77
|
+
get sorted() {
|
|
78
|
+
return Object.entries(this.#map)
|
|
79
|
+
.filter(([, msg]) => msg !== null && msg !== undefined)
|
|
80
|
+
.map(([path, msg]) => ({ path, ...msg }))
|
|
81
|
+
.sort((a, b) => (SEVERITY[a.state] ?? 4) - (SEVERITY[b.state] ?? 4))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Validate one field against its schema, and record the result. Null without a schema. */
|
|
85
|
+
validateField(path) {
|
|
86
|
+
const schema = this.#definition.fieldSchema(path)
|
|
87
|
+
if (!schema) return null
|
|
88
|
+
const result = validateOne(this.#values.get(path), schema, this.#definition.fieldLabel(path))
|
|
89
|
+
this.set(path, result)
|
|
90
|
+
return result
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Validate the whole form. A multi-step form validates every step at once; a flat one keeps
|
|
95
|
+
* only the results for fields currently visible.
|
|
96
|
+
*/
|
|
97
|
+
validateAll() {
|
|
98
|
+
const { schema, layout } = this.#definition
|
|
99
|
+
if (this.#steps.isMultiStep) {
|
|
100
|
+
const results = validateAll(this.#values.data, schema, {
|
|
101
|
+
...layout,
|
|
102
|
+
elements: this.#steps.allStepElements
|
|
103
|
+
})
|
|
104
|
+
this.#map = results
|
|
105
|
+
return results
|
|
106
|
+
}
|
|
107
|
+
const visible = new SvelteSet(this.#visiblePaths())
|
|
108
|
+
const results = Object.fromEntries(
|
|
109
|
+
Object.entries(validateAll(this.#values.data, schema, layout)).filter(([path]) =>
|
|
110
|
+
visible.has(path)
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
this.#map = results
|
|
114
|
+
return results
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Validate one step's fields, replacing their messages and leaving every other field's alone.
|
|
119
|
+
* A non-step index is trivially valid.
|
|
120
|
+
*/
|
|
121
|
+
validateStep(index) {
|
|
122
|
+
const step = this.#steps.step(index)
|
|
123
|
+
if (!step) return true
|
|
124
|
+
const { schema, layout } = this.#definition
|
|
125
|
+
const results = validateAll(this.#values.data, schema, {
|
|
126
|
+
...layout,
|
|
127
|
+
elements: step.elements ?? []
|
|
128
|
+
})
|
|
129
|
+
for (const path of new SvelteSet(stepPaths(step.elements)))
|
|
130
|
+
this.set(path, results[path] ?? null)
|
|
131
|
+
return Object.values(results).every((msg) => msg?.state !== 'error')
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Drop the messages of fields that are no longer visible (a `showWhen` just hid them). */
|
|
135
|
+
clearHidden() {
|
|
136
|
+
const visible = new SvelteSet(this.#visiblePaths())
|
|
137
|
+
const kept = Object.fromEntries(Object.entries(this.#map).filter(([path]) => visible.has(path)))
|
|
138
|
+
if (Object.keys(kept).length !== Object.keys(this.#map).length) this.#map = kept
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A form's data, and how it differs from where it started.
|
|
3
|
+
*
|
|
4
|
+
* The initial snapshot is deliberately NOT reactive: it is a baseline, read by the dirty
|
|
5
|
+
* getters, and taking a new one (after a save) must not by itself re-render anything.
|
|
6
|
+
*/
|
|
7
|
+
export class FormValues {
|
|
8
|
+
/** @param {Record<string, unknown>} data */
|
|
9
|
+
constructor(data?: Record<string, unknown>);
|
|
10
|
+
set data(value: {});
|
|
11
|
+
get data(): {};
|
|
12
|
+
/** @param {string} path */
|
|
13
|
+
get(path: string): any;
|
|
14
|
+
/** Write one value; the data gets a new root, so readers of `data` re-run. */
|
|
15
|
+
set(path: any, value: any): void;
|
|
16
|
+
/** The value a path held in the snapshot. */
|
|
17
|
+
initial(path: any): any;
|
|
18
|
+
get isDirty(): boolean;
|
|
19
|
+
/** Top-level keys whose value differs from the snapshot. */
|
|
20
|
+
get dirtyFields(): SvelteSet<string>;
|
|
21
|
+
/** @param {string} path */
|
|
22
|
+
isFieldDirty(path: string): boolean;
|
|
23
|
+
/** Make the current data the new baseline — after a successful save. */
|
|
24
|
+
snapshot(): void;
|
|
25
|
+
/** Restore the baseline. */
|
|
26
|
+
reset(): void;
|
|
27
|
+
#private;
|
|
28
|
+
}
|
|
29
|
+
import { SvelteSet } from 'svelte/reactivity';
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { SvelteSet } from 'svelte/reactivity'
|
|
2
|
+
import { deepClone, deepEqual, getPath, setPath } from '../values.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A form's data, and how it differs from where it started.
|
|
6
|
+
*
|
|
7
|
+
* The initial snapshot is deliberately NOT reactive: it is a baseline, read by the dirty
|
|
8
|
+
* getters, and taking a new one (after a save) must not by itself re-render anything.
|
|
9
|
+
*/
|
|
10
|
+
export class FormValues {
|
|
11
|
+
#data = $state({})
|
|
12
|
+
#initial = {}
|
|
13
|
+
|
|
14
|
+
/** @param {Record<string, unknown>} data */
|
|
15
|
+
constructor(data = {}) {
|
|
16
|
+
this.#data = data
|
|
17
|
+
this.#initial = deepClone(data)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
get data() {
|
|
21
|
+
return this.#data
|
|
22
|
+
}
|
|
23
|
+
set data(value) {
|
|
24
|
+
this.#data = value
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** @param {string} path */
|
|
28
|
+
get(path) {
|
|
29
|
+
return getPath(this.#data, path)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Write one value; the data gets a new root, so readers of `data` re-run. */
|
|
33
|
+
set(path, value) {
|
|
34
|
+
this.#data = path.includes('/')
|
|
35
|
+
? setPath(this.#data, path, value)
|
|
36
|
+
: { ...this.#data, [path]: value }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The value a path held in the snapshot. */
|
|
40
|
+
initial(path) {
|
|
41
|
+
return getPath(this.#initial, path)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get isDirty() {
|
|
45
|
+
return !deepEqual(this.#data, this.#initial)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Top-level keys whose value differs from the snapshot. */
|
|
49
|
+
get dirtyFields() {
|
|
50
|
+
const current = this.#data ?? {}
|
|
51
|
+
const keys = new SvelteSet([...Object.keys(current), ...Object.keys(this.#initial ?? {})])
|
|
52
|
+
return new SvelteSet([...keys].filter((k) => !deepEqual(current[k], this.#initial?.[k])))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** @param {string} path */
|
|
56
|
+
isFieldDirty(path) {
|
|
57
|
+
return !deepEqual(this.get(path), this.initial(path))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Make the current data the new baseline — after a successful save. */
|
|
61
|
+
snapshot() {
|
|
62
|
+
this.#initial = deepClone(this.#data)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Restore the baseline. */
|
|
66
|
+
reset() {
|
|
67
|
+
this.#data = deepClone(this.#initial)
|
|
68
|
+
}
|
|
69
|
+
}
|
package/dist/lib/validation.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { schemaAt } from './schema.js'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Validation utility for FormBuilder
|
|
3
5
|
* Provides field validation functions that return message objects
|
|
@@ -174,33 +176,6 @@ export const patterns = {
|
|
|
174
176
|
creditCard: /^\d{4}\s?\d{4}\s?\d{4}\s?\d{4}$/
|
|
175
177
|
}
|
|
176
178
|
|
|
177
|
-
/**
|
|
178
|
-
* Get field schema by path
|
|
179
|
-
* @private
|
|
180
|
-
* @param {string} fieldPath - Field path
|
|
181
|
-
* @param {Object} schema - Schema object
|
|
182
|
-
* @returns {Object|null} Field schema
|
|
183
|
-
*/
|
|
184
|
-
function getFieldSchema(fieldPath, schema) {
|
|
185
|
-
/* v8 ignore start -- unreachable: the only caller is validateElement, reached
|
|
186
|
-
from validateAll, which already returns early on !schema.properties. */
|
|
187
|
-
if (!schema.properties) return null
|
|
188
|
-
/* v8 ignore stop */
|
|
189
|
-
|
|
190
|
-
const keys = fieldPath.split('/')
|
|
191
|
-
let current = schema.properties
|
|
192
|
-
|
|
193
|
-
for (const key of keys) {
|
|
194
|
-
if (current && current[key]) {
|
|
195
|
-
current = current[key]
|
|
196
|
-
} else {
|
|
197
|
-
return null
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
return current
|
|
202
|
-
}
|
|
203
|
-
|
|
204
179
|
/**
|
|
205
180
|
* Get value by path from data object
|
|
206
181
|
* @private
|
|
@@ -273,10 +248,12 @@ export function validateField(value, fieldSchema, fieldLabel = 'Field') {
|
|
|
273
248
|
* @private
|
|
274
249
|
*/
|
|
275
250
|
function validateElement(element, data, schema, results) {
|
|
251
|
+
// A group's children are fields too — validated at any depth, not just the top level.
|
|
252
|
+
for (const child of element.elements ?? []) validateElement(child, data, schema, results)
|
|
276
253
|
if (!element.scope) return
|
|
277
254
|
|
|
278
255
|
const fieldPath = element.scope.replace(/^#\//, '')
|
|
279
|
-
const fieldSchema =
|
|
256
|
+
const fieldSchema = schemaAt(schema, fieldPath)
|
|
280
257
|
const fieldLabel = element.label || element.title || fieldPath
|
|
281
258
|
const value = getValueByPath(data, fieldPath)
|
|
282
259
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers over plain form values — primitives, plain objects and arrays, addressed by
|
|
3
|
+
* slash paths (`'settings/distance'`).
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Deep-clone a plain value. A JSON round-trip, which also unwraps `$state` proxies safely.
|
|
7
|
+
* @param {any} value
|
|
8
|
+
* @returns {any}
|
|
9
|
+
*/
|
|
10
|
+
export function deepClone(value: any): any;
|
|
11
|
+
/**
|
|
12
|
+
* Structural equality for plain values. Nullish equals only itself; an array never equals an
|
|
13
|
+
* object.
|
|
14
|
+
* @param {any} a
|
|
15
|
+
* @param {any} b
|
|
16
|
+
* @returns {boolean}
|
|
17
|
+
*/
|
|
18
|
+
export function deepEqual(a: any, b: any): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* The value at a slash path, or undefined — also when the path runs through a non-object.
|
|
21
|
+
* @param {any} data
|
|
22
|
+
* @param {string} path
|
|
23
|
+
*/
|
|
24
|
+
export function getPath(data: any, path: string): any;
|
|
25
|
+
/**
|
|
26
|
+
* A copy of `data` with `value` at the slash path — every object along the path is copied, so
|
|
27
|
+
* the original is untouched and reactive readers see a new root.
|
|
28
|
+
* @param {Record<string, any>} data
|
|
29
|
+
* @param {string} path
|
|
30
|
+
* @param {any} value
|
|
31
|
+
*/
|
|
32
|
+
export function setPath(data: Record<string, any>, path: string, value: any): {
|
|
33
|
+
[x: string]: any;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* A field path in its canonical, bare form. Accepts a JSON Forms scope as well — `'#/addr/city'`
|
|
37
|
+
* and `'addr/city'` name the same field — so the convention layouts are written in is also safe
|
|
38
|
+
* to pass to the API. (Unnormalised, `'#/name'` wrote to a `'#'` key.)
|
|
39
|
+
* @param {string} path - anything else (a missing path) passes through unchanged
|
|
40
|
+
* @returns {string}
|
|
41
|
+
*/
|
|
42
|
+
export function toFieldPath(path: string): string;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers over plain form values — primitives, plain objects and arrays, addressed by
|
|
3
|
+
* slash paths (`'settings/distance'`).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Deep-clone a plain value. A JSON round-trip, which also unwraps `$state` proxies safely.
|
|
8
|
+
* @param {any} value
|
|
9
|
+
* @returns {any}
|
|
10
|
+
*/
|
|
11
|
+
export function deepClone(value) {
|
|
12
|
+
if (value === null || value === undefined || typeof value !== 'object') return value
|
|
13
|
+
return JSON.parse(JSON.stringify(value))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const isNullish = (v) => v === null || v === undefined
|
|
17
|
+
|
|
18
|
+
/** Same-shaped containers: both objects, and both arrays or both not. */
|
|
19
|
+
const sameContainer = (a, b) =>
|
|
20
|
+
typeof a === 'object' && typeof b === 'object' && Array.isArray(a) === Array.isArray(b)
|
|
21
|
+
|
|
22
|
+
function equalArrays(a, b) {
|
|
23
|
+
return a.length === b.length && a.every((v, i) => deepEqual(v, b[i]))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function equalObjects(a, b) {
|
|
27
|
+
const keys = Object.keys(a)
|
|
28
|
+
return (
|
|
29
|
+
keys.length === Object.keys(b).length &&
|
|
30
|
+
keys.every((k) => Object.hasOwn(b, k) && deepEqual(a[k], b[k]))
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Structural equality for plain values. Nullish equals only itself; an array never equals an
|
|
36
|
+
* object.
|
|
37
|
+
* @param {any} a
|
|
38
|
+
* @param {any} b
|
|
39
|
+
* @returns {boolean}
|
|
40
|
+
*/
|
|
41
|
+
export function deepEqual(a, b) {
|
|
42
|
+
if (a === b) return true
|
|
43
|
+
if (isNullish(a) || isNullish(b) || !sameContainer(a, b)) return false
|
|
44
|
+
return Array.isArray(a) ? equalArrays(a, b) : equalObjects(a, b)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The value at a slash path, or undefined — also when the path runs through a non-object.
|
|
49
|
+
* @param {any} data
|
|
50
|
+
* @param {string} path
|
|
51
|
+
*/
|
|
52
|
+
export function getPath(data, path) {
|
|
53
|
+
if (!path) return undefined
|
|
54
|
+
let current = data
|
|
55
|
+
for (const key of path.split('/')) {
|
|
56
|
+
if (!current || typeof current !== 'object') return undefined
|
|
57
|
+
current = current[key]
|
|
58
|
+
}
|
|
59
|
+
return current
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* A copy of `data` with `value` at the slash path — every object along the path is copied, so
|
|
64
|
+
* the original is untouched and reactive readers see a new root.
|
|
65
|
+
* @param {Record<string, any>} data
|
|
66
|
+
* @param {string} path
|
|
67
|
+
* @param {any} value
|
|
68
|
+
*/
|
|
69
|
+
export function setPath(data, path, value) {
|
|
70
|
+
const keys = path.split('/')
|
|
71
|
+
const root = { ...data }
|
|
72
|
+
let current = root
|
|
73
|
+
for (const key of keys.slice(0, -1)) {
|
|
74
|
+
current[key] = { ...current[key] }
|
|
75
|
+
current = current[key]
|
|
76
|
+
}
|
|
77
|
+
current[keys[keys.length - 1]] = value
|
|
78
|
+
return root
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* A field path in its canonical, bare form. Accepts a JSON Forms scope as well — `'#/addr/city'`
|
|
83
|
+
* and `'addr/city'` name the same field — so the convention layouts are written in is also safe
|
|
84
|
+
* to pass to the API. (Unnormalised, `'#/name'` wrote to a `'#'` key.)
|
|
85
|
+
* @param {string} path - anything else (a missing path) passes through unchanged
|
|
86
|
+
* @returns {string}
|
|
87
|
+
*/
|
|
88
|
+
export function toFieldPath(path) {
|
|
89
|
+
return typeof path === 'string' && path.startsWith('#/') ? path.slice(2) : path
|
|
90
|
+
}
|