kiru 0.44.4
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 +7 -0
- package/README.md +5 -0
- package/package.json +81 -0
- package/src/appContext.ts +186 -0
- package/src/cloneVNode.ts +14 -0
- package/src/constants.ts +146 -0
- package/src/context.ts +56 -0
- package/src/dom.ts +712 -0
- package/src/element.ts +54 -0
- package/src/env.ts +6 -0
- package/src/error.ts +85 -0
- package/src/flags.ts +15 -0
- package/src/form/index.ts +662 -0
- package/src/form/types.ts +261 -0
- package/src/form/utils.ts +19 -0
- package/src/generateId.ts +19 -0
- package/src/globalContext.ts +161 -0
- package/src/globals.ts +21 -0
- package/src/hmr.ts +178 -0
- package/src/hooks/index.ts +14 -0
- package/src/hooks/useAsync.ts +136 -0
- package/src/hooks/useCallback.ts +31 -0
- package/src/hooks/useContext.ts +79 -0
- package/src/hooks/useEffect.ts +44 -0
- package/src/hooks/useEffectEvent.ts +24 -0
- package/src/hooks/useId.ts +42 -0
- package/src/hooks/useLayoutEffect.ts +47 -0
- package/src/hooks/useMemo.ts +33 -0
- package/src/hooks/useReducer.ts +50 -0
- package/src/hooks/useRef.ts +40 -0
- package/src/hooks/useState.ts +62 -0
- package/src/hooks/useSyncExternalStore.ts +59 -0
- package/src/hooks/useViewTransition.ts +26 -0
- package/src/hooks/utils.ts +259 -0
- package/src/hydration.ts +67 -0
- package/src/index.ts +61 -0
- package/src/jsx.ts +11 -0
- package/src/lazy.ts +238 -0
- package/src/memo.ts +48 -0
- package/src/portal.ts +43 -0
- package/src/profiling.ts +105 -0
- package/src/props.ts +36 -0
- package/src/reconciler.ts +531 -0
- package/src/renderToString.ts +91 -0
- package/src/router/index.ts +2 -0
- package/src/router/route.ts +51 -0
- package/src/router/router.ts +275 -0
- package/src/router/routerUtils.ts +49 -0
- package/src/scheduler.ts +522 -0
- package/src/signals/base.ts +237 -0
- package/src/signals/computed.ts +139 -0
- package/src/signals/effect.ts +60 -0
- package/src/signals/globals.ts +11 -0
- package/src/signals/index.ts +12 -0
- package/src/signals/jsx.ts +45 -0
- package/src/signals/types.ts +10 -0
- package/src/signals/utils.ts +12 -0
- package/src/signals/watch.ts +151 -0
- package/src/ssr/client.ts +29 -0
- package/src/ssr/hydrationBoundary.ts +63 -0
- package/src/ssr/index.ts +1 -0
- package/src/ssr/server.ts +124 -0
- package/src/store.ts +241 -0
- package/src/swr.ts +360 -0
- package/src/transition.ts +80 -0
- package/src/types.dom.ts +1250 -0
- package/src/types.ts +209 -0
- package/src/types.utils.ts +39 -0
- package/src/utils.ts +581 -0
- package/src/warning.ts +9 -0
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
import { __DEV__ } from "../env.js"
|
|
2
|
+
import { Fragment } from "../element.js"
|
|
3
|
+
import { generateRandomID } from "../generateId.js"
|
|
4
|
+
import { safeStringify, shallowCompare } from "../utils.js"
|
|
5
|
+
import { useEffect } from "../hooks/useEffect.js"
|
|
6
|
+
import { useMemo } from "../hooks/useMemo.js"
|
|
7
|
+
import { useRef } from "../hooks/useRef.js"
|
|
8
|
+
import { useHook, useRequestUpdate } from "../hooks/utils.js"
|
|
9
|
+
import { objGet, objSet } from "./utils.js"
|
|
10
|
+
import type {
|
|
11
|
+
AsyncValidatorContext,
|
|
12
|
+
FormContext,
|
|
13
|
+
FormController,
|
|
14
|
+
FormFieldContext,
|
|
15
|
+
FormFieldProps,
|
|
16
|
+
FormFieldState,
|
|
17
|
+
FormFieldValidators,
|
|
18
|
+
FormStateSubscriber,
|
|
19
|
+
FormSubscribeProps,
|
|
20
|
+
InferRecordKeyValue,
|
|
21
|
+
RecordKey,
|
|
22
|
+
SelectorState,
|
|
23
|
+
UseFormConfig,
|
|
24
|
+
UseFormInternalState,
|
|
25
|
+
UseFormState,
|
|
26
|
+
} from "./types"
|
|
27
|
+
|
|
28
|
+
export type * from "./types"
|
|
29
|
+
|
|
30
|
+
function createFormController<T extends Record<string, unknown>>(
|
|
31
|
+
config: UseFormConfig<T>
|
|
32
|
+
): FormController<T> {
|
|
33
|
+
let isSubmitting = false
|
|
34
|
+
const subscribers = new Set<FormStateSubscriber<T>>()
|
|
35
|
+
const state: T = structuredClone(config.initialValues ?? {}) as T
|
|
36
|
+
const formFieldValidators = {} as {
|
|
37
|
+
[fieldName in RecordKey<T>]: Partial<
|
|
38
|
+
FormFieldValidators<RecordKey<T>, InferRecordKeyValue<T, fieldName>>
|
|
39
|
+
>
|
|
40
|
+
}
|
|
41
|
+
const formFieldDependencies = {} as {
|
|
42
|
+
[key in RecordKey<T>]: Set<RecordKey<T>>
|
|
43
|
+
}
|
|
44
|
+
const formFieldsTouched = {} as {
|
|
45
|
+
[key in RecordKey<T>]?: boolean
|
|
46
|
+
}
|
|
47
|
+
const formFieldUpdaters = new Map<RecordKey<T>, Set<() => void>>()
|
|
48
|
+
const asyncFormFieldValidators = {} as {
|
|
49
|
+
[key in RecordKey<T>]: {
|
|
50
|
+
onChangeAsync?: {
|
|
51
|
+
timeout: number
|
|
52
|
+
epoch: number
|
|
53
|
+
abortController: AbortController | null
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const formFieldErrors = {} as {
|
|
58
|
+
[key in RecordKey<T>]: {
|
|
59
|
+
onMount?: any
|
|
60
|
+
onChange?: any
|
|
61
|
+
onBlur?: any
|
|
62
|
+
onSubmit?: any
|
|
63
|
+
onChangeAsync?: any
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const canSubmit = () => {
|
|
68
|
+
return isAnyFieldValidating() === false && getErrors().length === 0
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const getSelectorState = (): SelectorState<T> => {
|
|
72
|
+
return {
|
|
73
|
+
values: state,
|
|
74
|
+
canSubmit: canSubmit(),
|
|
75
|
+
isSubmitting,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const updateSubscribers = () => {
|
|
80
|
+
const selectorState = getSelectorState()
|
|
81
|
+
for (const sub of subscribers) {
|
|
82
|
+
const { selector, selection, update } = sub
|
|
83
|
+
const newSelection = selector(selectorState)
|
|
84
|
+
if (shallowCompare(selection, newSelection)) continue
|
|
85
|
+
sub.selection = newSelection
|
|
86
|
+
update()
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const updateFieldComponents = (name: RecordKey<T>) => {
|
|
91
|
+
if (!formFieldUpdaters.has(name)) return
|
|
92
|
+
for (const update of formFieldUpdaters.get(name)!) update()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const validateField = async <K extends RecordKey<T>>(
|
|
96
|
+
name: K,
|
|
97
|
+
evt: keyof Omit<
|
|
98
|
+
FormFieldValidators<RecordKey<T>, InferRecordKeyValue<T, K>>,
|
|
99
|
+
"onChangeAsyncDebounceMs"
|
|
100
|
+
>
|
|
101
|
+
) => {
|
|
102
|
+
const fieldErrors = (formFieldErrors[name] ??= {})
|
|
103
|
+
const asyncFieldValidatorStates = (asyncFormFieldValidators[name] ??= {})
|
|
104
|
+
const fieldValidators = formFieldValidators[name] ?? {}
|
|
105
|
+
|
|
106
|
+
const validatorCtx = { value: getFieldValue(name) }
|
|
107
|
+
|
|
108
|
+
switch (evt) {
|
|
109
|
+
case "onMount": {
|
|
110
|
+
if (!fieldValidators.onMount) return
|
|
111
|
+
|
|
112
|
+
fieldErrors.onMount = fieldValidators.onMount(validatorCtx)
|
|
113
|
+
updateSubscribers()
|
|
114
|
+
updateFieldComponents(name)
|
|
115
|
+
break
|
|
116
|
+
}
|
|
117
|
+
case "onChange": {
|
|
118
|
+
if (fieldErrors.onMount) delete fieldErrors.onMount
|
|
119
|
+
formFieldsTouched[name] = true
|
|
120
|
+
if (!fieldValidators.onChange) return
|
|
121
|
+
|
|
122
|
+
fieldErrors.onChange = fieldValidators.onChange(validatorCtx)
|
|
123
|
+
|
|
124
|
+
if (
|
|
125
|
+
asyncFieldValidatorStates.onChangeAsync &&
|
|
126
|
+
asyncFieldValidatorStates.onChangeAsync.timeout !== -1
|
|
127
|
+
) {
|
|
128
|
+
window.clearTimeout(asyncFieldValidatorStates.onChangeAsync.timeout)
|
|
129
|
+
asyncFieldValidatorStates.onChangeAsync.abortController?.abort()
|
|
130
|
+
|
|
131
|
+
asyncFieldValidatorStates.onChangeAsync = {
|
|
132
|
+
epoch: asyncFieldValidatorStates.onChangeAsync.epoch,
|
|
133
|
+
timeout: -1,
|
|
134
|
+
abortController: null,
|
|
135
|
+
}
|
|
136
|
+
delete fieldErrors.onChangeAsync
|
|
137
|
+
} else if (fieldErrors.onChangeAsync) {
|
|
138
|
+
delete fieldErrors.onChangeAsync
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
updateSubscribers()
|
|
142
|
+
updateFieldComponents(name)
|
|
143
|
+
break
|
|
144
|
+
}
|
|
145
|
+
case "onBlur": {
|
|
146
|
+
formFieldsTouched[name] = true
|
|
147
|
+
if (!fieldValidators.onBlur) return
|
|
148
|
+
fieldErrors.onBlur = fieldValidators.onBlur(validatorCtx)
|
|
149
|
+
updateSubscribers()
|
|
150
|
+
updateFieldComponents(name)
|
|
151
|
+
break
|
|
152
|
+
}
|
|
153
|
+
case "onChangeAsync": {
|
|
154
|
+
if (fieldErrors.onChange || !fieldValidators.onChangeAsync) return
|
|
155
|
+
|
|
156
|
+
window.clearTimeout(asyncFieldValidatorStates.onChangeAsync?.timeout)
|
|
157
|
+
|
|
158
|
+
const epoch = (asyncFieldValidatorStates.onChangeAsync?.epoch ?? 0) + 1
|
|
159
|
+
const debounceMs = fieldValidators.onChangeAsyncDebounceMs ?? 0
|
|
160
|
+
|
|
161
|
+
const abortController = new AbortController()
|
|
162
|
+
const asyncValidatorCtx: AsyncValidatorContext<any> = {
|
|
163
|
+
...validatorCtx,
|
|
164
|
+
abortSignal: abortController.signal,
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (debounceMs <= 0) {
|
|
168
|
+
fieldErrors.onChangeAsync = await fieldValidators.onChangeAsync(
|
|
169
|
+
asyncValidatorCtx
|
|
170
|
+
)
|
|
171
|
+
updateSubscribers()
|
|
172
|
+
updateFieldComponents(name)
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
asyncFieldValidatorStates.onChangeAsync = {
|
|
176
|
+
abortController,
|
|
177
|
+
timeout: window.setTimeout(() => {
|
|
178
|
+
fieldValidators
|
|
179
|
+
.onChangeAsync?.(asyncValidatorCtx)
|
|
180
|
+
.then((result) => {
|
|
181
|
+
if (fieldErrors.onChange) return
|
|
182
|
+
if (epoch !== asyncFieldValidatorStates.onChangeAsync?.epoch) {
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
fieldErrors.onChangeAsync = result
|
|
186
|
+
asyncFieldValidatorStates.onChangeAsync = {
|
|
187
|
+
timeout: -1,
|
|
188
|
+
epoch,
|
|
189
|
+
abortController,
|
|
190
|
+
}
|
|
191
|
+
updateSubscribers()
|
|
192
|
+
updateFieldComponents(name)
|
|
193
|
+
})
|
|
194
|
+
}, debounceMs),
|
|
195
|
+
epoch,
|
|
196
|
+
}
|
|
197
|
+
updateSubscribers()
|
|
198
|
+
break
|
|
199
|
+
}
|
|
200
|
+
case "onSubmit": {
|
|
201
|
+
fieldErrors.onSubmit = fieldValidators.onSubmit?.(validatorCtx)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const getFieldValue = <K extends RecordKey<T>>(
|
|
207
|
+
name: K
|
|
208
|
+
): InferRecordKeyValue<T, K> => {
|
|
209
|
+
return objGet(state, (name as string).split(".")) as InferRecordKeyValue<
|
|
210
|
+
T,
|
|
211
|
+
K
|
|
212
|
+
>
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const getFieldState = <K extends RecordKey<T>>(
|
|
216
|
+
name: K
|
|
217
|
+
): FormFieldState<T, K> => {
|
|
218
|
+
let errors: any[] = []
|
|
219
|
+
let isValidating = false
|
|
220
|
+
const fieldErrors = formFieldErrors[name] ?? {}
|
|
221
|
+
const asyncMeta = asyncFormFieldValidators[name] ?? {}
|
|
222
|
+
if (asyncMeta.onChangeAsync && asyncMeta.onChangeAsync.timeout !== -1) {
|
|
223
|
+
isValidating = true
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (!isValidating) {
|
|
227
|
+
errors.push(
|
|
228
|
+
...[
|
|
229
|
+
fieldErrors.onChangeAsync,
|
|
230
|
+
fieldErrors.onMount,
|
|
231
|
+
fieldErrors.onChange,
|
|
232
|
+
fieldErrors.onBlur,
|
|
233
|
+
].filter(Boolean)
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
value: getFieldValue(name),
|
|
239
|
+
errors,
|
|
240
|
+
isTouched: !!formFieldsTouched[name],
|
|
241
|
+
isValidating,
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const onFieldChanged = (name: RecordKey<T>) => {
|
|
246
|
+
validateField(name, "onChange")
|
|
247
|
+
if (formFieldValidators[name]?.onChangeAsync) {
|
|
248
|
+
validateField(name, "onChangeAsync")
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (formFieldDependencies[name]) {
|
|
252
|
+
for (const dependentOn of formFieldDependencies[name]) {
|
|
253
|
+
validateField(dependentOn, "onChange")
|
|
254
|
+
if (formFieldValidators[dependentOn]?.onChangeAsync) {
|
|
255
|
+
validateField(dependentOn, "onChangeAsync")
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
formFieldUpdaters.get(name)?.forEach((update) => update())
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const setFieldValue = <K extends RecordKey<T>>(
|
|
264
|
+
name: K,
|
|
265
|
+
value: InferRecordKeyValue<T, K>
|
|
266
|
+
) => {
|
|
267
|
+
objSet(state, (name as string).split("."), value)
|
|
268
|
+
|
|
269
|
+
onFieldChanged(name)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const removeFieldMeta = (name: RecordKey<T>) => {
|
|
273
|
+
for (const metaMap of [
|
|
274
|
+
formFieldErrors,
|
|
275
|
+
asyncFormFieldValidators,
|
|
276
|
+
formFieldsTouched,
|
|
277
|
+
formFieldValidators,
|
|
278
|
+
]) {
|
|
279
|
+
delete metaMap[name]
|
|
280
|
+
for (const key in metaMap) {
|
|
281
|
+
if (key.startsWith(`${name}.`)) {
|
|
282
|
+
delete metaMap[key as RecordKey<T>]
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
formFieldUpdaters.delete(name)
|
|
288
|
+
for (const key in formFieldUpdaters) {
|
|
289
|
+
if (key.startsWith(`${name}.`)) {
|
|
290
|
+
formFieldUpdaters.delete(key as RecordKey<T>)
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const arrayFieldReplace = (name: RecordKey<T>, index: number, value: any) => {
|
|
296
|
+
const path = [...(name as string).split("."), index.toString()]
|
|
297
|
+
objSet(state, path, value)
|
|
298
|
+
|
|
299
|
+
removeFieldMeta(`${name}.${index}` as RecordKey<T>)
|
|
300
|
+
|
|
301
|
+
onFieldChanged(name)
|
|
302
|
+
updateFieldComponents(name)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const arrayFieldPush = (name: RecordKey<T>, value: any) => {
|
|
306
|
+
const path = [...(name as string).split(".")]
|
|
307
|
+
const arr = objGet<any[]>(state, path)
|
|
308
|
+
arr.push(value)
|
|
309
|
+
|
|
310
|
+
onFieldChanged(name)
|
|
311
|
+
updateFieldComponents(name)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const arrayFieldRemove = (name: RecordKey<T>, index: number) => {
|
|
315
|
+
const path = [...(name as string).split(".")]
|
|
316
|
+
const arr = objGet<any[]>(state, path)
|
|
317
|
+
arr.splice(index, 1)
|
|
318
|
+
|
|
319
|
+
removeFieldMeta(`${name}.${index}` as RecordKey<T>)
|
|
320
|
+
|
|
321
|
+
onFieldChanged(name)
|
|
322
|
+
updateFieldComponents(name)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const getErrors = () => {
|
|
326
|
+
const errors: string[] = []
|
|
327
|
+
for (const fieldName in formFieldErrors) {
|
|
328
|
+
const meta = formFieldErrors[fieldName as RecordKey<T>]
|
|
329
|
+
errors.push(
|
|
330
|
+
...[
|
|
331
|
+
meta.onChangeAsync,
|
|
332
|
+
meta.onMount,
|
|
333
|
+
meta.onChange,
|
|
334
|
+
meta.onBlur,
|
|
335
|
+
].filter(Boolean)
|
|
336
|
+
)
|
|
337
|
+
}
|
|
338
|
+
return errors
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const isAnyFieldValidating = () => {
|
|
342
|
+
for (const fieldName in asyncFormFieldValidators) {
|
|
343
|
+
const fieldValidators =
|
|
344
|
+
asyncFormFieldValidators[fieldName as RecordKey<T>]
|
|
345
|
+
if (
|
|
346
|
+
fieldValidators.onChangeAsync &&
|
|
347
|
+
fieldValidators.onChangeAsync.timeout !== -1
|
|
348
|
+
) {
|
|
349
|
+
return true
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return false
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const connectField = (name: RecordKey<T>, update: () => void) => {
|
|
356
|
+
if (!formFieldUpdaters.has(name)) {
|
|
357
|
+
formFieldUpdaters.set(name, new Set())
|
|
358
|
+
}
|
|
359
|
+
formFieldUpdaters.get(name)!.add(update)
|
|
360
|
+
}
|
|
361
|
+
const disconnectField = (name: RecordKey<T>, update: () => void) => {
|
|
362
|
+
if (!formFieldUpdaters.has(name)) return
|
|
363
|
+
formFieldUpdaters.get(name)!.delete(update)
|
|
364
|
+
|
|
365
|
+
const asyncValidators = asyncFormFieldValidators[name] ?? {}
|
|
366
|
+
const { abortController, timeout } = asyncValidators.onChangeAsync ?? {}
|
|
367
|
+
window.clearTimeout(timeout)
|
|
368
|
+
abortController?.abort()
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const reset = (values?: T) => {
|
|
372
|
+
if (values) {
|
|
373
|
+
for (const key in values) {
|
|
374
|
+
state[key] = values[key]
|
|
375
|
+
}
|
|
376
|
+
} else {
|
|
377
|
+
const initialValues = (config.initialValues ?? {}) as T
|
|
378
|
+
const keys = new Set([
|
|
379
|
+
...Object.keys(state),
|
|
380
|
+
...Object.keys(initialValues),
|
|
381
|
+
])
|
|
382
|
+
for (const key of keys) {
|
|
383
|
+
if (key in initialValues) {
|
|
384
|
+
state[key as RecordKey<T>] = structuredClone(
|
|
385
|
+
initialValues[key] as T[RecordKey<T>]
|
|
386
|
+
)
|
|
387
|
+
} else {
|
|
388
|
+
delete state[key]
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
for (const fieldName in formFieldsTouched) {
|
|
393
|
+
delete formFieldsTouched[fieldName as RecordKey<T>]
|
|
394
|
+
}
|
|
395
|
+
for (const fieldName in asyncFormFieldValidators) {
|
|
396
|
+
const asyncFieldValidators =
|
|
397
|
+
asyncFormFieldValidators[fieldName as RecordKey<T>]
|
|
398
|
+
const { timeout, abortController } =
|
|
399
|
+
asyncFieldValidators?.onChangeAsync ?? {}
|
|
400
|
+
if (timeout !== -1) {
|
|
401
|
+
window.clearTimeout(timeout)
|
|
402
|
+
}
|
|
403
|
+
abortController?.abort()
|
|
404
|
+
delete asyncFormFieldValidators[fieldName as RecordKey<T>]
|
|
405
|
+
}
|
|
406
|
+
for (const fieldName in formFieldErrors) {
|
|
407
|
+
delete formFieldErrors[fieldName as RecordKey<T>]
|
|
408
|
+
}
|
|
409
|
+
updateSubscribers()
|
|
410
|
+
formFieldUpdaters.forEach((updaters) => {
|
|
411
|
+
updaters.forEach((update) => update())
|
|
412
|
+
})
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const validateForm = async () => {
|
|
416
|
+
for (const fieldName in formFieldValidators) {
|
|
417
|
+
const fieldValidators = formFieldValidators[fieldName as RecordKey<T>]
|
|
418
|
+
if (fieldValidators?.onChange) {
|
|
419
|
+
await validateField(fieldName as RecordKey<T>, "onChange")
|
|
420
|
+
}
|
|
421
|
+
if (fieldValidators?.onSubmit) {
|
|
422
|
+
await validateField(fieldName as RecordKey<T>, "onSubmit")
|
|
423
|
+
}
|
|
424
|
+
if (
|
|
425
|
+
!formFieldErrors[fieldName as RecordKey<T>]?.onChange &&
|
|
426
|
+
fieldValidators?.onChangeAsync
|
|
427
|
+
) {
|
|
428
|
+
const value = state[fieldName] as T[RecordKey<T>]
|
|
429
|
+
const abortController = new AbortController()
|
|
430
|
+
const asyncValidators = (asyncFormFieldValidators[
|
|
431
|
+
fieldName as RecordKey<T>
|
|
432
|
+
] ??= {})
|
|
433
|
+
const epoch = asyncValidators.onChangeAsync?.epoch ?? 0
|
|
434
|
+
asyncValidators.onChangeAsync = {
|
|
435
|
+
timeout: 0,
|
|
436
|
+
epoch,
|
|
437
|
+
abortController,
|
|
438
|
+
}
|
|
439
|
+
const ctx: AsyncValidatorContext<any> = {
|
|
440
|
+
value,
|
|
441
|
+
abortSignal: abortController.signal,
|
|
442
|
+
}
|
|
443
|
+
formFieldErrors[fieldName as RecordKey<T>].onChangeAsync =
|
|
444
|
+
await fieldValidators.onChangeAsync(ctx)
|
|
445
|
+
asyncValidators.onChangeAsync = {
|
|
446
|
+
timeout: -1,
|
|
447
|
+
epoch,
|
|
448
|
+
abortController: null,
|
|
449
|
+
}
|
|
450
|
+
updateFieldComponents(fieldName as RecordKey<T>)
|
|
451
|
+
updateSubscribers()
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return getErrors()
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const deleteField = (name: RecordKey<T>) => {
|
|
458
|
+
delete state[name]
|
|
459
|
+
delete formFieldErrors[name]
|
|
460
|
+
delete asyncFormFieldValidators[name]
|
|
461
|
+
delete formFieldsTouched[name]
|
|
462
|
+
delete formFieldValidators[name]
|
|
463
|
+
formFieldUpdaters.delete(name)
|
|
464
|
+
|
|
465
|
+
updateSubscribers()
|
|
466
|
+
}
|
|
467
|
+
const resetField = (name: RecordKey<T>) => {
|
|
468
|
+
if (config.initialValues?.[name]) {
|
|
469
|
+
state[name] = config.initialValues[name]
|
|
470
|
+
} else {
|
|
471
|
+
delete state[name]
|
|
472
|
+
}
|
|
473
|
+
delete formFieldErrors[name]
|
|
474
|
+
delete asyncFormFieldValidators[name]
|
|
475
|
+
delete formFieldsTouched[name]
|
|
476
|
+
|
|
477
|
+
updateSubscribers()
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const getFormContext = (): FormContext<T> => {
|
|
481
|
+
return {
|
|
482
|
+
deleteField,
|
|
483
|
+
resetField,
|
|
484
|
+
setFieldValue,
|
|
485
|
+
validateForm,
|
|
486
|
+
state,
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const setFieldValidators = <K extends RecordKey<T>>(
|
|
491
|
+
name: K,
|
|
492
|
+
validators: Partial<
|
|
493
|
+
FormFieldValidators<RecordKey<T>, InferRecordKeyValue<T, K>>
|
|
494
|
+
>
|
|
495
|
+
) => {
|
|
496
|
+
formFieldValidators[name] = validators
|
|
497
|
+
if (validators.dependentOn && validators.dependentOn.length > 0) {
|
|
498
|
+
for (const dependentOn of validators.dependentOn) {
|
|
499
|
+
if (!formFieldDependencies[dependentOn]) {
|
|
500
|
+
formFieldDependencies[dependentOn] = new Set()
|
|
501
|
+
}
|
|
502
|
+
formFieldDependencies[dependentOn].add(name)
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return {
|
|
508
|
+
subscribers,
|
|
509
|
+
state,
|
|
510
|
+
validateField,
|
|
511
|
+
getFieldState,
|
|
512
|
+
setFieldValue,
|
|
513
|
+
arrayFieldReplace,
|
|
514
|
+
arrayFieldPush,
|
|
515
|
+
arrayFieldRemove,
|
|
516
|
+
connectField,
|
|
517
|
+
disconnectField,
|
|
518
|
+
setFieldValidators,
|
|
519
|
+
getSelectorState,
|
|
520
|
+
validateForm,
|
|
521
|
+
reset,
|
|
522
|
+
getFormContext,
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
export function useForm<T extends Record<string, unknown> = {}>(
|
|
527
|
+
config: UseFormConfig<T>
|
|
528
|
+
): UseFormState<T> {
|
|
529
|
+
return useHook(
|
|
530
|
+
"useForm",
|
|
531
|
+
{} as UseFormInternalState<T>,
|
|
532
|
+
({ hook, isInit, isHMR }) => {
|
|
533
|
+
if (__DEV__) {
|
|
534
|
+
if (isInit) {
|
|
535
|
+
hook.dev = {
|
|
536
|
+
initialArgs: [config],
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
if (isHMR) {
|
|
540
|
+
const [c] = hook.dev!.initialArgs
|
|
541
|
+
if (safeStringify(c) !== safeStringify(config)) {
|
|
542
|
+
hook.cleanup?.()
|
|
543
|
+
isInit = true
|
|
544
|
+
hook.dev!.initialArgs = [config]
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
if (isInit) {
|
|
549
|
+
const $controller = (hook.formController = createFormController(config))
|
|
550
|
+
|
|
551
|
+
hook.Field = function Field<
|
|
552
|
+
Name extends RecordKey<T>,
|
|
553
|
+
Validators extends FormFieldValidators<
|
|
554
|
+
RecordKey<T>,
|
|
555
|
+
InferRecordKeyValue<T, Name>
|
|
556
|
+
>,
|
|
557
|
+
IsArray extends boolean
|
|
558
|
+
>(props: FormFieldProps<T, Name, Validators, IsArray>) {
|
|
559
|
+
const didMount = useRef(false)
|
|
560
|
+
const update = useRequestUpdate()
|
|
561
|
+
if (props.validators) {
|
|
562
|
+
$controller.setFieldValidators(props.name, props.validators)
|
|
563
|
+
}
|
|
564
|
+
useEffect(() => {
|
|
565
|
+
$controller.connectField(props.name, update)
|
|
566
|
+
if (props.validators?.onMount && !didMount.current) {
|
|
567
|
+
didMount.current = true
|
|
568
|
+
$controller.validateField(props.name, "onMount")
|
|
569
|
+
}
|
|
570
|
+
return () => {
|
|
571
|
+
$controller.disconnectField(props.name, update)
|
|
572
|
+
}
|
|
573
|
+
}, [])
|
|
574
|
+
|
|
575
|
+
const fieldState = $controller.getFieldState(props.name)
|
|
576
|
+
|
|
577
|
+
const childProps: FormFieldContext<T, Name, Validators, false> = {
|
|
578
|
+
name: props.name,
|
|
579
|
+
state: fieldState as FormFieldContext<
|
|
580
|
+
T,
|
|
581
|
+
Name,
|
|
582
|
+
Validators,
|
|
583
|
+
false
|
|
584
|
+
>["state"],
|
|
585
|
+
handleChange: (value: InferRecordKeyValue<T, Name>) => {
|
|
586
|
+
$controller.setFieldValue(props.name, value)
|
|
587
|
+
},
|
|
588
|
+
handleBlur: () => {
|
|
589
|
+
$controller.validateField(props.name, "onBlur")
|
|
590
|
+
},
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (props.array) {
|
|
594
|
+
const asArrayProps = childProps as FormFieldContext<
|
|
595
|
+
T,
|
|
596
|
+
Name,
|
|
597
|
+
Validators,
|
|
598
|
+
true
|
|
599
|
+
>
|
|
600
|
+
asArrayProps.items = {
|
|
601
|
+
replace: (index: number, value: any) => {
|
|
602
|
+
$controller.arrayFieldReplace(props.name, index, value)
|
|
603
|
+
},
|
|
604
|
+
push: (value: any) => {
|
|
605
|
+
$controller.arrayFieldPush(props.name, value)
|
|
606
|
+
},
|
|
607
|
+
remove: (index: number) => {
|
|
608
|
+
$controller.arrayFieldRemove(props.name, index)
|
|
609
|
+
},
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
return Fragment({
|
|
613
|
+
key: useMemo(generateRandomID, []),
|
|
614
|
+
children: props.children(
|
|
615
|
+
childProps as FormFieldContext<T, Name, Validators, IsArray>
|
|
616
|
+
),
|
|
617
|
+
})
|
|
618
|
+
}
|
|
619
|
+
hook.Subscribe = function Subscribe<
|
|
620
|
+
Selector extends (state: SelectorState<T>) => unknown
|
|
621
|
+
>(props: FormSubscribeProps<T, Selector, ReturnType<Selector>>) {
|
|
622
|
+
const selection = useHook(
|
|
623
|
+
"useFormSubscription",
|
|
624
|
+
{ sub: null! as FormStateSubscriber<T> },
|
|
625
|
+
({ hook, isInit, isHMR, update }) => {
|
|
626
|
+
if (__DEV__) {
|
|
627
|
+
if (isHMR) {
|
|
628
|
+
isInit = true
|
|
629
|
+
hook.cleanup?.()
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
if (isInit) {
|
|
633
|
+
hook.sub = {
|
|
634
|
+
selector: props.selector,
|
|
635
|
+
selection: props.selector($controller.getSelectorState()),
|
|
636
|
+
update,
|
|
637
|
+
}
|
|
638
|
+
$controller.subscribers.add(hook.sub)
|
|
639
|
+
hook.cleanup = () => $controller.subscribers.delete(hook.sub)
|
|
640
|
+
}
|
|
641
|
+
return hook.sub.selection
|
|
642
|
+
}
|
|
643
|
+
) as ReturnType<Selector>
|
|
644
|
+
return props.children(selection)
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
return {
|
|
648
|
+
Field: hook.Field,
|
|
649
|
+
Subscribe: hook.Subscribe,
|
|
650
|
+
handleSubmit: async () => {
|
|
651
|
+
const errors = await hook.formController.validateForm()
|
|
652
|
+
const formCtx = hook.formController.getFormContext()
|
|
653
|
+
if (errors.length) return config.onSubmitInvalid?.(formCtx)
|
|
654
|
+
await config.onSubmit?.(formCtx)
|
|
655
|
+
},
|
|
656
|
+
reset: (values?: T) => hook.formController.reset(values),
|
|
657
|
+
getFieldState: <K extends RecordKey<T>>(name: K) =>
|
|
658
|
+
hook.formController.getFieldState(name),
|
|
659
|
+
} satisfies UseFormState<T>
|
|
660
|
+
}
|
|
661
|
+
)
|
|
662
|
+
}
|