-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathform.js
143 lines (123 loc) · 4.06 KB
/
form.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
import React, {
memo,
createContext,
useCallback,
useRef,
forwardRef,
useMemo,
} from 'react'
import { func } from 'prop-types'
export const FormContext = createContext(null)
const getName = (ref) => ref.id || ref.name
const Form = forwardRef(({ onSubmit, ...rest }, ref) => {
const formRef = useRef(ref)
const touched = useRef({})
const fields = useRef({})
/**
* This is invoked from `useValidation`
* Each element, as it's mounted, must register with us so we can do things with them
* This happens in a `useEffect` - the disposable will call the unregister function.
*/
const register = useCallback((ref, ctx) => {
fields.current[getName(ref)] = { ref, ctx }
}, [])
const unregister = useCallback((ref) => {
delete fields.current[getName(ref)]
}, [])
/**
* Validates a single input.
* - Pass in a formInput to find relevant details (validation, update state function) from our fields ref.
* - this allows calling this routine from anywhere which is useful.
* - Also we pass along all the other form inputs so validation routines can check the state of the form.
*
* This is called in form#submit, and potentially change/blur on individual elements if configured.
* - must have been touched OR force = true
* - if constraints fail, return early with those errors
* - if constraints pass, call custom validation routines (if any)
* - if we get back an error from custom validation, set it on the input.
* - otherwise, call into `updateState` which fires callbacks for state updates
*
* @param {HtmlInputElement} formInput the input to validate
* @param {boolean} [force=false] whether to bypass touched check.
*/
const validateSingle = useCallback((ref, force = false) => {
const isTouched = touched.current[ref.name]
if (!force && !isTouched) return
ref.setCustomValidity('')
if (!ref.checkValidity()) return // the invalid event will have fired.
const { ctx } = fields.current[getName(ref)]
const refs = Object.entries(fields.current).map(([, { ref }]) => ref)
let [error] = (ctx.validation ?? [])
.map((fn) => fn(ref, refs))
.filter((valResult) => valResult != null)
if (typeof error === 'string') error = new Error(error)
if (error != null) {
ref.setCustomValidity(error.message)
ref.checkValidity()
} else {
ctx.updateState(null, ref.validity)
}
}, [])
/**
* Validates a single input, accounting for `others`
* If input has `others`: upon validation, all elements in `other` are validated as well.
*/
const validate = useCallback(
({ target: element }) => {
const { ctx } = fields.current[getName(element)]
const allFields = ctx.otherArray.reduce(
(acc, item) => {
const other = fields.current[item]
if (other) acc.push(other.ref)
return acc
},
[element]
)
allFields.forEach((field) => validateSingle(field))
},
[validateSingle]
)
/**
* Form submit handler
* Verify each of our inputs passes custom validation before calling onSubmit
* If custom validation fails replicate existing dom behavior of not submitting
*/
const handleSubmit = useCallback(
(e) => {
for (const [, { ref }] of Object.entries(fields.current)) {
validateSingle(ref, true)
}
if (e.target.checkValidity()) {
onSubmit?.(e)
} else {
e.preventDefault()
}
},
[onSubmit, validateSingle]
)
const setInputTouched = useCallback(
(e) => (touched.current[e.target.name] = true),
[touched]
)
const contextValue = useMemo(
() => ({
register,
unregister,
validate,
setInputTouched,
}),
[register, unregister, validate, setInputTouched]
)
return (
<FormContext.Provider value={contextValue}>
<form ref={formRef} onSubmit={handleSubmit} {...rest}></form>
</FormContext.Provider>
)
})
Form.displayName = 'Form'
Form.propTypes = {
onSubmit: func,
}
const memoized = memo(Form)
memoized.displayName = 'Memo(Form)'
export { memoized as Form }