@4riders/reform 3.0.24
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/LICENSE +21 -0
- package/README.md +266 -0
- package/dist/index.d.ts +2715 -0
- package/dist/index.es.js +1715 -0
- package/dist/index.es.js.map +1 -0
- package/package.json +70 -0
- package/src/index.ts +90 -0
- package/src/reform/ArrayHelper.ts +164 -0
- package/src/reform/Form.tsx +81 -0
- package/src/reform/FormManager.ts +494 -0
- package/src/reform/Reform.ts +15 -0
- package/src/reform/components/BaseCheckboxField.tsx +72 -0
- package/src/reform/components/BaseDateField.tsx +84 -0
- package/src/reform/components/BaseRadioField.tsx +72 -0
- package/src/reform/components/BaseSelectField.tsx +103 -0
- package/src/reform/components/BaseTextAreaField.tsx +87 -0
- package/src/reform/components/BaseTextField.tsx +135 -0
- package/src/reform/components/InputHTMLProps.tsx +89 -0
- package/src/reform/observers/observer.ts +131 -0
- package/src/reform/observers/observerPath.ts +327 -0
- package/src/reform/observers/useObservers.ts +232 -0
- package/src/reform/useForm.ts +246 -0
- package/src/reform/useFormContext.tsx +37 -0
- package/src/reform/useFormField.ts +75 -0
- package/src/reform/useRender.ts +12 -0
- package/src/yop/MessageProvider.ts +204 -0
- package/src/yop/Metadata.ts +304 -0
- package/src/yop/ObjectsUtil.ts +811 -0
- package/src/yop/TypesUtil.ts +148 -0
- package/src/yop/ValidationContext.ts +207 -0
- package/src/yop/Yop.ts +430 -0
- package/src/yop/constraints/CommonConstraints.ts +124 -0
- package/src/yop/constraints/Constraint.ts +135 -0
- package/src/yop/constraints/MinMaxConstraints.ts +53 -0
- package/src/yop/constraints/OneOfConstraint.ts +40 -0
- package/src/yop/constraints/TestConstraint.ts +176 -0
- package/src/yop/decorators/array.ts +157 -0
- package/src/yop/decorators/boolean.ts +69 -0
- package/src/yop/decorators/date.ts +73 -0
- package/src/yop/decorators/email.ts +66 -0
- package/src/yop/decorators/file.ts +69 -0
- package/src/yop/decorators/id.ts +35 -0
- package/src/yop/decorators/ignored.ts +40 -0
- package/src/yop/decorators/instance.ts +110 -0
- package/src/yop/decorators/number.ts +73 -0
- package/src/yop/decorators/string.ts +90 -0
- package/src/yop/decorators/test.ts +41 -0
- package/src/yop/decorators/time.ts +112 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { Path } from "../../yop/ObjectsUtil"
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* State constants for the observer path parser state machine.
|
|
6
|
+
* @ignore
|
|
7
|
+
*/
|
|
8
|
+
const SLASH = 1
|
|
9
|
+
const OPEN_BRACKET = 2
|
|
10
|
+
const SINGLE_QUOTE = 3
|
|
11
|
+
const DOUBLE_QUOTE = 4
|
|
12
|
+
const CLOSE_QUOTE = 5
|
|
13
|
+
const CLOSE_BRACKET = 6
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Type representing the parser state.
|
|
18
|
+
* @ignore
|
|
19
|
+
*/
|
|
20
|
+
type State = typeof SLASH | typeof OPEN_BRACKET | typeof SINGLE_QUOTE | typeof DOUBLE_QUOTE | typeof CLOSE_QUOTE | typeof CLOSE_BRACKET | undefined
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Represents a segment of an observer path.
|
|
25
|
+
* @property kind - The type of path segment (e.g., property, index, wildcard).
|
|
26
|
+
* @property value - The value of the segment, if applicable.
|
|
27
|
+
* @ignore
|
|
28
|
+
*/
|
|
29
|
+
type PathElement = {
|
|
30
|
+
kind: 'root' | 'parent' | 'property' | 'key' | 'index' | 'wildcard-index' | 'wildcard' | 'double-wildcard'
|
|
31
|
+
value?: string | number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Regular expression to match valid JavaScript identifiers.
|
|
37
|
+
* @ignore
|
|
38
|
+
*/
|
|
39
|
+
const identifier = /^[$_\p{ID_Start}][$\p{ID_Continue}]*$/u
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Pushes a property, wildcard, or double-wildcard segment to the segments array.
|
|
43
|
+
* @param segment - The string segment to parse.
|
|
44
|
+
* @param segments - The array to push the parsed PathElement into.
|
|
45
|
+
* @returns True if the segment was valid and pushed, false otherwise.
|
|
46
|
+
* @ignore
|
|
47
|
+
*/
|
|
48
|
+
function pushProperty(segment: string, segments: PathElement[]): boolean {
|
|
49
|
+
if (identifier.test(segment))
|
|
50
|
+
segments.push({ kind: "property", value: segment })
|
|
51
|
+
else if (segment === '*')
|
|
52
|
+
segments.push({ kind: "wildcard" })
|
|
53
|
+
else if (segment === '**')
|
|
54
|
+
segments.push({ kind: "double-wildcard" })
|
|
55
|
+
else
|
|
56
|
+
return false
|
|
57
|
+
return true
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Splits an observer path into its constituent segments.
|
|
62
|
+
* @param path - The observer path string.
|
|
63
|
+
* @param cache - Optional cache to store and retrieve previously parsed paths.
|
|
64
|
+
* @returns An array of PathElement objects representing the path segments, or undefined if the path is invalid.
|
|
65
|
+
* @ignore
|
|
66
|
+
*/
|
|
67
|
+
export function splitObserverPath(path: string, cache?: Map<string, PathElement[]>): PathElement[] | undefined {
|
|
68
|
+
|
|
69
|
+
if (path.length === 0)
|
|
70
|
+
return undefined
|
|
71
|
+
|
|
72
|
+
if (cache != null) {
|
|
73
|
+
const cached = cache.get(path)
|
|
74
|
+
if (cached != null)
|
|
75
|
+
return cached.slice()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const segments: PathElement[] = []
|
|
79
|
+
|
|
80
|
+
let state: State = undefined,
|
|
81
|
+
escape = false,
|
|
82
|
+
segment = "",
|
|
83
|
+
i = 0
|
|
84
|
+
|
|
85
|
+
if (path.charAt(0) === '/' ) {
|
|
86
|
+
segments.push({ kind: "root" })
|
|
87
|
+
state = SLASH
|
|
88
|
+
i++
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
while (path.startsWith("..", i)) {
|
|
92
|
+
segments.push({ kind: "parent" })
|
|
93
|
+
i += 2
|
|
94
|
+
if (i === path.length)
|
|
95
|
+
return segments
|
|
96
|
+
const c = path.charAt(i)
|
|
97
|
+
if (c === '/' ) {
|
|
98
|
+
i++
|
|
99
|
+
state = SLASH
|
|
100
|
+
}
|
|
101
|
+
else if (c === '[') {
|
|
102
|
+
i++
|
|
103
|
+
state = OPEN_BRACKET
|
|
104
|
+
break
|
|
105
|
+
}
|
|
106
|
+
else
|
|
107
|
+
return undefined
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for ( ; i < path.length; i++) {
|
|
112
|
+
let c = path.charAt(i)
|
|
113
|
+
|
|
114
|
+
switch (c) {
|
|
115
|
+
|
|
116
|
+
case '\\':
|
|
117
|
+
if (state !== SINGLE_QUOTE && state !== DOUBLE_QUOTE)
|
|
118
|
+
return undefined
|
|
119
|
+
if (escape)
|
|
120
|
+
segment += '\\'
|
|
121
|
+
escape = !escape
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
case ' ': case '\t': case '\r': case '\n': case '.':
|
|
125
|
+
if (state !== SINGLE_QUOTE && state !== DOUBLE_QUOTE)
|
|
126
|
+
return undefined
|
|
127
|
+
segment += c
|
|
128
|
+
break
|
|
129
|
+
|
|
130
|
+
case '/':
|
|
131
|
+
if (state === SINGLE_QUOTE || state === DOUBLE_QUOTE)
|
|
132
|
+
segment += c
|
|
133
|
+
else if (state === CLOSE_BRACKET) {
|
|
134
|
+
if (segment)
|
|
135
|
+
return undefined
|
|
136
|
+
state = SLASH
|
|
137
|
+
}
|
|
138
|
+
else if (state === undefined || state === SLASH) {
|
|
139
|
+
if (!pushProperty(segment, segments))
|
|
140
|
+
return undefined
|
|
141
|
+
segment = ""
|
|
142
|
+
state = SLASH
|
|
143
|
+
}
|
|
144
|
+
else
|
|
145
|
+
return undefined
|
|
146
|
+
break
|
|
147
|
+
|
|
148
|
+
case '[':
|
|
149
|
+
if (state === SINGLE_QUOTE || state === DOUBLE_QUOTE)
|
|
150
|
+
segment += c
|
|
151
|
+
else if (state === SLASH) {
|
|
152
|
+
if (!pushProperty(segment, segments))
|
|
153
|
+
return undefined
|
|
154
|
+
segment = ""
|
|
155
|
+
state = OPEN_BRACKET
|
|
156
|
+
}
|
|
157
|
+
else if (state === CLOSE_BRACKET) {
|
|
158
|
+
if (segment)
|
|
159
|
+
return undefined
|
|
160
|
+
state = OPEN_BRACKET
|
|
161
|
+
}
|
|
162
|
+
else if (state === undefined) {
|
|
163
|
+
if (segment) {
|
|
164
|
+
if (!pushProperty(segment, segments))
|
|
165
|
+
return undefined
|
|
166
|
+
segment = ""
|
|
167
|
+
}
|
|
168
|
+
state = OPEN_BRACKET
|
|
169
|
+
}
|
|
170
|
+
else
|
|
171
|
+
return undefined
|
|
172
|
+
break
|
|
173
|
+
|
|
174
|
+
case ']':
|
|
175
|
+
if (state === SINGLE_QUOTE || state === DOUBLE_QUOTE)
|
|
176
|
+
segment += c
|
|
177
|
+
else if (state === OPEN_BRACKET) {
|
|
178
|
+
if (!segment)
|
|
179
|
+
return undefined
|
|
180
|
+
if (segment === '*')
|
|
181
|
+
segments.push({ kind: "wildcard-index" })
|
|
182
|
+
else
|
|
183
|
+
segments.push({ kind: "index", value: parseInt(segment, 10) })
|
|
184
|
+
segment = ""
|
|
185
|
+
state = CLOSE_BRACKET
|
|
186
|
+
}
|
|
187
|
+
else if (state === CLOSE_QUOTE) {
|
|
188
|
+
segments.push({ kind: "key", value: segment })
|
|
189
|
+
segment = ""
|
|
190
|
+
state = CLOSE_BRACKET
|
|
191
|
+
}
|
|
192
|
+
else
|
|
193
|
+
return undefined
|
|
194
|
+
break
|
|
195
|
+
|
|
196
|
+
case '\'':
|
|
197
|
+
if (escape || state === DOUBLE_QUOTE)
|
|
198
|
+
segment += c
|
|
199
|
+
else if (state === SINGLE_QUOTE)
|
|
200
|
+
state = CLOSE_QUOTE
|
|
201
|
+
else if (state === OPEN_BRACKET && !segment)
|
|
202
|
+
state = SINGLE_QUOTE
|
|
203
|
+
else
|
|
204
|
+
return undefined
|
|
205
|
+
break
|
|
206
|
+
|
|
207
|
+
case '"':
|
|
208
|
+
if (escape || state === SINGLE_QUOTE)
|
|
209
|
+
segment += c
|
|
210
|
+
else if (state === DOUBLE_QUOTE)
|
|
211
|
+
state = CLOSE_QUOTE
|
|
212
|
+
else if (state === OPEN_BRACKET && !segment)
|
|
213
|
+
state = DOUBLE_QUOTE
|
|
214
|
+
else
|
|
215
|
+
return undefined
|
|
216
|
+
break
|
|
217
|
+
|
|
218
|
+
default:
|
|
219
|
+
if (state === CLOSE_QUOTE)
|
|
220
|
+
return undefined
|
|
221
|
+
if (state === OPEN_BRACKET) {
|
|
222
|
+
if (c === '*') {
|
|
223
|
+
if (segment.length > 0)
|
|
224
|
+
return undefined
|
|
225
|
+
}
|
|
226
|
+
else if (c >= '0' && c <= '9') {
|
|
227
|
+
if (segment === '*')
|
|
228
|
+
return undefined
|
|
229
|
+
}
|
|
230
|
+
else
|
|
231
|
+
return undefined
|
|
232
|
+
}
|
|
233
|
+
segment += c
|
|
234
|
+
break
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
escape = false
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
switch (state) {
|
|
241
|
+
case undefined:
|
|
242
|
+
if (segment && !pushProperty(segment, segments))
|
|
243
|
+
return undefined
|
|
244
|
+
break
|
|
245
|
+
case CLOSE_BRACKET:
|
|
246
|
+
if (segment)
|
|
247
|
+
return undefined
|
|
248
|
+
break
|
|
249
|
+
case SLASH:
|
|
250
|
+
if (segment && !pushProperty(segment, segments))
|
|
251
|
+
return undefined
|
|
252
|
+
break
|
|
253
|
+
default:
|
|
254
|
+
return undefined
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (cache != null) {
|
|
258
|
+
if (cache.size >= 500)
|
|
259
|
+
cache.clear()
|
|
260
|
+
cache.set(path, segments.slice())
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return segments
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Regular expression to match RegExp special characters.
|
|
269
|
+
* @ignore
|
|
270
|
+
*/
|
|
271
|
+
const reRegExpChar = /[\\^$.*+?()[\]{}|]/g
|
|
272
|
+
/**
|
|
273
|
+
* Precompiled RegExp to test for RegExp special characters.
|
|
274
|
+
* @ignore
|
|
275
|
+
*/
|
|
276
|
+
const reHasRegExpChar = RegExp(reRegExpChar.source)
|
|
277
|
+
/**
|
|
278
|
+
* Escapes RegExp special characters in a string.
|
|
279
|
+
* @param s - The string to escape.
|
|
280
|
+
* @returns The escaped string.
|
|
281
|
+
* @ignore
|
|
282
|
+
*/
|
|
283
|
+
const escapeRegExp = (s: string) => s && reHasRegExpChar.test(s) ? s.replace(reRegExpChar, '\\$&') : s
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Converts an observer path (as PathElement array) to a regular expression string for matching actual paths.
|
|
287
|
+
*
|
|
288
|
+
* @param observerPath - The parsed observer path as an array of PathElement.
|
|
289
|
+
* @param currentPath - The current path context for resolving relative paths.
|
|
290
|
+
* @returns A regular expression string, or undefined if the path is invalid.
|
|
291
|
+
* @ignore
|
|
292
|
+
*/
|
|
293
|
+
export function observerPathToRegexp(observerPath: PathElement[] | undefined, currentPath: Path): string | undefined {
|
|
294
|
+
if (observerPath == null || observerPath.length === 0)
|
|
295
|
+
return undefined
|
|
296
|
+
|
|
297
|
+
if (observerPath[0].kind === 'root')
|
|
298
|
+
observerPath.shift()
|
|
299
|
+
else {
|
|
300
|
+
const parentPath = currentPath.slice(0, -1)
|
|
301
|
+
while (observerPath[0].kind === 'parent') {
|
|
302
|
+
if (parentPath.pop() == null)
|
|
303
|
+
return undefined
|
|
304
|
+
observerPath.shift()
|
|
305
|
+
}
|
|
306
|
+
observerPath.unshift(...parentPath.map(segment => ({ kind: typeof segment === "number" ? "index" : "property", value: segment } as PathElement)))
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const regexPath = observerPath.map((segment, index) => {
|
|
310
|
+
switch (segment.kind) {
|
|
311
|
+
case 'wildcard':
|
|
312
|
+
return (index === 0 ? "" : "\\.") + "[$_\\p{ID_Start}][$\\p{ID_Continue}]*"
|
|
313
|
+
case 'double-wildcard':
|
|
314
|
+
return (index === 0 ? "" : "\\.") + ".*"
|
|
315
|
+
case 'wildcard-index':
|
|
316
|
+
return "\\[[0-9]+\\]"
|
|
317
|
+
case "key":
|
|
318
|
+
return `\\['${ escapeRegExp(segment.value as string) }'\\]`
|
|
319
|
+
case "index":
|
|
320
|
+
return Number.isNaN(segment.value) ? "\\[[0-9]+\\]" : `\\[${ (segment.value as number).toFixed(0) }\\]`
|
|
321
|
+
default: // case "property":
|
|
322
|
+
return (index === 0 ? "" : "\\.") + escapeRegExp(segment.value as string)
|
|
323
|
+
}
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
return `^${ regexPath.join('') }$`
|
|
327
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { useEffect } from "react"
|
|
2
|
+
import { getClassConstructor, getMetadataFields } from "../../yop/Metadata"
|
|
3
|
+
import { Path, splitPath } from "../../yop/ObjectsUtil"
|
|
4
|
+
import { ClassConstructor } from "../../yop/TypesUtil"
|
|
5
|
+
import { FormManager, ReformSetValueEvent, SetValueOptions } from "../FormManager"
|
|
6
|
+
import { ObserverCallbackContext, ObserverCallbackOptions, ObserverMetadata, ObserversField, observer } from "./observer"
|
|
7
|
+
import { observerPathToRegexp, splitObserverPath } from "./observerPath"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Holds observer metadata and its associated path.
|
|
11
|
+
* @template T - The type of the observed value.
|
|
12
|
+
* @ignore
|
|
13
|
+
*/
|
|
14
|
+
type ObserverData<T> = {
|
|
15
|
+
observer: ObserverMetadata<T>
|
|
16
|
+
path: Path
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Recursively collects all observers from a model and its fields, populating the observers map.
|
|
21
|
+
* @template T
|
|
22
|
+
* @param path - The current path in the model.
|
|
23
|
+
* @param model - The class constructor to inspect.
|
|
24
|
+
* @param observersMap - The map to populate with observer data.
|
|
25
|
+
* @ignore
|
|
26
|
+
*/
|
|
27
|
+
function collectObservers<T>(path: Path, model: ClassConstructor<any>, observersMap: Map<string, ObserverData<T>[]>) {
|
|
28
|
+
const metadata = getMetadataFields(model) as Record<string, ObserversField>
|
|
29
|
+
|
|
30
|
+
Object.entries(metadata ?? {}).forEach(([name, fieldMetadata]) => {
|
|
31
|
+
const isArray = fieldMetadata.kind === "array"
|
|
32
|
+
|
|
33
|
+
path.push(name)
|
|
34
|
+
if (isArray)
|
|
35
|
+
path.push(Number.NaN)
|
|
36
|
+
|
|
37
|
+
fieldMetadata.observers?.forEach(observer => {
|
|
38
|
+
const observerPath = splitObserverPath(observer.path)
|
|
39
|
+
if (observerPath != null) {
|
|
40
|
+
const pathRegExp = observerPathToRegexp(observerPath, path)
|
|
41
|
+
if (pathRegExp != null) {
|
|
42
|
+
let observersData = observersMap.get(pathRegExp)
|
|
43
|
+
if (observersData == null) {
|
|
44
|
+
observersData = []
|
|
45
|
+
observersMap.set(pathRegExp, observersData)
|
|
46
|
+
}
|
|
47
|
+
observersData.push({ observer, path: path.concat() })
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const fieldModel = getClassConstructor(fieldMetadata)
|
|
53
|
+
if (fieldModel != null)
|
|
54
|
+
collectObservers(path, fieldModel as ClassConstructor<any>, observersMap)
|
|
55
|
+
|
|
56
|
+
if (isArray)
|
|
57
|
+
path.pop()
|
|
58
|
+
path.pop()
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Tracks whether setValue was called during observer execution.
|
|
65
|
+
* @ignore
|
|
66
|
+
*/
|
|
67
|
+
type SetValueCalled = { value: boolean }
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Creates the context object passed to observer callbacks.
|
|
71
|
+
* @template T
|
|
72
|
+
* @param path - The path to the field being observed.
|
|
73
|
+
* @param value - The current value at the path.
|
|
74
|
+
* @param event - The event that triggered the observer.
|
|
75
|
+
* @param setValueCalled - Tracks if setValue was called.
|
|
76
|
+
* @returns The observer callback context.
|
|
77
|
+
* @ignore
|
|
78
|
+
*/
|
|
79
|
+
function createCallbackContext<T>(path: Path, value: any, event: ReformSetValueEvent, setValueCalled: SetValueCalled): ObserverCallbackContext<T> {
|
|
80
|
+
return {
|
|
81
|
+
path: path,
|
|
82
|
+
observedValue: event.detail.value,
|
|
83
|
+
currentValue: value,
|
|
84
|
+
setValue: (value: any, options?: ObserverCallbackOptions) => {
|
|
85
|
+
const setValueOptions: SetValueOptions = { touch: true, propagate: false }
|
|
86
|
+
if (options != null) {
|
|
87
|
+
if (options.untouch === true)
|
|
88
|
+
setValueOptions.touch = false
|
|
89
|
+
if (options.propagate === true)
|
|
90
|
+
setValueOptions.propagate = true
|
|
91
|
+
}
|
|
92
|
+
event.detail.form.setValue(path, value, setValueOptions)
|
|
93
|
+
setValueCalled.value = true
|
|
94
|
+
},
|
|
95
|
+
event,
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Recursively calls observer callbacks for matching paths and values.
|
|
101
|
+
* @param observerData - The observer metadata and path.
|
|
102
|
+
* @param value - The current value at the path.
|
|
103
|
+
* @param startPath - The starting path for recursion.
|
|
104
|
+
* @param path - The remaining path to traverse.
|
|
105
|
+
* @param event - The event that triggered the observer.
|
|
106
|
+
* @param setValueCalled - Tracks if setValue was called.
|
|
107
|
+
* @ignore
|
|
108
|
+
*/
|
|
109
|
+
function callObservers(observerData: ObserverData<any>, value: any, startPath: Path, path: Path, event: ReformSetValueEvent, setValueCalled: SetValueCalled) {
|
|
110
|
+
if (path.length === 0 || value == null)
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
const pathElement = path[0]
|
|
114
|
+
if (typeof pathElement === "string") {
|
|
115
|
+
if (pathElement in value) {
|
|
116
|
+
value = value[pathElement]
|
|
117
|
+
if (path.length === 1)
|
|
118
|
+
observerData.observer.callback(createCallbackContext(startPath.concat(pathElement), value, event, setValueCalled))
|
|
119
|
+
else if (value != null)
|
|
120
|
+
callObservers(observerData, value, startPath.concat(pathElement), path.slice(1), event, setValueCalled)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
else if (Array.isArray(value)) {
|
|
124
|
+
const itemPath = path.slice(1)
|
|
125
|
+
|
|
126
|
+
if (Number.isNaN(pathElement)) {
|
|
127
|
+
value.forEach((item, itemIndex) => {
|
|
128
|
+
if (item != null) {
|
|
129
|
+
const newStartPath = startPath.concat(itemIndex)
|
|
130
|
+
if (itemPath.length === 0)
|
|
131
|
+
observerData.observer.callback(createCallbackContext(newStartPath, item, event, setValueCalled))
|
|
132
|
+
else
|
|
133
|
+
callObservers(observerData, item, newStartPath, itemPath, event, setValueCalled)
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
const item = value[pathElement]
|
|
139
|
+
if (item != null) {
|
|
140
|
+
const newStartPath = startPath.concat(pathElement)
|
|
141
|
+
if (itemPath.length === 0)
|
|
142
|
+
observerData.observer.callback(createCallbackContext(newStartPath, item, event, setValueCalled))
|
|
143
|
+
else
|
|
144
|
+
callObservers(observerData, item, newStartPath, itemPath, event, setValueCalled)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Creates an event listener that triggers observers for a given model.
|
|
152
|
+
* @param model - The class constructor to observe.
|
|
153
|
+
* @returns An event listener for reform events.
|
|
154
|
+
* @ignore
|
|
155
|
+
*/
|
|
156
|
+
function createReformEventListener(model: ClassConstructor<any>) {
|
|
157
|
+
const observersMap = new Map<string, ObserverData<any>[]>()
|
|
158
|
+
collectObservers([], model, observersMap)
|
|
159
|
+
const observers = Array.from(observersMap.entries()).map(([path, observerData]) => [new RegExp(path, "u"), observerData]) as [RegExp, ObserverData<any>[]][]
|
|
160
|
+
|
|
161
|
+
return ((event: ReformSetValueEvent) => {
|
|
162
|
+
const values = event.detail.form.values
|
|
163
|
+
if (values == null)
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
const eventPath = splitPath(event.detail.path) ?? []
|
|
167
|
+
const setValueCalled = { value: false }
|
|
168
|
+
|
|
169
|
+
observers.forEach(([pathRegExp, observersData]) => {
|
|
170
|
+
if (pathRegExp.test(event.detail.path)) {
|
|
171
|
+
observersData.forEach(observerData => {
|
|
172
|
+
let value: any = values
|
|
173
|
+
const startPath: Path = []
|
|
174
|
+
if (observerData.observer.path[0] !== '/') {
|
|
175
|
+
for (let i = 0; i < observerData.path.length && i < eventPath.length; i++) {
|
|
176
|
+
const pathSegment = observerData.path[i]
|
|
177
|
+
const eventSegment = eventPath[i]
|
|
178
|
+
if (pathSegment !== eventSegment && !(Number.isNaN(pathSegment) && (typeof eventSegment === "number")))
|
|
179
|
+
break
|
|
180
|
+
startPath.push(eventSegment)
|
|
181
|
+
value = value[eventSegment]
|
|
182
|
+
if (value == null)
|
|
183
|
+
break
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const path = (startPath.length > 0 ? observerData.path.slice(startPath.length) : observerData.path)
|
|
187
|
+
callObservers(observerData, value, startPath, path, event, setValueCalled)
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
if (setValueCalled.value) {
|
|
193
|
+
event.detail.form.validate()
|
|
194
|
+
event.detail.form.render()
|
|
195
|
+
}
|
|
196
|
+
}) as EventListener
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* React hook to register reform event listeners for {@link observer}s on a model. This hook scans the provided model class for any observer metadata
|
|
201
|
+
* and registers a single event listener on the form manager instance that will trigger the appropriate observer callbacks when relevant fields
|
|
202
|
+
* are updated.
|
|
203
|
+
*
|
|
204
|
+
* There is no need to use this hook if you are using the {@link useForm} hook with a model class, as observers will be automatically registered on
|
|
205
|
+
* the form manager instance.
|
|
206
|
+
*
|
|
207
|
+
* Example usage:
|
|
208
|
+
* ```tsx
|
|
209
|
+
* const form = useForm({
|
|
210
|
+
* initialValues: new MyFormModel(),
|
|
211
|
+
* validationSchema: instance({ of: MyFormModel }),
|
|
212
|
+
* onSubmit: (form) => { ... }
|
|
213
|
+
* })
|
|
214
|
+
* useObservers(MyFormModel, form)
|
|
215
|
+
* ```
|
|
216
|
+
*
|
|
217
|
+
* @template T
|
|
218
|
+
* @param model - The class constructor to scan for observers.
|
|
219
|
+
* @param form - The form manager instance holding the values to observe.
|
|
220
|
+
* @category Observers
|
|
221
|
+
*/
|
|
222
|
+
export function useObservers<T extends object>(model: ClassConstructor<T> | null | undefined, form: FormManager<unknown>) {
|
|
223
|
+
useEffect(() => {
|
|
224
|
+
if (model != null) {
|
|
225
|
+
const reformEventListener = createReformEventListener(model)
|
|
226
|
+
form.addReformEventListener(reformEventListener)
|
|
227
|
+
return () => {
|
|
228
|
+
form.removeReformEventListener(reformEventListener)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}, [model])
|
|
232
|
+
}
|