@tachui/forms 0.7.1-alpha → 0.8.0-alpha
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/README.md +87 -272
- package/dist/DatePicker-D5nRFTUm.js +475 -0
- package/dist/DatePicker-D5nRFTUm.js.map +1 -0
- package/dist/Select-yZyKooXk.js +945 -0
- package/dist/Select-yZyKooXk.js.map +1 -0
- package/dist/Slider-0-oal5YR.js +644 -0
- package/dist/Slider-0-oal5YR.js.map +1 -0
- package/dist/TextField-hX15dY3U.js +509 -0
- package/dist/TextField-hX15dY3U.js.map +1 -0
- package/dist/components/advanced/Slider.d.ts +190 -0
- package/dist/components/advanced/Slider.d.ts.map +1 -0
- package/dist/components/advanced/Stepper.d.ts +161 -0
- package/dist/components/advanced/Stepper.d.ts.map +1 -0
- package/dist/components/advanced/index.d.ts +15 -0
- package/dist/components/advanced/index.d.ts.map +1 -0
- package/dist/components/advanced/index.js +6 -0
- package/dist/components/advanced/index.js.map +1 -0
- package/dist/components/date-picker/DatePicker.d.ts +126 -0
- package/dist/components/date-picker/DatePicker.d.ts.map +1 -0
- package/dist/components/date-picker/index.d.ts +14 -0
- package/dist/components/date-picker/index.d.ts.map +1 -0
- package/dist/components/date-picker/index.js +5 -0
- package/dist/components/date-picker/index.js.map +1 -0
- package/dist/components/form-container/index.d.ts +58 -0
- package/dist/components/form-container/index.d.ts.map +1 -0
- package/dist/components/selection/Checkbox.d.ts.map +1 -0
- package/dist/components/selection/Radio.d.ts.map +1 -0
- package/dist/components/selection/Select.d.ts.map +1 -0
- package/dist/components/selection/index.d.ts +68 -0
- package/dist/components/selection/index.d.ts.map +1 -0
- package/dist/components/selection/index.js +12 -0
- package/dist/components/selection/index.js.map +1 -0
- package/dist/components/text-input/TextField.d.ts.map +1 -0
- package/dist/components/text-input/index.d.ts +8 -0
- package/dist/components/text-input/index.d.ts.map +1 -0
- package/dist/components/text-input/index.js +18 -0
- package/dist/components/text-input/index.js.map +1 -0
- package/dist/{state/index.js → index-D3WfkqVv.js} +15 -8
- package/dist/index-D3WfkqVv.js.map +1 -0
- package/dist/index.d.ts +10 -15
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +198 -376
- package/dist/index.js.map +1 -0
- package/dist/state/index.d.ts.map +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/index.d.ts +19 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/validation/component-validation.d.ts +11 -2
- package/dist/validation/component-validation.d.ts.map +1 -1
- package/dist/validation/index.d.ts.map +1 -1
- package/dist/validation/index.js +282 -191
- package/dist/validation/index.js.map +1 -0
- package/package.json +53 -39
- package/src/components/advanced/Slider.ts +722 -0
- package/src/components/advanced/Stepper.ts +715 -0
- package/src/components/advanced/index.ts +20 -0
- package/src/components/date-picker/DatePicker.ts +925 -0
- package/src/components/date-picker/index.ts +20 -0
- package/src/components/form-container/index.ts +266 -0
- package/src/components/selection/Checkbox.ts +478 -0
- package/src/components/selection/Radio.ts +470 -0
- package/src/components/selection/Select.ts +620 -0
- package/src/components/selection/index.ts +81 -0
- package/src/components/text-input/TextField.ts +728 -0
- package/src/components/text-input/index.ts +35 -0
- package/src/index.ts +48 -0
- package/src/state/index.ts +544 -0
- package/src/types/index.ts +579 -0
- package/src/utils/formatters.ts +184 -0
- package/src/utils/index.ts +57 -0
- package/src/validation/component-validation.ts +429 -0
- package/src/validation/index.ts +641 -0
- package/dist/TextField-CGBM3x7K.js +0 -1799
- package/dist/components/Form.d.ts +0 -76
- package/dist/components/Form.d.ts.map +0 -1
- package/dist/components/index.d.ts +0 -9
- package/dist/components/index.d.ts.map +0 -1
- package/dist/components/index.js +0 -28
- package/dist/components/input/Checkbox.d.ts.map +0 -1
- package/dist/components/input/Radio.d.ts.map +0 -1
- package/dist/components/input/Select.d.ts.map +0 -1
- package/dist/components/input/TextField.d.ts.map +0 -1
- package/dist/components/input/index.d.ts +0 -11
- package/dist/components/input/index.d.ts.map +0 -1
- package/dist/utils/validators.d.ts +0 -101
- package/dist/utils/validators.d.ts.map +0 -1
- /package/dist/components/{input → selection}/Checkbox.d.ts +0 -0
- /package/dist/components/{input → selection}/Radio.d.ts +0 -0
- /package/dist/components/{input → selection}/Select.d.ts +0 -0
- /package/dist/components/{input → text-input}/TextField.d.ts +0 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text Input Components
|
|
3
|
+
*
|
|
4
|
+
* All text-based input components with validation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Import and re-export the complete TextField implementation
|
|
8
|
+
export {
|
|
9
|
+
TextField,
|
|
10
|
+
EmailField,
|
|
11
|
+
PasswordField,
|
|
12
|
+
SearchField,
|
|
13
|
+
URLField,
|
|
14
|
+
PhoneField,
|
|
15
|
+
NumberField,
|
|
16
|
+
CreditCardField,
|
|
17
|
+
SSNField,
|
|
18
|
+
PostalCodeField,
|
|
19
|
+
TextArea,
|
|
20
|
+
DateField,
|
|
21
|
+
TimeField,
|
|
22
|
+
ColorField,
|
|
23
|
+
} from './TextField'
|
|
24
|
+
|
|
25
|
+
// Export types from the main types file
|
|
26
|
+
export type {
|
|
27
|
+
TextFieldProps,
|
|
28
|
+
NumberFieldProps,
|
|
29
|
+
TextFieldType,
|
|
30
|
+
TextFieldFormatter,
|
|
31
|
+
TextFieldParser,
|
|
32
|
+
AutoCapitalization,
|
|
33
|
+
KeyboardType,
|
|
34
|
+
ReturnKeyType,
|
|
35
|
+
} from '../../types'
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TachUI Forms - Comprehensive Form Components and Validation
|
|
3
|
+
*
|
|
4
|
+
* Unified package combining all form functionality from @tachui/forms and @tachui/advanced-forms
|
|
5
|
+
* Provides 27 components with comprehensive validation and optimal tree-shaking
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Form Container Components
|
|
9
|
+
export * from './components/form-container'
|
|
10
|
+
|
|
11
|
+
// Text Input Components (TextField, TextArea, EmailField, etc.)
|
|
12
|
+
export * from './components/text-input'
|
|
13
|
+
|
|
14
|
+
// Selection Components (Checkbox, Radio, Select, etc.)
|
|
15
|
+
export * from './components/selection'
|
|
16
|
+
|
|
17
|
+
// Advanced Components (Stepper, Slider)
|
|
18
|
+
export * from './components/advanced'
|
|
19
|
+
|
|
20
|
+
// Date Components (DatePicker with rich calendar interface)
|
|
21
|
+
export * from './components/date-picker'
|
|
22
|
+
|
|
23
|
+
// State Management
|
|
24
|
+
export * from './state'
|
|
25
|
+
|
|
26
|
+
// Validation System
|
|
27
|
+
export * from './validation'
|
|
28
|
+
|
|
29
|
+
// Utilities and Helpers
|
|
30
|
+
export * from './utils'
|
|
31
|
+
|
|
32
|
+
// Export specific types to avoid conflicts
|
|
33
|
+
export type {
|
|
34
|
+
// Core form types
|
|
35
|
+
ValidationRule,
|
|
36
|
+
ValidationResult,
|
|
37
|
+
FieldValidation,
|
|
38
|
+
FieldState,
|
|
39
|
+
|
|
40
|
+
// Component prop types (using specific names to avoid conflicts)
|
|
41
|
+
TextFieldProps,
|
|
42
|
+
NumberFieldProps,
|
|
43
|
+
TextFieldFormatter,
|
|
44
|
+
TextFieldParser,
|
|
45
|
+
AutoCapitalization,
|
|
46
|
+
KeyboardType,
|
|
47
|
+
ReturnKeyType,
|
|
48
|
+
} from './types'
|
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TachUI Forms State Management
|
|
3
|
+
*
|
|
4
|
+
* Reactive form state management with validation, field tracking,
|
|
5
|
+
* and form lifecycle management using TachUI's signal system.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createComputed, createEffect, createSignal } from '@tachui/core'
|
|
9
|
+
import type {
|
|
10
|
+
FieldState,
|
|
11
|
+
FieldValidation,
|
|
12
|
+
FormState,
|
|
13
|
+
FormSubmitHandler,
|
|
14
|
+
UseFormReturn,
|
|
15
|
+
ValidationResult,
|
|
16
|
+
} from '../types'
|
|
17
|
+
import { validateValueAsync } from '../validation'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Create a reactive field state
|
|
21
|
+
*/
|
|
22
|
+
function createFieldState<T = any>(
|
|
23
|
+
initialValue?: T
|
|
24
|
+
): {
|
|
25
|
+
state: () => FieldState<T>
|
|
26
|
+
setValue: (value: T) => void
|
|
27
|
+
setError: (error: string | undefined) => void
|
|
28
|
+
setTouched: (touched: boolean) => void
|
|
29
|
+
setFocused: (focused: boolean) => void
|
|
30
|
+
setValidating: (validating: boolean) => void
|
|
31
|
+
} {
|
|
32
|
+
const [value, setValue] = createSignal<T>(initialValue as T)
|
|
33
|
+
const [error, setError] = createSignal<string | undefined>(undefined)
|
|
34
|
+
const [touched, setTouched] = createSignal(false)
|
|
35
|
+
const [dirty, setDirty] = createSignal(false)
|
|
36
|
+
const [focused, setFocused] = createSignal(false)
|
|
37
|
+
const [validating, setValidating] = createSignal(false)
|
|
38
|
+
|
|
39
|
+
// Mark dirty when value changes from initial
|
|
40
|
+
createEffect(() => {
|
|
41
|
+
const currentValue = value()
|
|
42
|
+
if (currentValue !== initialValue) {
|
|
43
|
+
setDirty(true)
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const valid = createComputed(() => !error())
|
|
48
|
+
|
|
49
|
+
const state = createComputed(
|
|
50
|
+
(): FieldState<T> => ({
|
|
51
|
+
value: value(),
|
|
52
|
+
error: error(),
|
|
53
|
+
touched: touched(),
|
|
54
|
+
dirty: dirty(),
|
|
55
|
+
valid: valid(),
|
|
56
|
+
validating: validating(),
|
|
57
|
+
focused: focused(),
|
|
58
|
+
})
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
state,
|
|
63
|
+
setValue: (newValue: T) => {
|
|
64
|
+
setValue(newValue)
|
|
65
|
+
// Mark dirty if value changed from initial
|
|
66
|
+
if (newValue !== initialValue) {
|
|
67
|
+
setDirty(true)
|
|
68
|
+
} else {
|
|
69
|
+
setDirty(false)
|
|
70
|
+
}
|
|
71
|
+
if (touched()) {
|
|
72
|
+
// Trigger validation if field is touched
|
|
73
|
+
// This will be handled by the form's validation system
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
setError,
|
|
77
|
+
setTouched,
|
|
78
|
+
setFocused,
|
|
79
|
+
setValidating,
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Create a form state manager
|
|
85
|
+
*/
|
|
86
|
+
export function createFormState(
|
|
87
|
+
initialValues: Record<string, any> = {}
|
|
88
|
+
): UseFormReturn {
|
|
89
|
+
const fields = new Map<string, ReturnType<typeof createFieldState>>()
|
|
90
|
+
const validations = new Map<string, FieldValidation>()
|
|
91
|
+
|
|
92
|
+
const [submitting, setSubmitting] = createSignal(false)
|
|
93
|
+
const [submitted, setSubmitted] = createSignal(false)
|
|
94
|
+
|
|
95
|
+
// Auto-register fields from initial values
|
|
96
|
+
Object.keys(initialValues).forEach(name => {
|
|
97
|
+
const fieldManager = createFieldState(initialValues[name])
|
|
98
|
+
fields.set(name, fieldManager)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
// Reactive form state
|
|
102
|
+
const formState = createComputed((): FormState => {
|
|
103
|
+
const fieldStates: Record<string, FieldState> = {}
|
|
104
|
+
const errors: Record<string, string> = {}
|
|
105
|
+
const touched: Record<string, boolean> = {}
|
|
106
|
+
let valid = true
|
|
107
|
+
let dirty = false
|
|
108
|
+
|
|
109
|
+
for (const [name, fieldManager] of fields) {
|
|
110
|
+
const fieldState = fieldManager.state()
|
|
111
|
+
fieldStates[name] = fieldState
|
|
112
|
+
|
|
113
|
+
if (fieldState.error) {
|
|
114
|
+
errors[name] = fieldState.error
|
|
115
|
+
valid = false
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (fieldState.touched) {
|
|
119
|
+
touched[name] = true
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (fieldState.dirty) {
|
|
123
|
+
dirty = true
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
fields: fieldStates,
|
|
129
|
+
valid,
|
|
130
|
+
dirty,
|
|
131
|
+
submitting: submitting(),
|
|
132
|
+
submitted: submitted(),
|
|
133
|
+
errors,
|
|
134
|
+
touched,
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
// Register a field
|
|
139
|
+
const register = (name: string, validation?: FieldValidation) => {
|
|
140
|
+
if (fields.has(name)) {
|
|
141
|
+
// Field already exists, just update validation if provided
|
|
142
|
+
if (validation) {
|
|
143
|
+
validations.set(name, validation)
|
|
144
|
+
}
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const fieldManager = createFieldState(initialValues[name])
|
|
149
|
+
fields.set(name, fieldManager)
|
|
150
|
+
|
|
151
|
+
if (validation) {
|
|
152
|
+
validations.set(name, validation)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Set up validation effects
|
|
156
|
+
if (validation) {
|
|
157
|
+
const fieldState = fieldManager.state
|
|
158
|
+
|
|
159
|
+
createEffect(async () => {
|
|
160
|
+
const state = fieldState()
|
|
161
|
+
const shouldValidate =
|
|
162
|
+
validation.validateOn === 'change' ||
|
|
163
|
+
(validation.validateOn === 'blur' && state.touched) ||
|
|
164
|
+
(validation.validateOn === 'submit' && submitted())
|
|
165
|
+
|
|
166
|
+
if (shouldValidate && (state.dirty || state.touched)) {
|
|
167
|
+
fieldManager.setValidating(true)
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
const result = await validateFieldAsync(state, validation)
|
|
171
|
+
fieldManager.setError(result.valid ? undefined : result.message)
|
|
172
|
+
} catch (_error) {
|
|
173
|
+
fieldManager.setError('Validation error occurred')
|
|
174
|
+
} finally {
|
|
175
|
+
fieldManager.setValidating(false)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Unregister a field
|
|
183
|
+
const unregister = (name: string) => {
|
|
184
|
+
fields.delete(name)
|
|
185
|
+
validations.delete(name)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Set field value
|
|
189
|
+
const setValue = (name: string, value: any) => {
|
|
190
|
+
const fieldManager = fields.get(name)
|
|
191
|
+
if (fieldManager) {
|
|
192
|
+
fieldManager.setValue(value)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Get field value
|
|
197
|
+
const getValue = (name: string) => {
|
|
198
|
+
const fieldManager = fields.get(name)
|
|
199
|
+
return fieldManager?.state().value
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Get field error
|
|
203
|
+
const getError = (name: string) => {
|
|
204
|
+
const fieldManager = fields.get(name)
|
|
205
|
+
return fieldManager?.state().error
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Validate a specific field
|
|
209
|
+
const validateField = async (name: string): Promise<boolean> => {
|
|
210
|
+
const fieldManager = fields.get(name)
|
|
211
|
+
const validation = validations.get(name)
|
|
212
|
+
|
|
213
|
+
if (!fieldManager || !validation) {
|
|
214
|
+
return true
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const fieldState = fieldManager.state()
|
|
218
|
+
fieldManager.setValidating(true)
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
const result = await validateFieldAsync(fieldState, validation)
|
|
222
|
+
fieldManager.setError(result.valid ? undefined : result.message)
|
|
223
|
+
return result.valid
|
|
224
|
+
} catch (_error) {
|
|
225
|
+
fieldManager.setError('Validation error occurred')
|
|
226
|
+
return false
|
|
227
|
+
} finally {
|
|
228
|
+
fieldManager.setValidating(false)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Validate entire form
|
|
233
|
+
const validateForm = async (): Promise<boolean> => {
|
|
234
|
+
const validationPromises = Array.from(fields.keys()).map(validateField)
|
|
235
|
+
const results = await Promise.all(validationPromises)
|
|
236
|
+
return results.every(result => result)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Reset form to initial state
|
|
240
|
+
const resetForm = () => {
|
|
241
|
+
for (const [name, fieldManager] of fields) {
|
|
242
|
+
fieldManager.setValue(initialValues[name])
|
|
243
|
+
fieldManager.setError(undefined)
|
|
244
|
+
fieldManager.setTouched(false)
|
|
245
|
+
fieldManager.setFocused(false)
|
|
246
|
+
fieldManager.setValidating(false)
|
|
247
|
+
}
|
|
248
|
+
setSubmitting(false)
|
|
249
|
+
setSubmitted(false)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Submit form
|
|
253
|
+
const submitForm = async (handler?: FormSubmitHandler) => {
|
|
254
|
+
setSubmitting(true)
|
|
255
|
+
setSubmitted(true)
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
// Validate all fields
|
|
259
|
+
const isValid = await validateForm()
|
|
260
|
+
|
|
261
|
+
if (isValid && handler) {
|
|
262
|
+
// Collect form values
|
|
263
|
+
const values: Record<string, any> = {}
|
|
264
|
+
for (const [name, fieldManager] of fields) {
|
|
265
|
+
values[name] = fieldManager.state().value
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
await handler(values, formState())
|
|
269
|
+
}
|
|
270
|
+
} finally {
|
|
271
|
+
setSubmitting(false)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Watch for changes in specific fields
|
|
276
|
+
const watch = (fieldNames?: string[]): Record<string, any> => {
|
|
277
|
+
const values: Record<string, any> = {}
|
|
278
|
+
const fieldsToWatch = fieldNames || Array.from(fields.keys())
|
|
279
|
+
|
|
280
|
+
for (const name of fieldsToWatch) {
|
|
281
|
+
if (fields.has(name)) {
|
|
282
|
+
values[name] = getValue(name)
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return values
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Trigger validation for specific fields
|
|
290
|
+
const trigger = async (fieldNames?: string[]): Promise<boolean> => {
|
|
291
|
+
const fieldsToValidate = fieldNames || Array.from(fields.keys())
|
|
292
|
+
const validationPromises = fieldsToValidate.map(validateField)
|
|
293
|
+
const results = await Promise.all(validationPromises)
|
|
294
|
+
return results.every(result => result)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
fields: formState().fields,
|
|
299
|
+
state: formState(),
|
|
300
|
+
register,
|
|
301
|
+
unregister,
|
|
302
|
+
setValue,
|
|
303
|
+
getValue,
|
|
304
|
+
getError,
|
|
305
|
+
validateField,
|
|
306
|
+
validateForm,
|
|
307
|
+
resetForm,
|
|
308
|
+
submitForm,
|
|
309
|
+
watch,
|
|
310
|
+
trigger,
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Async field validation helper
|
|
316
|
+
*/
|
|
317
|
+
async function validateFieldAsync(
|
|
318
|
+
fieldState: FieldState,
|
|
319
|
+
validation: FieldValidation
|
|
320
|
+
): Promise<ValidationResult> {
|
|
321
|
+
if (!validation.rules || validation.rules.length === 0) {
|
|
322
|
+
return { valid: true }
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Use debounced validation if specified
|
|
326
|
+
if (validation.debounceMs) {
|
|
327
|
+
return new Promise(resolve => {
|
|
328
|
+
setTimeout(async () => {
|
|
329
|
+
const result = await validateValueAsync(
|
|
330
|
+
fieldState.value,
|
|
331
|
+
validation.rules!
|
|
332
|
+
)
|
|
333
|
+
resolve(result)
|
|
334
|
+
}, validation.debounceMs)
|
|
335
|
+
|
|
336
|
+
// Store timeout ID for potential cleanup
|
|
337
|
+
// In a real implementation, you'd want to manage this better
|
|
338
|
+
})
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return validateValueAsync(fieldState.value, validation.rules)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Form field hook for individual field management
|
|
346
|
+
*/
|
|
347
|
+
export function createField<T = any>(
|
|
348
|
+
_name: string,
|
|
349
|
+
initialValue?: T,
|
|
350
|
+
validation?: FieldValidation
|
|
351
|
+
): {
|
|
352
|
+
value: () => T
|
|
353
|
+
setValue: (value: T) => void
|
|
354
|
+
error: () => string | undefined
|
|
355
|
+
touched: () => boolean
|
|
356
|
+
dirty: () => boolean
|
|
357
|
+
valid: () => boolean
|
|
358
|
+
validating: () => boolean
|
|
359
|
+
focused: () => boolean
|
|
360
|
+
onFocus: () => void
|
|
361
|
+
onBlur: () => void
|
|
362
|
+
validate: () => Promise<boolean>
|
|
363
|
+
reset: () => void
|
|
364
|
+
} {
|
|
365
|
+
const fieldManager = createFieldState(initialValue)
|
|
366
|
+
const fieldState = fieldManager.state
|
|
367
|
+
|
|
368
|
+
// Validation effect
|
|
369
|
+
if (validation) {
|
|
370
|
+
createEffect(async () => {
|
|
371
|
+
const state = fieldState()
|
|
372
|
+
const shouldValidate =
|
|
373
|
+
validation.validateOn === 'change' ||
|
|
374
|
+
(validation.validateOn === 'blur' && state.touched)
|
|
375
|
+
|
|
376
|
+
if (shouldValidate && (state.dirty || state.touched)) {
|
|
377
|
+
fieldManager.setValidating(true)
|
|
378
|
+
|
|
379
|
+
try {
|
|
380
|
+
const result = await validateFieldAsync(state, validation)
|
|
381
|
+
fieldManager.setError(result.valid ? undefined : result.message)
|
|
382
|
+
} catch (_error) {
|
|
383
|
+
fieldManager.setError('Validation error occurred')
|
|
384
|
+
} finally {
|
|
385
|
+
fieldManager.setValidating(false)
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
})
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const validate = async (): Promise<boolean> => {
|
|
392
|
+
if (!validation) return true
|
|
393
|
+
|
|
394
|
+
fieldManager.setValidating(true)
|
|
395
|
+
try {
|
|
396
|
+
const result = await validateFieldAsync(fieldState(), validation)
|
|
397
|
+
fieldManager.setError(result.valid ? undefined : result.message)
|
|
398
|
+
return result.valid
|
|
399
|
+
} catch (_error) {
|
|
400
|
+
fieldManager.setError('Validation error occurred')
|
|
401
|
+
return false
|
|
402
|
+
} finally {
|
|
403
|
+
fieldManager.setValidating(false)
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const reset = () => {
|
|
408
|
+
fieldManager.setValue(initialValue as T)
|
|
409
|
+
fieldManager.setError(undefined)
|
|
410
|
+
fieldManager.setTouched(false)
|
|
411
|
+
fieldManager.setFocused(false)
|
|
412
|
+
fieldManager.setValidating(false)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return {
|
|
416
|
+
value: () => fieldState().value,
|
|
417
|
+
setValue: fieldManager.setValue,
|
|
418
|
+
error: () => fieldState().error,
|
|
419
|
+
touched: () => fieldState().touched,
|
|
420
|
+
dirty: () => fieldState().dirty,
|
|
421
|
+
valid: () => fieldState().valid,
|
|
422
|
+
validating: () => fieldState().validating,
|
|
423
|
+
focused: () => fieldState().focused,
|
|
424
|
+
onFocus: () => {
|
|
425
|
+
fieldManager.setFocused(true)
|
|
426
|
+
},
|
|
427
|
+
onBlur: () => {
|
|
428
|
+
fieldManager.setFocused(false)
|
|
429
|
+
fieldManager.setTouched(true)
|
|
430
|
+
},
|
|
431
|
+
validate,
|
|
432
|
+
reset,
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Multi-step form state manager
|
|
438
|
+
*/
|
|
439
|
+
export function createMultiStepFormState(
|
|
440
|
+
steps: string[],
|
|
441
|
+
initialValues: Record<string, any> = {}
|
|
442
|
+
) {
|
|
443
|
+
const [currentStep, setCurrentStep] = createSignal(0)
|
|
444
|
+
const [completedSteps, setCompletedSteps] = createSignal<Set<number>>(
|
|
445
|
+
new Set()
|
|
446
|
+
)
|
|
447
|
+
const stepForms = new Map<number, UseFormReturn>()
|
|
448
|
+
|
|
449
|
+
// Create form state for each step
|
|
450
|
+
steps.forEach((_, index) => {
|
|
451
|
+
const stepValues = Object.keys(initialValues)
|
|
452
|
+
.filter(key => key.startsWith(`step_${index}_`))
|
|
453
|
+
.reduce(
|
|
454
|
+
(acc, key) => {
|
|
455
|
+
const fieldName = key.replace(`step_${index}_`, '')
|
|
456
|
+
acc[fieldName] = initialValues[key]
|
|
457
|
+
return acc
|
|
458
|
+
},
|
|
459
|
+
{} as Record<string, any>
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
stepForms.set(index, createFormState(stepValues))
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
const nextStep = async (validateCurrent = true): Promise<boolean> => {
|
|
466
|
+
const current = currentStep()
|
|
467
|
+
const currentForm = stepForms.get(current)
|
|
468
|
+
|
|
469
|
+
if (validateCurrent && currentForm) {
|
|
470
|
+
const isValid = await currentForm.validateForm()
|
|
471
|
+
if (!isValid) {
|
|
472
|
+
return false
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (current < steps.length - 1) {
|
|
477
|
+
setCompletedSteps(prev => new Set([...prev, current]))
|
|
478
|
+
setCurrentStep(current + 1)
|
|
479
|
+
return true
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return false
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const previousStep = (): boolean => {
|
|
486
|
+
const current = currentStep()
|
|
487
|
+
if (current > 0) {
|
|
488
|
+
setCurrentStep(current - 1)
|
|
489
|
+
return true
|
|
490
|
+
}
|
|
491
|
+
return false
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const goToStep = (stepIndex: number): boolean => {
|
|
495
|
+
if (stepIndex >= 0 && stepIndex < steps.length) {
|
|
496
|
+
setCurrentStep(stepIndex)
|
|
497
|
+
return true
|
|
498
|
+
}
|
|
499
|
+
return false
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const getCurrentForm = (): UseFormReturn | undefined => {
|
|
503
|
+
return stepForms.get(currentStep())
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const getAllValues = (): Record<string, any> => {
|
|
507
|
+
const allValues: Record<string, any> = {}
|
|
508
|
+
|
|
509
|
+
stepForms.forEach((form, stepIndex) => {
|
|
510
|
+
const stepValues = form.watch()
|
|
511
|
+
Object.keys(stepValues).forEach(fieldName => {
|
|
512
|
+
allValues[`step_${stepIndex}_${fieldName}`] = stepValues[fieldName]
|
|
513
|
+
})
|
|
514
|
+
})
|
|
515
|
+
|
|
516
|
+
return allValues
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const validateAllSteps = async (): Promise<boolean> => {
|
|
520
|
+
const validationPromises = Array.from(stepForms.values()).map(form =>
|
|
521
|
+
form.validateForm()
|
|
522
|
+
)
|
|
523
|
+
const results = await Promise.all(validationPromises)
|
|
524
|
+
return results.every(result => result)
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return {
|
|
528
|
+
currentStep,
|
|
529
|
+
completedSteps,
|
|
530
|
+
nextStep,
|
|
531
|
+
previousStep,
|
|
532
|
+
goToStep,
|
|
533
|
+
getCurrentForm,
|
|
534
|
+
getAllValues,
|
|
535
|
+
validateAllSteps,
|
|
536
|
+
getStepForm: (stepIndex: number) => stepForms.get(stepIndex),
|
|
537
|
+
isStepCompleted: (stepIndex: number) => completedSteps().has(stepIndex),
|
|
538
|
+
canGoToStep: (stepIndex: number) =>
|
|
539
|
+
stepIndex <= currentStep() || completedSteps().has(stepIndex),
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Export validation function for internal use
|
|
544
|
+
export { validateValueAsync } from '../validation'
|