@vobs/forms 0.1.0 → 1.0.0

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/src/form.ts ADDED
@@ -0,0 +1,673 @@
1
+ import { createOwner, state, type Signal } from '@vobs/reactivity'
2
+ import type { VobsNode } from '@vobs/vobs'
3
+ import { Field } from './field'
4
+
5
+ export type FormFieldName<T extends object> = Extract<keyof T, string>
6
+ export type ValidationTrigger = 'input' | 'blur' | 'submit' | 'manual'
7
+ export type ValidationResult = string | null | undefined | void
8
+ export type ValidationOutput = string | null | Promise<string | null>
9
+
10
+ export type Validator<TValue, TValues extends object> = (
11
+ value: TValue,
12
+ values: Readonly<TValues>,
13
+ signal?: AbortSignal
14
+ ) => ValidationResult | PromiseLike<ValidationResult>
15
+
16
+ export type ValidatorMap<T extends object> = Partial<{
17
+ [K in FormFieldName<T>]: Validator<T[K], T> | readonly Validator<T[K], T>[]
18
+ }>
19
+
20
+ export type FormErrorName<T extends object> = FormFieldName<T> | '__form'
21
+ export type FormErrors<T extends object> = Partial<Record<FormErrorName<T>, string>>
22
+
23
+ export interface SchemaAdapter<T extends object> {
24
+ validate(values: Readonly<T>): FormErrors<T> | PromiseLike<FormErrors<T>>
25
+ }
26
+
27
+ export type SubmitHandler<T extends object, TResult = unknown> = (
28
+ values: Readonly<T>
29
+ ) => TResult | PromiseLike<TResult>
30
+
31
+ export interface FormOptions<T extends object> {
32
+ validators?: ValidatorMap<T>
33
+ schema?: SchemaAdapter<T>
34
+ validateOn?: ValidationTrigger
35
+ validateDebounce?: number | Partial<Record<FormFieldName<T>, number>>
36
+ onSubmit?: SubmitHandler<T>
37
+ }
38
+
39
+ export interface DynamicFieldOptions<T extends object, TValue = unknown> {
40
+ validators?: Validator<TValue, T> | readonly Validator<TValue, T>[]
41
+ validateDebounce?: number
42
+ }
43
+
44
+ export interface FormField<T> {
45
+ readonly name: string
46
+ readonly value: Signal<T>
47
+ readonly touched: Signal<boolean>
48
+ readonly dirty: Signal<boolean>
49
+ readonly error: Signal<string | null>
50
+ readonly validating: Signal<boolean>
51
+ set(value: T): void
52
+ markTouched(): ValidationOutput
53
+ validate(): ValidationOutput
54
+ reset(value?: T): void
55
+ }
56
+
57
+ export interface SubmitSuccess<T extends object, TResult = unknown> {
58
+ readonly valid: true
59
+ readonly values: Readonly<T>
60
+ readonly result: TResult | undefined
61
+ }
62
+
63
+ export interface SubmitFailure<T extends object> {
64
+ readonly valid: false
65
+ readonly errors: FormErrors<T>
66
+ }
67
+
68
+ export type SubmitResult<T extends object, TResult = unknown> =
69
+ | SubmitSuccess<T, TResult>
70
+ | SubmitFailure<T>
71
+
72
+ export interface Form<T extends object> {
73
+ readonly values: Readonly<T>
74
+ readonly dirty: Signal<boolean>
75
+ readonly touched: Signal<ReadonlySet<FormFieldName<T>>>
76
+ readonly errors: Signal<Readonly<FormErrors<T>>>
77
+ readonly hasErrors: Signal<boolean>
78
+ readonly submitting: Signal<boolean>
79
+ readonly validating: Signal<boolean>
80
+ readonly validatingFields: Signal<ReadonlySet<FormFieldName<T>>>
81
+ readonly fieldNames: Signal<ReadonlySet<string>>
82
+ readonly Field: FormFieldComponent<T>
83
+ field<K extends FormFieldName<T>>(name: K): FormField<T[K]>
84
+ addField<TValue>(name: string, initialValue: TValue, options?: DynamicFieldOptions<T, TValue>): FormField<TValue>
85
+ removeField(name: string): boolean
86
+ validateField<K extends FormFieldName<T>>(name: K): ValidationOutput
87
+ validateAll(): FormErrors<T> | Promise<FormErrors<T>>
88
+ setServerErrors(errors: Partial<Record<FormErrorName<T>, string | null | undefined>>): void
89
+ clearErrors(name?: FormErrorName<T>): void
90
+ getErrorFields(): FormFieldName<T>[]
91
+ submit<TResult = unknown>(handler?: SubmitHandler<T, TResult>): Promise<SubmitResult<T, TResult>>
92
+ reset(values?: Partial<T>): void
93
+ dispose(): void
94
+ }
95
+
96
+ export interface FormFieldComponent<T extends object> {
97
+ (props: FormFieldProps<T>): VobsNode
98
+ }
99
+
100
+ export interface FormFieldProps<T extends object> {
101
+ form: Form<T>
102
+ name: string
103
+ label?: string
104
+ children?: (field: FormField<unknown>) => VobsNode
105
+ }
106
+
107
+ interface InternalField<TValue, TValues extends object> {
108
+ readonly name: string
109
+ readonly value: Signal<TValue>
110
+ readonly touched: Signal<boolean>
111
+ readonly dirty: Signal<boolean>
112
+ readonly error: Signal<string | null>
113
+ readonly validating: Signal<boolean>
114
+ initialValue: TValue
115
+ validationRun: number
116
+ validators?: readonly Validator<unknown, TValues>[]
117
+ validateDebounce?: number
118
+ controller?: AbortController
119
+ debounceTimer?: ReturnType<typeof setTimeout>
120
+ debounceCancel?: () => void
121
+ }
122
+
123
+ const FORM_DISPOSED = 'Vobs forms: 表单已销毁'
124
+
125
+ export function useForm<T extends object>(initialValues: T, options: FormOptions<T> = {}): Form<T> {
126
+ return createForm(initialValues, options)
127
+ }
128
+
129
+ export function createForm<T extends object>(initialValues: T, options: FormOptions<T> = {}): Form<T> {
130
+ const owner = createOwner()
131
+ const names = Object.keys(initialValues)
132
+ const fields = new Map<string, InternalField<unknown, T>>()
133
+ const touchedNames = new Set<FormFieldName<T>>()
134
+ const validatingNames = new Set<FormFieldName<T>>()
135
+ const dirty = owner.run(() => state(false))
136
+ const touched = owner.run(() => state<ReadonlySet<FormFieldName<T>>>(new Set()))
137
+ const errors = owner.run(() => state<Readonly<FormErrors<T>>>({} as FormErrors<T>))
138
+ const hasErrors = owner.run(() => state(false))
139
+ const submitting = owner.run(() => state(false))
140
+ const validating = owner.run(() => state(false))
141
+ const validatingFields = owner.run(() => state<ReadonlySet<FormFieldName<T>>>(new Set()))
142
+ const fieldNames = owner.run(() => state<ReadonlySet<string>>(new Set(names)))
143
+ let schemaRun = 0
144
+ let schemaPending = 0
145
+ let formError: string | null = null
146
+ let disposed = false
147
+ let submitPromise: Promise<SubmitResult<T, unknown>> | null = null
148
+
149
+ owner.run(() => {
150
+ for (const name of names) {
151
+ const initialValue = initialValues[name as FormFieldName<T>]
152
+ fields.set(name, {
153
+ name,
154
+ value: state(initialValue),
155
+ touched: state(false),
156
+ dirty: state(false),
157
+ error: state<string | null>(null),
158
+ validating: state(false),
159
+ initialValue,
160
+ validationRun: 0
161
+ })
162
+ }
163
+ })
164
+
165
+ const values = createValuesProxy<T>(fields)
166
+
167
+ function assertActive(): void {
168
+ if (disposed) throw new Error(FORM_DISPOSED)
169
+ }
170
+
171
+ function getInternalField(name: string): InternalField<unknown, T> {
172
+ const field = fields.get(name)
173
+ if (!field) throw new Error(`Vobs forms: 未定义字段 "${name}"`)
174
+ return field
175
+ }
176
+
177
+ function updateAggregateErrors(): void {
178
+ const next: FormErrors<T> = {}
179
+ if (formError) next.__form = formError
180
+ for (const name of names) {
181
+ const error = fields.get(name)!.error.value
182
+ if (error) next[name as FormErrorName<T>] = error
183
+ }
184
+ errors.value = next
185
+ hasErrors.value = Object.keys(next).length > 0
186
+ }
187
+
188
+ function setFieldError(name: string, message: string | null): void {
189
+ const field = getInternalField(name)
190
+ field.error.value = message
191
+ updateAggregateErrors()
192
+ }
193
+
194
+ function setFormError(message: string | null): void {
195
+ formError = message
196
+ updateAggregateErrors()
197
+ }
198
+
199
+ function updateDirty(): void {
200
+ dirty.value = names.some(name => fields.get(name)!.dirty.value)
201
+ }
202
+
203
+ function updateValidating(): void {
204
+ validating.value = validatingNames.size > 0 || schemaPending > 0
205
+ }
206
+
207
+ function setFieldValidating(name: string, active: boolean): void {
208
+ const field = getInternalField(name)
209
+ field.validating.value = active
210
+ const typedName = name as FormFieldName<T>
211
+ if (active) validatingNames.add(typedName)
212
+ else validatingNames.delete(typedName)
213
+ validatingFields.value = new Set(validatingNames)
214
+ updateValidating()
215
+ }
216
+
217
+ function normalizeMessage(value: unknown): string | null {
218
+ if (value === undefined || value === null || value === '' || value === false) return null
219
+ if (value instanceof Error) return value.message || '校验失败'
220
+ return String(value)
221
+ }
222
+
223
+ function validatorList(name: string): readonly Validator<unknown, T>[] {
224
+ const field = fields.get(name)
225
+ if (field?.validators) return field.validators
226
+ const configured = options.validators?.[name as FormFieldName<T>] as
227
+ | Validator<unknown, T>
228
+ | readonly Validator<unknown, T>[]
229
+ | undefined
230
+ if (!configured) return []
231
+ return (Array.isArray(configured) ? configured : [configured]) as readonly Validator<unknown, T>[]
232
+ }
233
+
234
+ function runValidators(name: string, value: unknown, snapshot: Readonly<T>, signal: AbortSignal): ValidationOutput {
235
+ const validators = validatorList(name)
236
+ let index = 0
237
+
238
+ const next = (): ValidationOutput => {
239
+ while (index < validators.length) {
240
+ const validator = validators[index++]
241
+ let result: ValidationResult | PromiseLike<ValidationResult>
242
+ try {
243
+ result = validator(value, snapshot, signal)
244
+ } catch (error) {
245
+ if (signal.aborted) return null
246
+ return normalizeMessage(error) ?? '校验失败'
247
+ }
248
+ if (isPromiseLike(result)) {
249
+ return Promise.resolve(result).then(
250
+ message => normalizeMessage(message) ?? next(),
251
+ error => signal.aborted ? null : normalizeMessage(error) ?? '校验失败'
252
+ )
253
+ }
254
+ const message = normalizeMessage(result)
255
+ if (message) return message
256
+ }
257
+ return null
258
+ }
259
+
260
+ return next()
261
+ }
262
+
263
+ function runSchema(snapshot: Readonly<T>): FormErrors<T> | Promise<FormErrors<T>> {
264
+ if (!options.schema) return {}
265
+ try {
266
+ const result = options.schema.validate(snapshot)
267
+ if (isPromiseLike(result)) {
268
+ return Promise.resolve(result).then(normalizeErrors, error => ({
269
+ __form: normalizeMessage(error) ?? '校验失败'
270
+ } as FormErrors<T>))
271
+ }
272
+ return normalizeErrors(result)
273
+ } catch (error) {
274
+ return { __form: normalizeMessage(error) ?? '校验失败' } as FormErrors<T>
275
+ }
276
+ }
277
+
278
+ function normalizeErrors(value: unknown): FormErrors<T> {
279
+ if (!value || typeof value !== 'object') return {}
280
+ const result: FormErrors<T> = {}
281
+ for (const [name, message] of Object.entries(value)) {
282
+ const normalized = normalizeMessage(message)
283
+ if (normalized) result[name as FormFieldName<T>] = normalized
284
+ }
285
+ return result
286
+ }
287
+
288
+ function applyValidation(name: string, run: number, result: unknown): string | null {
289
+ const field = fields.get(name)
290
+ const message = normalizeMessage(result)
291
+ if (!disposed && field && field.validationRun === run) setFieldError(name, message)
292
+ return message
293
+ }
294
+
295
+ function debounceFor(name: string, field: InternalField<unknown, T>): number {
296
+ if (field.validateDebounce !== undefined) return field.validateDebounce
297
+ if (typeof options.validateDebounce === 'number') return options.validateDebounce
298
+ return options.validateDebounce?.[name as FormFieldName<T>] ?? 0
299
+ }
300
+
301
+ function cancelFieldValidation(field: InternalField<unknown, T>): void {
302
+ field.validationRun++
303
+ field.controller?.abort()
304
+ field.controller = undefined
305
+ if (field.debounceCancel) field.debounceCancel()
306
+ if (field.debounceTimer !== undefined) clearTimeout(field.debounceTimer)
307
+ field.debounceTimer = undefined
308
+ field.debounceCancel = undefined
309
+ }
310
+
311
+ function beginFieldValidation(field: InternalField<unknown, T>): AbortController {
312
+ field.controller?.abort()
313
+ if (field.debounceCancel) field.debounceCancel()
314
+ if (field.debounceTimer !== undefined) clearTimeout(field.debounceTimer)
315
+ field.debounceCancel = undefined
316
+ const controller = new AbortController()
317
+ field.controller = controller
318
+ return controller
319
+ }
320
+
321
+ function validateFieldInternal(name: string, includeSchema: boolean): ValidationOutput {
322
+ assertActive()
323
+ const field = getInternalField(name)
324
+ const run = ++field.validationRun
325
+ const controller = beginFieldValidation(field)
326
+ const snapshot = snapshotValues<T>(fields)
327
+ const execute = (): ValidationOutput => runValidators(name, field.value.value, snapshot, controller.signal)
328
+ const delay = debounceFor(name, field)
329
+ const fieldResult = delay > 0
330
+ ? new Promise<ValidationResult>(resolve => {
331
+ field.debounceCancel = () => resolve(null)
332
+ field.debounceTimer = setTimeout(() => {
333
+ field.debounceTimer = undefined
334
+ field.debounceCancel = undefined
335
+ resolve(execute())
336
+ }, delay)
337
+ })
338
+ : execute()
339
+
340
+ const finish = (message: unknown): ValidationOutput => {
341
+ if (!includeSchema || !options.schema) return applyValidation(name, run, message)
342
+ const schemaResult = runSchema(snapshot)
343
+ if (isPromiseLike(schemaResult)) {
344
+ schemaPending++
345
+ updateValidating()
346
+ return Promise.resolve(schemaResult).then(schemaErrors => {
347
+ const schemaMessage = schemaErrors[name as FormFieldName<T>]
348
+ setFormError(schemaErrors.__form ?? null)
349
+ return applyValidation(name, run, schemaMessage ?? message)
350
+ }).finally(() => {
351
+ schemaPending--
352
+ updateValidating()
353
+ }) as Promise<string | null>
354
+ }
355
+ setFormError(schemaResult.__form ?? null)
356
+ return applyValidation(name, run, schemaResult[name as FormFieldName<T>] ?? message)
357
+ }
358
+
359
+ if (isPromiseLike(fieldResult)) {
360
+ setFieldValidating(name, true)
361
+ return Promise.resolve(fieldResult).then(finish).finally(() => {
362
+ if (!disposed && field.validationRun === run) {
363
+ field.controller = undefined
364
+ field.debounceCancel = undefined
365
+ setFieldValidating(name, false)
366
+ }
367
+ }) as Promise<string | null>
368
+ }
369
+ field.controller = undefined
370
+ field.debounceCancel = undefined
371
+ if (!disposed && field.validationRun === run) setFieldValidating(name, false)
372
+ return finish(fieldResult)
373
+ }
374
+
375
+ function validateField(name: FormFieldName<T>): ValidationOutput {
376
+ return validateFieldInternal(name, true)
377
+ }
378
+
379
+ function applyAllErrors(fieldErrors: FormErrors<T>, schemaErrors: FormErrors<T>): FormErrors<T> {
380
+ const result: FormErrors<T> = {}
381
+ setFormError(schemaErrors.__form ?? null)
382
+ if (schemaErrors.__form) result.__form = schemaErrors.__form
383
+ for (const name of names) {
384
+ const errorName = name as FormErrorName<T>
385
+ const message = schemaErrors[errorName] ?? fieldErrors[errorName] ?? null
386
+ setFieldError(name, message)
387
+ if (message) result[errorName] = message
388
+ }
389
+ return result
390
+ }
391
+
392
+ function validateAll(): FormErrors<T> | Promise<FormErrors<T>> {
393
+ assertActive()
394
+ for (const name of names) {
395
+ const field = fields.get(name)!
396
+ field.touched.value = true
397
+ touchedNames.add(name as FormFieldName<T>)
398
+ }
399
+ touched.value = new Set(touchedNames)
400
+
401
+ const currentSchemaRun = ++schemaRun
402
+ const fieldResults = names.map(name => validateFieldInternal(name, false))
403
+ const schemaResult = options.schema ? runSchema(snapshotValues<T>(fields)) : {}
404
+ const combine = (fieldMessages: readonly (string | null)[], schemaErrors: FormErrors<T>): FormErrors<T> => {
405
+ if (currentSchemaRun !== schemaRun) return {}
406
+ const fieldErrors: FormErrors<T> = {}
407
+ names.forEach((name, index) => {
408
+ if (fieldMessages[index]) fieldErrors[name as FormErrorName<T>] = fieldMessages[index]!
409
+ })
410
+ return applyAllErrors(fieldErrors, schemaErrors)
411
+ }
412
+
413
+ if (fieldResults.some(isPromiseLike) || isPromiseLike(schemaResult)) {
414
+ if (isPromiseLike(schemaResult)) {
415
+ schemaPending++
416
+ updateValidating()
417
+ }
418
+ return Promise.all(fieldResults.map(result => Promise.resolve(result))).then(fieldMessages => {
419
+ if (isPromiseLike(schemaResult)) {
420
+ return Promise.resolve(schemaResult).then(schemaErrors => combine(fieldMessages, schemaErrors as FormErrors<T>))
421
+ }
422
+ return combine(fieldMessages, schemaResult)
423
+ }).finally(() => {
424
+ if (isPromiseLike(schemaResult)) {
425
+ schemaPending--
426
+ updateValidating()
427
+ }
428
+ }).then(result => result as FormErrors<T>)
429
+ }
430
+
431
+ return combine(fieldResults as string[], schemaResult as FormErrors<T>)
432
+ }
433
+
434
+ function fieldApi<K extends FormFieldName<T>>(name: K): FormField<T[K]> {
435
+ const field = getInternalField(name)
436
+ return {
437
+ name,
438
+ value: field.value as Signal<T[K]>,
439
+ touched: field.touched,
440
+ dirty: field.dirty,
441
+ error: field.error,
442
+ validating: field.validating,
443
+ set(value: T[K]): void {
444
+ assertActive()
445
+ const nextValue = value
446
+ field.value.value = nextValue
447
+ field.dirty.value = !Object.is(nextValue, field.initialValue)
448
+ field.error.value = null
449
+ updateAggregateErrors()
450
+ updateDirty()
451
+ if (options.validateOn === 'input') void settle(validateField(name))
452
+ },
453
+ markTouched(): ValidationOutput {
454
+ assertActive()
455
+ field.touched.value = true
456
+ touchedNames.add(name)
457
+ touched.value = new Set(touchedNames)
458
+ return options.validateOn === 'blur' ? validateField(name) : null
459
+ },
460
+ validate(): ValidationOutput {
461
+ return validateField(name)
462
+ },
463
+ reset(value?: T[K]): void {
464
+ assertActive()
465
+ cancelFieldValidation(field)
466
+ setFieldValidating(name, false)
467
+ const nextValue = arguments.length === 0 ? field.initialValue : value as T[K]
468
+ field.initialValue = nextValue
469
+ field.value.value = nextValue
470
+ field.dirty.value = false
471
+ field.touched.value = false
472
+ touchedNames.delete(name)
473
+ field.error.value = null
474
+ touched.value = new Set(touchedNames)
475
+ updateAggregateErrors()
476
+ updateDirty()
477
+ }
478
+ }
479
+ }
480
+
481
+ function addField<TValue>(name: string, initialValue: TValue, fieldOptions: DynamicFieldOptions<T, TValue> = {}): FormField<TValue> {
482
+ assertActive()
483
+ if (!name) throw new Error('Vobs forms: 字段名不能为空')
484
+ if (fields.has(name)) throw new Error(`Vobs forms: 字段 "${name}" 已存在`)
485
+ const validators = fieldOptions.validators
486
+ ? (Array.isArray(fieldOptions.validators) ? fieldOptions.validators : [fieldOptions.validators]) as readonly Validator<unknown, T>[]
487
+ : undefined
488
+ fields.set(name, {
489
+ name,
490
+ value: state(initialValue),
491
+ touched: state(false),
492
+ dirty: state(false),
493
+ error: state<string | null>(null),
494
+ validating: state(false),
495
+ initialValue,
496
+ validationRun: 0,
497
+ validators,
498
+ validateDebounce: fieldOptions.validateDebounce
499
+ })
500
+ names.push(name)
501
+ fieldNames.value = new Set(names)
502
+ return fieldApi(name as FormFieldName<T>) as unknown as FormField<TValue>
503
+ }
504
+
505
+ function removeField(name: string): boolean {
506
+ assertActive()
507
+ const field = fields.get(name)
508
+ if (!field) return false
509
+ cancelFieldValidation(field)
510
+ setFieldValidating(name, false)
511
+ fields.delete(name)
512
+ const index = names.indexOf(name)
513
+ if (index >= 0) names.splice(index, 1)
514
+ touchedNames.delete(name as FormFieldName<T>)
515
+ fieldNames.value = new Set(names)
516
+ touched.value = new Set(touchedNames)
517
+ updateAggregateErrors()
518
+ updateDirty()
519
+ return true
520
+ }
521
+
522
+ function setServerErrors(serverErrors: Partial<Record<FormErrorName<T>, string | null | undefined>>): void {
523
+ assertActive()
524
+ for (const [name, message] of Object.entries(serverErrors)) {
525
+ if (name === '__form') {
526
+ setFormError(normalizeMessage(message))
527
+ continue
528
+ }
529
+ if (!fields.has(name)) continue
530
+ setFieldError(name, normalizeMessage(message))
531
+ }
532
+ }
533
+
534
+ function clearErrors(name?: FormErrorName<T>): void {
535
+ assertActive()
536
+ if (name === '__form') {
537
+ setFormError(null)
538
+ return
539
+ }
540
+ const targets = (name ? [name] : names) as string[]
541
+ for (const target of targets) {
542
+ const field = getInternalField(target)
543
+ cancelFieldValidation(field)
544
+ setFieldValidating(target, false)
545
+ field.error.value = null
546
+ }
547
+ if (!name) setFormError(null)
548
+ updateAggregateErrors()
549
+ }
550
+
551
+ function getErrorFields(): FormFieldName<T>[] {
552
+ return names.filter(name => Boolean(fields.get(name)!.error.value)) as FormFieldName<T>[]
553
+ }
554
+
555
+ function reset(nextValues: Partial<T> = {}): void {
556
+ assertActive()
557
+ schemaRun++
558
+ setFormError(null)
559
+ for (const name of names) {
560
+ const field = getInternalField(name)
561
+ cancelFieldValidation(field)
562
+ setFieldValidating(name, false)
563
+ const nextValue = Object.prototype.hasOwnProperty.call(nextValues, name)
564
+ ? nextValues[name as FormFieldName<T>]
565
+ : field.initialValue
566
+ field.initialValue = nextValue as unknown
567
+ field.value.value = nextValue
568
+ field.dirty.value = false
569
+ field.touched.value = false
570
+ field.error.value = null
571
+ }
572
+ touchedNames.clear()
573
+ touched.value = new Set()
574
+ updateAggregateErrors()
575
+ updateDirty()
576
+ }
577
+
578
+ function submit<TResult = unknown>(handler?: SubmitHandler<T, TResult>): Promise<SubmitResult<T, TResult>> {
579
+ assertActive()
580
+ if (submitPromise) return submitPromise as Promise<SubmitResult<T, TResult>>
581
+ const submitHandler = handler ?? options.onSubmit as SubmitHandler<T, TResult> | undefined
582
+ const run = async (): Promise<SubmitResult<T, TResult>> => {
583
+ submitting.value = true
584
+ try {
585
+ const validationErrors = await validateAll()
586
+ if (Object.keys(validationErrors).length > 0) {
587
+ return { valid: false, errors: validationErrors }
588
+ }
589
+ const snapshot = snapshotValues<T>(fields)
590
+ const result = submitHandler ? await submitHandler(snapshot) : undefined
591
+ return { valid: true, values: snapshot, result }
592
+ } finally {
593
+ submitting.value = false
594
+ }
595
+ }
596
+ const pending = run().finally(() => { submitPromise = null })
597
+ submitPromise = pending as Promise<SubmitResult<T, unknown>>
598
+ return pending
599
+ }
600
+
601
+ owner.onDispose(() => {
602
+ for (const field of fields.values()) cancelFieldValidation(field)
603
+ disposed = true
604
+ fields.clear()
605
+ touchedNames.clear()
606
+ validatingNames.clear()
607
+ })
608
+
609
+ const api = {
610
+ values,
611
+ dirty,
612
+ touched,
613
+ errors,
614
+ hasErrors,
615
+ submitting,
616
+ validating,
617
+ validatingFields,
618
+ fieldNames,
619
+ Field: (props: Omit<FormFieldProps<T>, 'form'>) => Field({ ...props, form: api }),
620
+ field: fieldApi,
621
+ addField,
622
+ removeField,
623
+ validateField,
624
+ validateAll,
625
+ setServerErrors,
626
+ clearErrors,
627
+ getErrorFields,
628
+ submit,
629
+ reset,
630
+ dispose(): void {
631
+ if (!disposed) owner.dispose()
632
+ }
633
+ } as Form<T>
634
+
635
+ return api
636
+ }
637
+
638
+ function snapshotValues<T extends object>(fields: Map<string, InternalField<unknown, T>>): T {
639
+ const snapshot: Record<string, unknown> = {}
640
+ for (const [name, field] of fields) snapshot[name] = field.value.value
641
+ return snapshot as T
642
+ }
643
+
644
+ function createValuesProxy<T extends object>(fields: Map<string, InternalField<unknown, T>>): Readonly<T> {
645
+ return new Proxy({} as T, {
646
+ get(_target, property: string | symbol): unknown {
647
+ if (typeof property !== 'string') return undefined
648
+ return fields.get(property)?.value.value
649
+ },
650
+ set(): boolean {
651
+ throw new Error('Vobs forms: values 不能直接赋值,请使用 field(name).set(value)')
652
+ },
653
+ has(_target, property: string | symbol): boolean {
654
+ return typeof property === 'string' && fields.has(property)
655
+ },
656
+ ownKeys(): string[] {
657
+ return [...fields.keys()]
658
+ },
659
+ getOwnPropertyDescriptor(_target, property: string | symbol): PropertyDescriptor | undefined {
660
+ if (typeof property !== 'string' || !fields.has(property)) return undefined
661
+ return { enumerable: true, configurable: true }
662
+ }
663
+ })
664
+ }
665
+
666
+ function isPromiseLike<T>(value: unknown): value is PromiseLike<T> {
667
+ return Boolean(value) && (typeof value === 'object' || typeof value === 'function')
668
+ && typeof (value as PromiseLike<T>).then === 'function'
669
+ }
670
+
671
+ function settle(value: ValidationOutput): Promise<void> {
672
+ return isPromiseLike(value) ? Promise.resolve(value).then(() => undefined) : Promise.resolve()
673
+ }
package/src/index.ts ADDED
@@ -0,0 +1,29 @@
1
+ export {
2
+ createForm,
3
+ useForm
4
+ } from './form'
5
+ export { Field } from './field'
6
+ export type {
7
+ DynamicFieldOptions,
8
+ Form,
9
+ FormErrorName,
10
+ FormErrors,
11
+ FormField,
12
+ FormFieldComponent,
13
+ FormFieldProps,
14
+ FormFieldName,
15
+ FormOptions,
16
+ SchemaAdapter,
17
+ SubmitFailure,
18
+ SubmitHandler,
19
+ SubmitResult,
20
+ SubmitSuccess,
21
+ ValidationOutput,
22
+ ValidationResult,
23
+ ValidationTrigger,
24
+ Validator,
25
+ ValidatorMap
26
+ } from './form'
27
+ export { rules } from './rules'
28
+ export { FORMS_KEY, formsPlugin } from './plugin'
29
+ export type { FormsClient, FormsPluginOptions } from './plugin'