@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,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
|
+
}
|