@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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +266 -0
  3. package/dist/index.d.ts +2715 -0
  4. package/dist/index.es.js +1715 -0
  5. package/dist/index.es.js.map +1 -0
  6. package/package.json +70 -0
  7. package/src/index.ts +90 -0
  8. package/src/reform/ArrayHelper.ts +164 -0
  9. package/src/reform/Form.tsx +81 -0
  10. package/src/reform/FormManager.ts +494 -0
  11. package/src/reform/Reform.ts +15 -0
  12. package/src/reform/components/BaseCheckboxField.tsx +72 -0
  13. package/src/reform/components/BaseDateField.tsx +84 -0
  14. package/src/reform/components/BaseRadioField.tsx +72 -0
  15. package/src/reform/components/BaseSelectField.tsx +103 -0
  16. package/src/reform/components/BaseTextAreaField.tsx +87 -0
  17. package/src/reform/components/BaseTextField.tsx +135 -0
  18. package/src/reform/components/InputHTMLProps.tsx +89 -0
  19. package/src/reform/observers/observer.ts +131 -0
  20. package/src/reform/observers/observerPath.ts +327 -0
  21. package/src/reform/observers/useObservers.ts +232 -0
  22. package/src/reform/useForm.ts +246 -0
  23. package/src/reform/useFormContext.tsx +37 -0
  24. package/src/reform/useFormField.ts +75 -0
  25. package/src/reform/useRender.ts +12 -0
  26. package/src/yop/MessageProvider.ts +204 -0
  27. package/src/yop/Metadata.ts +304 -0
  28. package/src/yop/ObjectsUtil.ts +811 -0
  29. package/src/yop/TypesUtil.ts +148 -0
  30. package/src/yop/ValidationContext.ts +207 -0
  31. package/src/yop/Yop.ts +430 -0
  32. package/src/yop/constraints/CommonConstraints.ts +124 -0
  33. package/src/yop/constraints/Constraint.ts +135 -0
  34. package/src/yop/constraints/MinMaxConstraints.ts +53 -0
  35. package/src/yop/constraints/OneOfConstraint.ts +40 -0
  36. package/src/yop/constraints/TestConstraint.ts +176 -0
  37. package/src/yop/decorators/array.ts +157 -0
  38. package/src/yop/decorators/boolean.ts +69 -0
  39. package/src/yop/decorators/date.ts +73 -0
  40. package/src/yop/decorators/email.ts +66 -0
  41. package/src/yop/decorators/file.ts +69 -0
  42. package/src/yop/decorators/id.ts +35 -0
  43. package/src/yop/decorators/ignored.ts +40 -0
  44. package/src/yop/decorators/instance.ts +110 -0
  45. package/src/yop/decorators/number.ts +73 -0
  46. package/src/yop/decorators/string.ts +90 -0
  47. package/src/yop/decorators/test.ts +41 -0
  48. 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
+ }