@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,72 @@
1
+ import React, { InputHTMLAttributes, useRef } from "react"
2
+ import { InputAttributes, ReformEvents } from "./InputHTMLProps"
3
+ import { useFormField } from "../.."
4
+
5
+ /**
6
+ * @ignore
7
+ */
8
+ export type BaseRadioFieldHTMLAttributes = Omit<InputAttributes<'radio'>,
9
+ 'accept' |
10
+ 'alt' |
11
+ 'autocomplete' |
12
+ 'capture' |
13
+ 'dirname' |
14
+ 'height' |
15
+ 'list' |
16
+ 'max' |
17
+ 'maxLength' |
18
+ 'min' |
19
+ 'minLength' |
20
+ 'multiple' |
21
+ 'placeholder' |
22
+ 'readOnly' |
23
+ 'size' |
24
+ 'src' |
25
+ 'step' |
26
+ 'type' |
27
+ 'width'
28
+ >
29
+
30
+ /**
31
+ * @category Base Inputs Components
32
+ */
33
+ export type BaseRadioFieldProps<V> = BaseRadioFieldHTMLAttributes & ReformEvents<V> & {
34
+ modelValue: V
35
+ render: () => void
36
+ }
37
+
38
+ /**
39
+ * A base radio field component that can be used to create custom radio input components connected to the form state.
40
+ * @category Base Inputs Components
41
+ */
42
+ export function BaseRadioField<V = any>(props: BaseRadioFieldProps<V>) {
43
+ const { onChange, onBlur, modelValue, render, ...inputProps } = props
44
+ const { value: fieldValue, form } = useFormField<V | null, number>(props.name)
45
+
46
+ const inputRef = useRef<HTMLInputElement>(null)
47
+
48
+ const internalOnChange = (event: React.ChangeEvent<HTMLInputElement>) => {
49
+ if (event.currentTarget.checked && modelValue !== fieldValue) {
50
+ form.setValue(props.name, modelValue, true)
51
+ onChange?.(modelValue, form)
52
+ }
53
+ }
54
+
55
+ // If this is the first render or if this input isn't currently edited
56
+ if (inputRef.current == null || inputRef.current !== document.activeElement) {
57
+ const value = fieldValue === modelValue
58
+ if (inputRef.current)
59
+ inputRef.current.checked = value
60
+ else
61
+ (inputProps as InputHTMLAttributes<HTMLInputElement>).defaultChecked = value
62
+ }
63
+
64
+ return (
65
+ <input
66
+ { ...inputProps }
67
+ type="radio"
68
+ ref={ inputRef }
69
+ onChange={ internalOnChange }
70
+ />
71
+ )
72
+ }
@@ -0,0 +1,103 @@
1
+ import React, { DOMAttributes, SelectHTMLAttributes, useRef } from "react"
2
+ import { ReformEvents } from "./InputHTMLProps"
3
+ import { useFormField } from "../useFormField"
4
+
5
+ /**
6
+ * @ignore
7
+ */
8
+ export type BaseSelectFieldHTMLAttributes = (
9
+ Omit<SelectHTMLAttributes<HTMLSelectElement>,
10
+ // HTMLAttributes
11
+ 'name' |
12
+
13
+ 'value' |
14
+
15
+ 'defaultValue' |
16
+ 'defaultChecked' |
17
+ 'suppressContentEditableWarning' |
18
+ 'suppressHydrationWarning' |
19
+
20
+ 'contentEditable' |
21
+ 'contextMenu' |
22
+ 'hidden' |
23
+ 'is' |
24
+
25
+ // SelectHTMLAttributes
26
+ 'autoComplete' |
27
+ 'form' |
28
+ 'multiple' |
29
+
30
+ keyof DOMAttributes<HTMLSelectElement>
31
+ > &
32
+ {
33
+ name: string
34
+ }
35
+ )
36
+
37
+ /**
38
+ * @category Base Inputs Components
39
+ */
40
+ export type BaseSelectFieldProps<V> = BaseSelectFieldHTMLAttributes & ReformEvents<V> & {
41
+ modelValues: V[]
42
+ toOptionValue: (modelValue: V) => string
43
+ toOptionContent: (modelValue: V) => string
44
+ toModelValue: (optionValue: string) => V
45
+ render: () => void
46
+ }
47
+
48
+ /**
49
+ * A base select field component that can be used to create custom select input components connected to the form state.
50
+ *
51
+ * To use it with with a basic { value, label } pair, you can use the following props:
52
+ * ```tsx
53
+ * <BaseSelectField
54
+ * name="mySelectId"
55
+ * modelValues={[ null, "1", "2" ]}
56
+ * toOptionValue={ modelValue => modelValue ?? "" }
57
+ * toOptionContent={ modelValue => modelValue == null ? "Select..." : `Option ${modelValue}` }
58
+ * toModelValue={ optionValue => optionValue === "" ? null : optionValue }
59
+ * render={ myRenderFunction } />
60
+ * ```
61
+ * @category Base Inputs Components
62
+ */
63
+ export function BaseSelectField<Value = string>(props: BaseSelectFieldProps<Value | null>) {
64
+
65
+ const { onChange, onBlur, toModelValue, render, modelValues, toOptionValue, toOptionContent, ...selectProps } = props
66
+ const { value: fieldValue, form } = useFormField<Value | null, number>(props.name)
67
+
68
+ const selectRef = useRef<HTMLSelectElement>(null)
69
+
70
+ const internalOnChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
71
+ const value = toModelValue(event.currentTarget.value)
72
+ if (value !== fieldValue) {
73
+ form.setValue(props.name, value, true)
74
+ onChange?.(value, form)
75
+ }
76
+ }
77
+
78
+ // If this is the first render or if this select isn't currently edited
79
+ if (selectRef.current == null || selectRef.current !== document.activeElement) {
80
+ const value = toOptionValue(fieldValue ?? null)
81
+ if (selectRef.current != null)
82
+ selectRef.current.value = value
83
+ else
84
+ (selectProps as SelectHTMLAttributes<HTMLSelectElement>).defaultValue = value
85
+ }
86
+
87
+ return (
88
+ <select
89
+ { ...selectProps }
90
+ ref={ selectRef }
91
+ onChange={ internalOnChange }
92
+ >
93
+ { modelValues.map(modelValue => {
94
+ const optionValue = toOptionValue(modelValue)
95
+ return (
96
+ <option key={ optionValue } value={ optionValue }>
97
+ { toOptionContent(modelValue)}
98
+ </option>
99
+ )
100
+ })}
101
+ </select>
102
+ )
103
+ }
@@ -0,0 +1,87 @@
1
+ import React, { DOMAttributes, TextareaHTMLAttributes, useRef } from "react"
2
+ import { useFormField } from "../useFormField"
3
+ import { ReformEvents } from "./InputHTMLProps"
4
+
5
+ /**
6
+ * @ignore
7
+ */
8
+ export type BaseTextAreaFieldHTMLAttributes = (
9
+ Omit<TextareaHTMLAttributes<HTMLTextAreaElement>,
10
+ // HTMLAttributes
11
+ 'name' |
12
+
13
+ 'value' |
14
+
15
+ 'defaultValue' |
16
+ 'defaultChecked' |
17
+ 'suppressContentEditableWarning' |
18
+ 'suppressHydrationWarning' |
19
+
20
+ 'contentEditable' |
21
+ 'contextMenu' |
22
+ 'hidden' |
23
+ 'is' |
24
+
25
+ // TextareaHTMLAttributes
26
+ 'autoComplete' |
27
+ 'dirName' |
28
+ 'form' |
29
+
30
+ keyof DOMAttributes<HTMLTextAreaElement>
31
+ > &
32
+ {
33
+ name: string
34
+ }
35
+ )
36
+
37
+ /**
38
+ * @category Base Inputs Components
39
+ */
40
+ export type BaseTextAreaFieldProps = BaseTextAreaFieldHTMLAttributes & ReformEvents<string> & {
41
+ render: () => void
42
+ }
43
+
44
+ /**
45
+ * A base text area component that can be used to create custom text area input components connected to the form state.
46
+ * @category Base Inputs Components
47
+ */
48
+ export function BaseTextAreaField(props: BaseTextAreaFieldProps) {
49
+
50
+ const { render, onChange, onBlur, ...textAreaProps } = props
51
+ const { value: fieldValue, form } = useFormField<string | null, number>(props.name)
52
+
53
+ const textAreaRef = useRef<HTMLTextAreaElement>(null)
54
+
55
+ const internalOnChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
56
+ const value = event.currentTarget.value || null
57
+ if (value !== fieldValue) {
58
+ form.setValue(props.name, value)
59
+ form.validateAt(props.name) && render()
60
+ onChange?.(value, form)
61
+ }
62
+ }
63
+
64
+ const internalOnBlur = (event: React.FocusEvent<HTMLTextAreaElement>) => {
65
+ const value = event.currentTarget.value || null
66
+ form.setValue(props.name, value, true)
67
+ onBlur?.(value, form)
68
+ }
69
+
70
+ // If this is the first render or if this textarea isn't currently edited
71
+ if (textAreaRef.current == null || textAreaRef.current !== document.activeElement) {
72
+ const value = fieldValue ?? ""
73
+ if (textAreaRef.current != null)
74
+ textAreaRef.current.value = value
75
+ else
76
+ (textAreaProps as TextareaHTMLAttributes<HTMLTextAreaElement>).defaultValue = value
77
+ }
78
+
79
+ return (
80
+ <textarea
81
+ { ...textAreaProps }
82
+ ref={ textAreaRef }
83
+ onChange={ internalOnChange }
84
+ onBlur={ internalOnBlur }
85
+ />
86
+ )
87
+ }
@@ -0,0 +1,135 @@
1
+ import React, { InputHTMLAttributes, useRef } from "react"
2
+ import { InputAttributes, ReformEvents } from "./InputHTMLProps"
3
+ import { useFormField } from "../useFormField"
4
+
5
+ /**
6
+ * @ignore
7
+ */
8
+ export interface InputSelection {
9
+ start: number | null
10
+ end: number | null
11
+ direction?: "forward" | "backward" | "none"
12
+ }
13
+
14
+ /**
15
+ * @ignore
16
+ */
17
+ export type BaseTextFieldHTMLAttributes = Omit<InputAttributes<"text" | "search" | "number" | "date" | "email" | "password" | "tel" | "time">,
18
+ 'accept' |
19
+ 'alt' |
20
+ 'capture' |
21
+ 'height' |
22
+ 'multiple' |
23
+ 'src' |
24
+ 'step' |
25
+ 'width'
26
+ >
27
+
28
+ /**
29
+ * @category Base Inputs Components
30
+ */
31
+ export type BaseTextFieldProps<V> = BaseTextFieldHTMLAttributes & ReformEvents<V> & {
32
+ toModelValue?: (value: string) => V | null
33
+ toTextValue?: (value: V | null) => string
34
+ acceptInputValue?: (value: string) => boolean
35
+ formatDisplayedValue?: (value: string) => string
36
+ formatOnEdit?: boolean
37
+ render: () => void
38
+ }
39
+
40
+ /**
41
+ * A base text field component that can be used to create custom text input components connected to the form state. It handles change and blur events,
42
+ * converts between the input value and the model value using the provided `toModelValue` and `toTextValue` functions, and calls the provided `render`
43
+ * function to update the form state when the value changes. It also supports an `acceptInputValue` function to validate the input value on each change
44
+ * and a `formatDisplayedValue` function to format the displayed value while editing.
45
+ * @category Base Inputs Components
46
+ */
47
+ export function BaseTextField<Value = string>(props: BaseTextFieldProps<Value>) {
48
+
49
+ const { onChange, onBlur, toModelValue, toTextValue, acceptInputValue, formatDisplayedValue, formatOnEdit, render, ...inputProps } = props
50
+ const { value: fieldValue, form } = useFormField<Value | null, number>(props.name)
51
+
52
+ const inputRef = useRef<HTMLInputElement>(null)
53
+ const previousInputValue = useRef('')
54
+ const previousInputSelection = useRef<InputSelection>({ start: null, end: null })
55
+
56
+ const getInputValue = (event: React.SyntheticEvent<HTMLInputElement>) => {
57
+ const value = event.currentTarget.value.replace(/\0/g, '')
58
+ if (toModelValue)
59
+ return toModelValue(value)
60
+ return value === '' ? null : value as Value
61
+ }
62
+
63
+ const internalOnSelect = (event: React.FormEvent<HTMLInputElement>) => {
64
+ const target = event.currentTarget
65
+
66
+ previousInputSelection.current = {
67
+ start: target.selectionStart,
68
+ end: target.selectionEnd,
69
+ direction: target.selectionDirection ?? undefined
70
+ }
71
+
72
+ // format displayed value when cursor is moved at the end of typed text
73
+ if (formatOnEdit !== false && formatDisplayedValue && target.selectionStart === target.value.length) {
74
+ const formattedValue = formatDisplayedValue(target.value)
75
+ if (target.value !== formattedValue)
76
+ target.value = formattedValue
77
+ }
78
+ }
79
+
80
+ const internalOnInput = (event: React.FormEvent<HTMLInputElement>) => {
81
+ const target = event.currentTarget
82
+
83
+ // Discard changes if it doesn't conform to acceptInputValue (could also be handled by a beforeInput event)
84
+ if (acceptInputValue?.(target.value) === false) {
85
+ target.value = previousInputValue.current
86
+ const selection = previousInputSelection.current!
87
+ target.setSelectionRange(selection.start, selection.end, selection.direction)
88
+ }
89
+ // format displayed value when cursor is at the end of typed text
90
+ else if (formatOnEdit !== false && formatDisplayedValue && target.selectionStart === target.value.length) {
91
+ const formattedValue = formatDisplayedValue(target.value)
92
+ if (target.value !== formattedValue)
93
+ target.value = formattedValue
94
+ }
95
+ }
96
+
97
+ const internalOnChange = (event: React.ChangeEvent<HTMLInputElement>) => {
98
+ previousInputValue.current = event.currentTarget.value
99
+ const value = getInputValue(event)
100
+ if (value !== fieldValue) {
101
+ form.setValue(props.name, value)
102
+ if (form.validateAt(props.name).changed)
103
+ render()
104
+ onChange?.(value, form)
105
+ }
106
+ }
107
+
108
+ const internalOnBlur = (event: React.FocusEvent<HTMLInputElement>) => {
109
+ const value = getInputValue(event)
110
+ form.setValue(props.name, value, true)
111
+ onBlur?.(value, form)
112
+ }
113
+
114
+ // If this is the first render or if this input isn't currently edited
115
+ if (inputRef.current == null || inputRef.current !== document.activeElement) {
116
+ const convertedValue = toTextValue?.(fieldValue ?? null) ?? String(fieldValue ?? '')
117
+ const value = formatDisplayedValue?.(convertedValue) ?? convertedValue
118
+ if (inputRef.current)
119
+ inputRef.current.value = value
120
+ else
121
+ (inputProps as InputHTMLAttributes<HTMLInputElement>).defaultValue = value
122
+ previousInputValue.current = value
123
+ }
124
+
125
+ return (
126
+ <input
127
+ { ...inputProps }
128
+ ref={ inputRef }
129
+ onSelect={ internalOnSelect }
130
+ onInput={ internalOnInput }
131
+ onChange={ internalOnChange }
132
+ onBlur={ internalOnBlur }
133
+ />
134
+ )
135
+ }
@@ -0,0 +1,89 @@
1
+ import { DOMAttributes, InputHTMLAttributes } from "react"
2
+ import { FormManager } from "../FormManager";
3
+
4
+ /**
5
+ * The HTMLInputTypeAttribute type is a union of string literals that represent the valid values for the "type" attribute of an HTML input element
6
+ * It includes common input types such as "text", "password", "email", etc., as well as a catch-all for any other string value that may be used as a
7
+ * custom input type. This allows for flexibility while still providing type safety for known input types.
8
+ * @ignore
9
+ */
10
+ type HTMLInputTypeAttribute =
11
+ | 'button'
12
+ | 'checkbox'
13
+ | 'color'
14
+ | 'date'
15
+ | 'datetime-local'
16
+ | 'email'
17
+ | 'file'
18
+ | 'hidden'
19
+ | 'image'
20
+ | 'month'
21
+ | 'number'
22
+ | 'password'
23
+ | 'radio'
24
+ | 'range'
25
+ | 'reset'
26
+ | 'search'
27
+ | 'submit'
28
+ | 'tel'
29
+ | 'text'
30
+ | 'time'
31
+ | 'url'
32
+ | 'week'
33
+ | (string & {});
34
+
35
+ /**
36
+ * The InputAttributes type is a utility type that takes an InputType parameter, which extends the HTMLInputTypeAttribute type. It uses the Omit utility
37
+ * type to exclude certain properties from the InputHTMLAttributes type, such as 'name', 'type', 'value', 'checked', and others that are not relevant
38
+ * for the input component. It then adds back the 'name' property as a required string and the 'type' property as an optional InputType. This allows for
39
+ * creating a more specific set of attributes for input components while still maintaining flexibility for different input types.
40
+ * @ignore
41
+ */
42
+ export type InputAttributes<InputType extends HTMLInputTypeAttribute> = (
43
+ Omit<InputHTMLAttributes<HTMLInputElement>,
44
+ // HTMLAttributes
45
+ 'name' |
46
+ 'type' |
47
+
48
+ 'value' |
49
+ 'checked' |
50
+
51
+ 'defaultValue' |
52
+ 'defaultChecked' |
53
+ 'suppressContentEditableWarning' |
54
+ 'suppressHydrationWarning' |
55
+
56
+ 'contentEditable' |
57
+ 'contextMenu' |
58
+ 'hidden' |
59
+ 'is' |
60
+
61
+ // InputHTMLAttributes
62
+ 'alt' |
63
+ 'form' |
64
+ 'formaction' |
65
+ 'formenctype' |
66
+ 'formmethod' |
67
+ 'formnovalidate' |
68
+ 'formtarget' |
69
+ 'pattern' |
70
+ 'src' |
71
+ keyof DOMAttributes<HTMLInputElement>
72
+ > &
73
+ {
74
+ name: string
75
+ type?: InputType
76
+ }
77
+ )
78
+
79
+ /**
80
+ * The ReformEvents type is a generic type that takes two parameters: Value, which represents the type of the value being handled, and Root,
81
+ * which extends object and represents the type of the form's root state. It defines two optional event handler properties: onChange and onBlur.
82
+ * Both handlers receive the current value (which can be of type Value or null) and an instance of FormManager that manages the form's state.
83
+ * This allows for handling changes and blur events in a way that is integrated with the form management system.
84
+ * @category Base Inputs Components
85
+ */
86
+ export type ReformEvents<Value, Root extends object = any> = {
87
+ onChange?: (value: Value | null, form: FormManager<Root>) => void
88
+ onBlur?: (value: Value | null, form: FormManager<Root>) => void
89
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Defines types and a decorator for observers in the Reform form management system. Observers allow you to react
3
+ * to changes in other fields by specifying a path to observe and a callback function that receives context about
4
+ * the change. The observer paths support a flexible syntax for targeting specific fields, including wildcards and
5
+ * relative paths.
6
+ */
7
+ import { useObservers } from "./useObservers"
8
+ import { InternalCommonConstraints } from "../../yop/constraints/CommonConstraints"
9
+ import { fieldDecorator } from "../../yop/Metadata"
10
+ import { Path } from "../../yop/ObjectsUtil"
11
+ import { ReformSetValueEvent } from "../FormManager"
12
+ import { FormConfig } from "../useForm"
13
+
14
+ /**
15
+ * Options for observer callback behavior.
16
+ * @category Observers
17
+ */
18
+ export type ObserverCallbackOptions = {
19
+
20
+ /** If `true`, marks the field as untouched. */
21
+ untouch?: boolean
22
+ /** If `true`, propagates the value change to other observers. Defaults to `false` to prevent potential infinite loops*/
23
+ propagate?: boolean
24
+ }
25
+
26
+ /**
27
+ * Context object passed to observer callbacks.
28
+ *
29
+ * @template T - The type of the field value where the observer is attached.
30
+ * @category Observers
31
+ */
32
+ export type ObserverCallbackContext<T> = {
33
+
34
+ /** The absolute path of the field value where the observer is attached */
35
+ path: Path
36
+ /** The value being observed. */
37
+ observedValue: unknown
38
+ /** The current value of the field where the observer is attached. */
39
+ currentValue: T
40
+ /** Sets the value of the field where the observer is attached. */
41
+ setValue: (value: T, options?: ObserverCallbackOptions) => void
42
+ /** The event object providing context. See {@link ReformSetValueEvent}. */
43
+ event: ReformSetValueEvent<unknown>
44
+ }
45
+
46
+ /**
47
+ * Observer callback function type.
48
+ *
49
+ * @template T - The type of the field value where the observer is attached.
50
+ * @param context - The context for the observer callback. See {@link ObserverCallbackContext}.
51
+ * @category Observers
52
+ */
53
+ export type ObserverCallback<T> = (context: ObserverCallbackContext<T>) => void
54
+
55
+ /**
56
+ * Metadata for an observer, including its path and callback.
57
+ *
58
+ * @template T - The type of the field value where the observer is attached.
59
+ * @ignore
60
+ */
61
+ export type ObserverMetadata<T> = {
62
+
63
+ /** The path of the field being observed. */
64
+ path: string
65
+ /** The observer callback function. */
66
+ callback: ObserverCallback<T>
67
+ }
68
+
69
+ /**
70
+ * Map of observer metadata, keyed by observed path.
71
+ * @ignore
72
+ */
73
+ export type Observers = Map<string, ObserverMetadata<any>>
74
+
75
+ /**
76
+ * Field metadata type that can hold observers in addition to common constraints.
77
+ * @ignore
78
+ */
79
+ export type ObserversField = InternalCommonConstraints & { observers?: Observers }
80
+
81
+ /**
82
+ * Decorator to register or remove an observer callback for a field. Observer paths use a syntax similar to Unix file paths,
83
+ * supporting wildcards and array indices, and are relative to the parent object of the field where the observer is attached.
84
+ * Paths can also be absolute (starting with a slash `/`) to observe fields from the root, or use `..` to observe fields from
85
+ * higher up in the object tree.
86
+ *
87
+ * Observers are only triggered if {@link FormConfig.dispatchEvent} isn't set to `false` and if the class holding `observers`
88
+ * decorators is bound to the current form using {@link useObservers}.
89
+ *
90
+ * Examples:
91
+ * - `name` observes the `name` field of the parent object.
92
+ * - `user/name` observes the `name` field of the sibling `user` object.
93
+ * - `items[*]/price` observes the `price` field of all items in the sibling `items` array.
94
+ * - `orders[0]/status` observes the `status` field of the first order in the sibling `orders` array.
95
+ * - `/name` observes the `name` field at the root level.
96
+ * - `../name` observes the `name` field in the parent of the parent object.
97
+ *
98
+ * Example usage:
99
+ * ```tsx
100
+ * class MyFormModel {
101
+ *
102
+ * age: number | null = null
103
+ *
104
+ * @observer("age", (context) => context.setValue(
105
+ * context.observedValue != null ? (context.observedValue as number) >= 18 : null,
106
+ * { untouch: true }
107
+ * ))
108
+ * adult: boolean | null = null
109
+ * }
110
+ *
111
+ * const form = useForm(MyFormModel, ...)
112
+ *
113
+ * ```
114
+ *
115
+ * @template Value - The type of the field value where the observer is attached.
116
+ * @template Parent - The parent type containing the field.
117
+ * @param path - The path to observe.
118
+ * @param callback - The observer callback function, or undefined to remove it.
119
+ * @returns A field decorator function.
120
+ * @category Observers
121
+ */
122
+ export function observer<Value, Parent>(path: string, callback: ObserverCallback<Value> | undefined) {
123
+ return fieldDecorator<Parent, Value>(field => {
124
+ const metadata = field as ObserversField
125
+ metadata.observers ??= new Map()
126
+ if (callback != null)
127
+ metadata.observers.set(path, { path, callback })
128
+ else
129
+ metadata.observers.delete(path)
130
+ })
131
+ }