@saas-ui/forms 2.0.0-next.3 → 2.0.0-next.6
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/CHANGELOG.md +43 -0
- package/README.md +53 -6
- package/dist/ajv/index.d.ts +358 -11
- package/dist/ajv/index.js +7 -9
- package/dist/ajv/index.js.map +1 -1
- package/dist/ajv/index.mjs +7 -10
- package/dist/ajv/index.mjs.map +1 -1
- package/dist/index.d.ts +448 -247
- package/dist/index.js +707 -682
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +691 -666
- package/dist/index.mjs.map +1 -1
- package/dist/yup/index.d.ts +580 -21
- package/dist/yup/index.js +6 -10
- package/dist/yup/index.js.map +1 -1
- package/dist/yup/index.mjs +4 -8
- package/dist/yup/index.mjs.map +1 -1
- package/dist/zod/index.d.ts +580 -11
- package/dist/zod/index.js +5 -0
- package/dist/zod/index.js.map +1 -1
- package/dist/zod/index.mjs +5 -1
- package/dist/zod/index.mjs.map +1 -1
- package/package.json +19 -10
- package/src/array-field.tsx +82 -45
- package/src/auto-form.tsx +7 -3
- package/src/base-field.tsx +54 -0
- package/src/create-field.tsx +144 -0
- package/src/create-form.tsx +54 -0
- package/src/default-fields.tsx +163 -0
- package/src/display-field.tsx +9 -11
- package/src/display-if.tsx +20 -13
- package/src/field-resolver.ts +10 -8
- package/src/field.tsx +18 -445
- package/src/fields-context.tsx +23 -0
- package/src/fields.tsx +34 -21
- package/src/form-context.tsx +84 -0
- package/src/form.tsx +69 -52
- package/src/index.ts +44 -4
- package/src/input-right-button/input-right-button.stories.tsx +1 -1
- package/src/input-right-button/input-right-button.tsx +0 -2
- package/src/layout.tsx +16 -11
- package/src/number-input/number-input.tsx +9 -5
- package/src/object-field.tsx +13 -8
- package/src/password-input/password-input.stories.tsx +23 -2
- package/src/password-input/password-input.tsx +6 -6
- package/src/pin-input/pin-input.tsx +1 -5
- package/src/radio/radio-input.stories.tsx +1 -1
- package/src/radio/radio-input.tsx +12 -10
- package/src/select/native-select.tsx +1 -4
- package/src/select/select-context.tsx +130 -0
- package/src/select/select.stories.tsx +116 -85
- package/src/select/select.test.tsx +1 -1
- package/src/select/select.tsx +160 -146
- package/src/step-form.tsx +29 -11
- package/src/submit-button.tsx +5 -1
- package/src/types.ts +144 -0
- package/src/use-array-field.tsx +9 -3
- package/src/utils.ts +23 -1
- package/src/watch-field.tsx +2 -6
- /package/src/radio/{radio.test.tsx → radio-input.test.tsx} +0 -0
package/src/field.tsx
CHANGED
@@ -1,45 +1,10 @@
|
|
1
1
|
import * as React from 'react'
|
2
|
-
import {
|
3
|
-
useFormContext,
|
4
|
-
FormState,
|
5
|
-
Controller,
|
6
|
-
get,
|
7
|
-
RegisterOptions,
|
8
|
-
FieldValues,
|
9
|
-
FieldPath,
|
10
|
-
} from 'react-hook-form'
|
11
|
-
|
12
|
-
import {
|
13
|
-
forwardRef,
|
14
|
-
Box,
|
15
|
-
FormControl,
|
16
|
-
FormControlProps,
|
17
|
-
FormLabel,
|
18
|
-
FormHelperText,
|
19
|
-
FormErrorMessage,
|
20
|
-
Input,
|
21
|
-
Textarea,
|
22
|
-
Checkbox,
|
23
|
-
Switch,
|
24
|
-
useMergeRefs,
|
25
|
-
InputGroup,
|
26
|
-
InputProps,
|
27
|
-
TextareaProps,
|
28
|
-
SwitchProps,
|
29
|
-
CheckboxProps,
|
30
|
-
PinInputField,
|
31
|
-
HStack,
|
32
|
-
PinInput,
|
33
|
-
UsePinInputProps,
|
34
|
-
SystemProps,
|
35
|
-
} from '@chakra-ui/react'
|
36
|
-
import { __DEV__, FocusableElement, callAllHandlers } from '@chakra-ui/utils'
|
37
|
-
|
38
|
-
import { NumberInput, NumberInputProps } from './number-input'
|
39
|
-
import { PasswordInput, PasswordInputProps } from './password-input'
|
40
|
-
import { RadioInput, RadioInputProps } from './radio'
|
2
|
+
import { RegisterOptions, FieldValues } from 'react-hook-form'
|
41
3
|
|
42
|
-
import {
|
4
|
+
import { FocusableElement } from '@chakra-ui/utils'
|
5
|
+
import { useField } from './fields-context'
|
6
|
+
import { FieldProps } from './types'
|
7
|
+
import { useFieldProps } from './form-context'
|
43
8
|
|
44
9
|
export interface Option {
|
45
10
|
value: string
|
@@ -52,425 +17,33 @@ export type FieldRules = Pick<
|
|
52
17
|
'required' | 'min' | 'max' | 'maxLength' | 'minLength' | 'pattern'
|
53
18
|
>
|
54
19
|
|
55
|
-
export interface FieldProps<
|
56
|
-
TFieldValues extends FieldValues = FieldValues,
|
57
|
-
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
58
|
-
> extends Omit<FormControlProps, 'label' | 'type'> {
|
59
|
-
/**
|
60
|
-
* The field name
|
61
|
-
*/
|
62
|
-
name: TName
|
63
|
-
/**
|
64
|
-
* The field label
|
65
|
-
*/
|
66
|
-
label?: string
|
67
|
-
/**
|
68
|
-
* Hide the field label
|
69
|
-
*/
|
70
|
-
hideLabel?: boolean
|
71
|
-
/**
|
72
|
-
* Field help text
|
73
|
-
*/
|
74
|
-
help?: string
|
75
|
-
/**
|
76
|
-
* React hook form rules
|
77
|
-
*/
|
78
|
-
rules?: Omit<
|
79
|
-
RegisterOptions<TFieldValues, TName>,
|
80
|
-
'valueAsNumber' | 'valueAsDate' | 'setValueAs' | 'disabled'
|
81
|
-
>
|
82
|
-
/**
|
83
|
-
* Build-in types:
|
84
|
-
* - text
|
85
|
-
* - number
|
86
|
-
* - password
|
87
|
-
* - textarea
|
88
|
-
* - select
|
89
|
-
* - native-select
|
90
|
-
* - checkbox
|
91
|
-
* - radio
|
92
|
-
* - switch
|
93
|
-
* - pin
|
94
|
-
*
|
95
|
-
* Will default to a text field if there is no matching type.
|
96
|
-
*/
|
97
|
-
type?: string
|
98
|
-
/**
|
99
|
-
* The input placeholder
|
100
|
-
*/
|
101
|
-
placeholder?: string
|
102
|
-
}
|
103
|
-
|
104
|
-
const inputTypes: Record<string, React.FC<any>> = {}
|
105
|
-
|
106
20
|
const defaultInputType = 'text'
|
107
21
|
|
108
|
-
const getInput = (type: string) => {
|
109
|
-
return inputTypes[type] || inputTypes[defaultInputType]
|
110
|
-
}
|
111
|
-
|
112
|
-
const getError = (name: string, formState: FormState<{ [x: string]: any }>) => {
|
113
|
-
return get(formState.errors, name)
|
114
|
-
}
|
115
|
-
|
116
|
-
const isTouched = (
|
117
|
-
name: string,
|
118
|
-
formState: FormState<{ [x: string]: any }>
|
119
|
-
) => {
|
120
|
-
return get(formState.touchedFields, name)
|
121
|
-
}
|
122
|
-
|
123
|
-
export const BaseField: React.FC<FieldProps> = (props) => {
|
124
|
-
const { name, label, help, hideLabel, children, ...controlProps } = props
|
125
|
-
|
126
|
-
const { formState } = useFormContext()
|
127
|
-
|
128
|
-
const error = getError(name, formState)
|
129
|
-
|
130
|
-
return (
|
131
|
-
<FormControl {...controlProps} isInvalid={!!error}>
|
132
|
-
{label && !hideLabel ? <FormLabel>{label}</FormLabel> : null}
|
133
|
-
<Box>
|
134
|
-
{children}
|
135
|
-
{help && !error?.message ? (
|
136
|
-
<FormHelperText>{help}</FormHelperText>
|
137
|
-
) : null}
|
138
|
-
{error?.message && (
|
139
|
-
<FormErrorMessage>{error?.message}</FormErrorMessage>
|
140
|
-
)}
|
141
|
-
</Box>
|
142
|
-
</FormControl>
|
143
|
-
)
|
144
|
-
}
|
145
|
-
|
146
|
-
if (__DEV__) {
|
147
|
-
BaseField.displayName = 'BaseField'
|
148
|
-
}
|
149
|
-
|
150
|
-
export type As<Props = any> = React.ElementType<Props>
|
151
|
-
|
152
|
-
export type PropsOf<T extends As> = React.ComponentPropsWithoutRef<T> & {
|
153
|
-
type?: FieldTypes
|
154
|
-
}
|
155
|
-
|
156
22
|
/**
|
23
|
+
* Form field component.
|
24
|
+
*
|
157
25
|
* Build-in types:
|
158
|
-
* -
|
159
|
-
* - number
|
160
|
-
* - password
|
161
|
-
* - textarea
|
162
|
-
* - select
|
163
|
-
* - native-select
|
164
|
-
* - checkbox
|
165
|
-
* - radio
|
166
|
-
* - switch
|
167
|
-
* - pin
|
26
|
+
* text, number, password, textarea, select, native-select, checkbox, radio, switch, pin
|
168
27
|
*
|
169
28
|
* Will default to a text field if there is no matching type.
|
29
|
+
|
30
|
+
* @see Docs https://saas-ui.dev/docs/components/forms/field
|
170
31
|
*/
|
171
32
|
export const Field = React.forwardRef(
|
172
33
|
<TFieldValues extends FieldValues = FieldValues>(
|
173
|
-
props: FieldProps<TFieldValues
|
34
|
+
props: FieldProps<TFieldValues>,
|
174
35
|
ref: React.ForwardedRef<FocusableElement>
|
175
36
|
) => {
|
176
|
-
const { type = defaultInputType } = props
|
177
|
-
const
|
37
|
+
const { type = defaultInputType, name } = props
|
38
|
+
const overrides = useFieldProps(name)
|
39
|
+
const InputComponent = useField(overrides?.type || type)
|
178
40
|
|
179
|
-
return <InputComponent ref={ref} {...props} />
|
41
|
+
return <InputComponent ref={ref} {...props} {...overrides} />
|
180
42
|
}
|
181
43
|
) as (<TFieldValues extends FieldValues>(
|
182
|
-
props: FieldProps<TFieldValues> &
|
183
|
-
|
184
|
-
|
185
|
-
}
|
44
|
+
props: FieldProps<TFieldValues> & {
|
45
|
+
ref?: React.ForwardedRef<FocusableElement>
|
46
|
+
}
|
186
47
|
) => React.ReactElement) & {
|
187
48
|
displayName?: string
|
188
49
|
}
|
189
|
-
|
190
|
-
interface CreateFieldProps {
|
191
|
-
displayName: string
|
192
|
-
hideLabel?: boolean
|
193
|
-
BaseField: React.FC<any>
|
194
|
-
}
|
195
|
-
|
196
|
-
const createField = (
|
197
|
-
InputComponent: React.FC<any>,
|
198
|
-
{ displayName, hideLabel, BaseField }: CreateFieldProps
|
199
|
-
) => {
|
200
|
-
const Field = forwardRef((props, ref) => {
|
201
|
-
const {
|
202
|
-
id,
|
203
|
-
name,
|
204
|
-
label,
|
205
|
-
help,
|
206
|
-
isDisabled,
|
207
|
-
isInvalid,
|
208
|
-
isReadOnly,
|
209
|
-
isRequired,
|
210
|
-
rules,
|
211
|
-
...inputProps
|
212
|
-
} = props
|
213
|
-
|
214
|
-
const inputRules = {
|
215
|
-
required: isRequired,
|
216
|
-
...rules,
|
217
|
-
}
|
218
|
-
|
219
|
-
return (
|
220
|
-
<BaseField
|
221
|
-
id={id}
|
222
|
-
name={name}
|
223
|
-
label={label}
|
224
|
-
help={help}
|
225
|
-
hideLabel={hideLabel}
|
226
|
-
isDisabled={isDisabled}
|
227
|
-
isInvalid={isInvalid}
|
228
|
-
isReadOnly={isReadOnly}
|
229
|
-
isRequired={isRequired}
|
230
|
-
>
|
231
|
-
<InputComponent
|
232
|
-
ref={ref}
|
233
|
-
id={id}
|
234
|
-
name={name}
|
235
|
-
label={hideLabel ? label : undefined} // Only pass down the label when it should be inline.
|
236
|
-
rules={inputRules}
|
237
|
-
{...inputProps}
|
238
|
-
/>
|
239
|
-
</BaseField>
|
240
|
-
)
|
241
|
-
})
|
242
|
-
Field.displayName = displayName
|
243
|
-
|
244
|
-
return Field
|
245
|
-
}
|
246
|
-
|
247
|
-
export const withControlledInput = (InputComponent: React.FC<any>) => {
|
248
|
-
return forwardRef<FieldProps, typeof InputComponent>(
|
249
|
-
({ name, rules, ...inputProps }, ref) => {
|
250
|
-
const { control } = useFormContext()
|
251
|
-
|
252
|
-
return (
|
253
|
-
<Controller
|
254
|
-
name={name}
|
255
|
-
control={control}
|
256
|
-
rules={rules}
|
257
|
-
render={({ field: { ref: _ref, ...field } }) => (
|
258
|
-
<InputComponent
|
259
|
-
{...field}
|
260
|
-
{...inputProps}
|
261
|
-
onChange={callAllHandlers(inputProps.onChange, field.onChange)}
|
262
|
-
onBlur={callAllHandlers(inputProps.onBlur, field.onBlur)}
|
263
|
-
ref={useMergeRefs(ref, _ref)}
|
264
|
-
/>
|
265
|
-
)}
|
266
|
-
/>
|
267
|
-
)
|
268
|
-
}
|
269
|
-
)
|
270
|
-
}
|
271
|
-
|
272
|
-
export const withUncontrolledInput = (InputComponent: React.FC<any>) => {
|
273
|
-
return forwardRef<FieldProps, typeof InputComponent>(
|
274
|
-
({ name, rules, ...inputProps }, ref) => {
|
275
|
-
const { register } = useFormContext()
|
276
|
-
|
277
|
-
const { ref: _ref, ...field } = register(name, rules)
|
278
|
-
|
279
|
-
return (
|
280
|
-
<InputComponent
|
281
|
-
{...field}
|
282
|
-
{...inputProps}
|
283
|
-
onChange={callAllHandlers(inputProps.onChange, field.onChange)}
|
284
|
-
onBlur={callAllHandlers(inputProps.onBlur, field.onBlur)}
|
285
|
-
ref={useMergeRefs(ref, _ref)}
|
286
|
-
/>
|
287
|
-
)
|
288
|
-
}
|
289
|
-
)
|
290
|
-
}
|
291
|
-
|
292
|
-
export interface RegisterFieldTypeOptions {
|
293
|
-
isControlled?: boolean
|
294
|
-
hideLabel?: boolean
|
295
|
-
BaseField?: React.FC<any>
|
296
|
-
}
|
297
|
-
|
298
|
-
/**
|
299
|
-
* Register a new field type
|
300
|
-
* @param type The name for this field in kebab-case, eg `email` or `array-field`
|
301
|
-
* @param component The React component
|
302
|
-
* @param options
|
303
|
-
* @param options.isControlled Set this to true if this is a controlled field.
|
304
|
-
* @param options.hideLabel Hide the field label, for example for the checkbox field.
|
305
|
-
*/
|
306
|
-
export const registerFieldType = <T extends object>(
|
307
|
-
type: string,
|
308
|
-
component: React.FC<T>,
|
309
|
-
options?: RegisterFieldTypeOptions
|
310
|
-
) => {
|
311
|
-
let InputComponent
|
312
|
-
if (options?.isControlled) {
|
313
|
-
InputComponent = withControlledInput(component)
|
314
|
-
} else {
|
315
|
-
InputComponent = withUncontrolledInput(component)
|
316
|
-
}
|
317
|
-
|
318
|
-
const Field = createField(InputComponent, {
|
319
|
-
displayName: `${type
|
320
|
-
.split('-')
|
321
|
-
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
322
|
-
.join('')}Field`,
|
323
|
-
hideLabel: options?.hideLabel,
|
324
|
-
BaseField: options?.BaseField || BaseField,
|
325
|
-
}) as React.FC<T & FieldProps>
|
326
|
-
|
327
|
-
inputTypes[type] = Field
|
328
|
-
|
329
|
-
return Field
|
330
|
-
}
|
331
|
-
|
332
|
-
export interface InputFieldProps extends InputProps {
|
333
|
-
type?: string
|
334
|
-
leftAddon?: React.ReactNode
|
335
|
-
rightAddon?: React.ReactNode
|
336
|
-
}
|
337
|
-
|
338
|
-
export const InputField = registerFieldType<InputFieldProps>(
|
339
|
-
'text',
|
340
|
-
forwardRef(({ type = 'text', leftAddon, rightAddon, size, ...rest }, ref) => {
|
341
|
-
const input = <Input type={type} size={size} {...rest} ref={ref} />
|
342
|
-
if (leftAddon || rightAddon) {
|
343
|
-
return (
|
344
|
-
<InputGroup size={size}>
|
345
|
-
{leftAddon}
|
346
|
-
{input}
|
347
|
-
{rightAddon}
|
348
|
-
</InputGroup>
|
349
|
-
)
|
350
|
-
}
|
351
|
-
return input
|
352
|
-
})
|
353
|
-
)
|
354
|
-
|
355
|
-
export interface NumberInputFieldProps extends NumberInputProps {
|
356
|
-
type: 'number'
|
357
|
-
}
|
358
|
-
|
359
|
-
export const NumberInputField = registerFieldType<NumberInputFieldProps>(
|
360
|
-
'number',
|
361
|
-
NumberInput,
|
362
|
-
{
|
363
|
-
isControlled: true,
|
364
|
-
}
|
365
|
-
)
|
366
|
-
|
367
|
-
export const PasswordInputField = registerFieldType<PasswordInputProps>(
|
368
|
-
'password',
|
369
|
-
forwardRef((props, ref) => <PasswordInput ref={ref} {...props} />)
|
370
|
-
)
|
371
|
-
|
372
|
-
export const TextareaField = registerFieldType<TextareaProps>(
|
373
|
-
'textarea',
|
374
|
-
Textarea
|
375
|
-
)
|
376
|
-
|
377
|
-
export const SwitchField = registerFieldType<SwitchProps>(
|
378
|
-
'switch',
|
379
|
-
forwardRef(({ type, value, ...rest }, ref) => {
|
380
|
-
return <Switch isChecked={!!value} {...rest} ref={ref} />
|
381
|
-
}),
|
382
|
-
{
|
383
|
-
isControlled: true,
|
384
|
-
}
|
385
|
-
)
|
386
|
-
|
387
|
-
export const SelectField = registerFieldType<SelectProps>('select', Select, {
|
388
|
-
isControlled: true,
|
389
|
-
})
|
390
|
-
|
391
|
-
export const CheckboxField = registerFieldType<CheckboxProps>(
|
392
|
-
'checkbox',
|
393
|
-
forwardRef(({ label, type, ...props }, ref) => {
|
394
|
-
return (
|
395
|
-
<Checkbox ref={ref} {...props}>
|
396
|
-
{label}
|
397
|
-
</Checkbox>
|
398
|
-
)
|
399
|
-
}),
|
400
|
-
{
|
401
|
-
hideLabel: true,
|
402
|
-
}
|
403
|
-
)
|
404
|
-
|
405
|
-
export const RadioField = registerFieldType<RadioInputProps>(
|
406
|
-
'radio',
|
407
|
-
RadioInput,
|
408
|
-
{
|
409
|
-
isControlled: true,
|
410
|
-
}
|
411
|
-
)
|
412
|
-
|
413
|
-
export const NativeSelectField = registerFieldType<NativeSelectProps>(
|
414
|
-
'native-select',
|
415
|
-
NativeSelect,
|
416
|
-
{ isControlled: true }
|
417
|
-
)
|
418
|
-
|
419
|
-
export interface PinFieldProps extends Omit<UsePinInputProps, 'type'> {
|
420
|
-
pinLength?: number
|
421
|
-
pinType?: 'alphanumeric' | 'number'
|
422
|
-
spacing?: SystemProps['margin']
|
423
|
-
}
|
424
|
-
|
425
|
-
export const PinField = registerFieldType<PinFieldProps>(
|
426
|
-
'pin',
|
427
|
-
forwardRef((props, ref) => {
|
428
|
-
const { pinLength = 4, pinType, spacing, ...inputProps } = props
|
429
|
-
|
430
|
-
const inputs: React.ReactNode[] = []
|
431
|
-
for (let i = 0; i < pinLength; i++) {
|
432
|
-
inputs.push(<PinInputField key={i} ref={ref} />)
|
433
|
-
}
|
434
|
-
|
435
|
-
return (
|
436
|
-
<HStack spacing={spacing}>
|
437
|
-
<PinInput {...inputProps} type={pinType}>
|
438
|
-
{inputs}
|
439
|
-
</PinInput>
|
440
|
-
</HStack>
|
441
|
-
)
|
442
|
-
}),
|
443
|
-
{
|
444
|
-
isControlled: true,
|
445
|
-
}
|
446
|
-
)
|
447
|
-
|
448
|
-
const fieldTypes = {
|
449
|
-
text: InputField,
|
450
|
-
email: InputField,
|
451
|
-
url: InputField,
|
452
|
-
phone: InputField,
|
453
|
-
number: NumberInputField,
|
454
|
-
password: PasswordInputField,
|
455
|
-
textarea: TextareaField,
|
456
|
-
switch: SwitchField,
|
457
|
-
checkbox: CheckboxField,
|
458
|
-
radio: RadioField,
|
459
|
-
pin: PinField,
|
460
|
-
select: SelectField,
|
461
|
-
'native-select': NativeSelectField,
|
462
|
-
}
|
463
|
-
|
464
|
-
type FieldTypes = typeof fieldTypes
|
465
|
-
|
466
|
-
type FieldType<Props = any> = React.ElementType<Props>
|
467
|
-
|
468
|
-
type TypeProps<P extends FieldType, T> = React.ComponentPropsWithoutRef<P> & {
|
469
|
-
type: T
|
470
|
-
}
|
471
|
-
|
472
|
-
type FieldTypeProps =
|
473
|
-
| {
|
474
|
-
[Property in keyof FieldTypes]: TypeProps<FieldTypes[Property], Property>
|
475
|
-
}[keyof FieldTypes]
|
476
|
-
| { type?: string }
|
@@ -0,0 +1,23 @@
|
|
1
|
+
import React from 'react'
|
2
|
+
import { defaultFieldTypes, InputField } from './default-fields'
|
3
|
+
|
4
|
+
const FieldsContext = React.createContext<Record<string, React.FC<any>> | null>(
|
5
|
+
null
|
6
|
+
)
|
7
|
+
|
8
|
+
export const FieldsProvider: React.FC<{
|
9
|
+
value: Record<string, React.FC<any>>
|
10
|
+
children: React.ReactNode
|
11
|
+
}> = (props) => {
|
12
|
+
const fields = { ...defaultFieldTypes, ...props.value }
|
13
|
+
return (
|
14
|
+
<FieldsContext.Provider value={fields}>
|
15
|
+
{props.children}
|
16
|
+
</FieldsContext.Provider>
|
17
|
+
)
|
18
|
+
}
|
19
|
+
|
20
|
+
export const useField = (type: string): React.FC<any> => {
|
21
|
+
const context = React.useContext(FieldsContext)
|
22
|
+
return context?.[type] || InputField
|
23
|
+
}
|
package/src/fields.tsx
CHANGED
@@ -1,17 +1,16 @@
|
|
1
1
|
import * as React from 'react'
|
2
|
-
import { __DEV__ } from '@chakra-ui/utils'
|
3
2
|
|
4
|
-
import { Form } from './form'
|
5
3
|
import { FormLayout } from './layout'
|
6
|
-
import {
|
4
|
+
import { BaseFieldProps } from './types'
|
5
|
+
import { Field } from './field'
|
7
6
|
|
8
7
|
import { ArrayField } from './array-field'
|
9
8
|
import { ObjectField } from './object-field'
|
10
9
|
import { FieldResolver } from './field-resolver'
|
11
|
-
import { useFormContext } from '
|
10
|
+
import { useFormContext } from './form-context'
|
12
11
|
|
13
|
-
export interface FieldsProps {
|
14
|
-
schema
|
12
|
+
export interface FieldsProps<TSchema = any> {
|
13
|
+
schema?: TSchema
|
15
14
|
fieldResolver?: FieldResolver
|
16
15
|
focusFirstField?: boolean
|
17
16
|
}
|
@@ -20,42 +19,54 @@ const mapNestedFields = (resolver: FieldResolver, name: string) => {
|
|
20
19
|
return resolver
|
21
20
|
.getNestedFields(name)
|
22
21
|
?.map(
|
23
|
-
(
|
24
|
-
|
22
|
+
(
|
23
|
+
{ name, type, ...nestedFieldProps }: BaseFieldProps,
|
24
|
+
i
|
25
|
+
): React.ReactNode => (
|
26
|
+
<Field
|
27
|
+
key={name || i}
|
28
|
+
name={name}
|
29
|
+
type={type as any}
|
30
|
+
{...nestedFieldProps}
|
31
|
+
/>
|
25
32
|
)
|
26
33
|
)
|
27
34
|
}
|
28
35
|
|
29
|
-
export const
|
30
|
-
schema,
|
31
|
-
fieldResolver,
|
36
|
+
export const AutoFields: React.FC<FieldsProps> = ({
|
37
|
+
schema: schemaProp,
|
38
|
+
fieldResolver: fieldResolverProp,
|
32
39
|
focusFirstField,
|
33
40
|
...props
|
34
41
|
}) => {
|
35
|
-
const
|
36
|
-
|
37
|
-
|
38
|
-
)
|
42
|
+
const context = useFormContext()
|
43
|
+
const schema = schemaProp || context.schema
|
44
|
+
const fieldResolver = fieldResolverProp || context.fieldResolver
|
45
|
+
const resolver = React.useMemo(() => fieldResolver, [schema, fieldResolver])
|
39
46
|
|
40
|
-
const fields = React.useMemo(() => resolver
|
47
|
+
const fields = React.useMemo(() => resolver?.getFields(), [resolver])
|
41
48
|
|
42
49
|
const form = useFormContext()
|
43
50
|
|
44
51
|
React.useEffect(() => {
|
45
|
-
if (focusFirstField && fields[0]?.name) {
|
52
|
+
if (focusFirstField && fields?.[0]?.name) {
|
46
53
|
form.setFocus(fields[0].name)
|
47
54
|
}
|
48
55
|
}, [schema, fieldResolver, focusFirstField])
|
49
56
|
|
57
|
+
if (!resolver) {
|
58
|
+
return null
|
59
|
+
}
|
60
|
+
|
50
61
|
return (
|
51
62
|
<FormLayout {...props}>
|
52
|
-
{fields
|
63
|
+
{fields?.map(
|
53
64
|
({
|
54
65
|
name,
|
55
66
|
type,
|
56
67
|
defaultValue,
|
57
68
|
...fieldProps
|
58
|
-
}:
|
69
|
+
}: BaseFieldProps): React.ReactNode => {
|
59
70
|
if (type === 'array') {
|
60
71
|
return (
|
61
72
|
<ArrayField key={name} name={name} {...fieldProps}>
|
@@ -70,11 +81,13 @@ export const Fields: React.FC<FieldsProps> = ({
|
|
70
81
|
)
|
71
82
|
}
|
72
83
|
|
73
|
-
return
|
84
|
+
return (
|
85
|
+
<Field key={name} name={name} type={type as any} {...fieldProps} />
|
86
|
+
)
|
74
87
|
}
|
75
88
|
)}
|
76
89
|
</FormLayout>
|
77
90
|
)
|
78
91
|
}
|
79
92
|
|
80
|
-
|
93
|
+
AutoFields.displayName = 'Fields'
|
@@ -0,0 +1,84 @@
|
|
1
|
+
import { createContext, useContext } from 'react'
|
2
|
+
import {
|
3
|
+
FormProvider as HookFormProvider,
|
4
|
+
FormProviderProps as HookFormProviderProps,
|
5
|
+
useFormContext as useHookFormContext,
|
6
|
+
FieldValues,
|
7
|
+
} from 'react-hook-form'
|
8
|
+
import { FieldResolver } from './field-resolver'
|
9
|
+
import { BaseFieldProps, FieldProps } from './types'
|
10
|
+
|
11
|
+
export type FormContextValue<
|
12
|
+
TFieldValues extends FieldValues = FieldValues,
|
13
|
+
TContext = any,
|
14
|
+
TSchema = any
|
15
|
+
> = {
|
16
|
+
fieldResolver?: FieldResolver
|
17
|
+
schema?: TSchema
|
18
|
+
fields?: {
|
19
|
+
[key: string]: unknown
|
20
|
+
}
|
21
|
+
}
|
22
|
+
|
23
|
+
export type FormProviderProps<
|
24
|
+
TFieldValues extends FieldValues = FieldValues,
|
25
|
+
TContext = any,
|
26
|
+
TSchema = any
|
27
|
+
> = HookFormProviderProps<TFieldValues, TContext> & {
|
28
|
+
fieldResolver?: FieldResolver
|
29
|
+
schema?: TSchema
|
30
|
+
fields?: {
|
31
|
+
[key: string]: unknown
|
32
|
+
}
|
33
|
+
}
|
34
|
+
|
35
|
+
const FormContext = createContext<FormContextValue | null>(null)
|
36
|
+
|
37
|
+
export const useFormContext = <
|
38
|
+
TFieldValues extends FieldValues = FieldValues,
|
39
|
+
TContext = any,
|
40
|
+
TSchema = any
|
41
|
+
>() => {
|
42
|
+
const context = useContext(FormContext)
|
43
|
+
const hookContext = useHookFormContext()
|
44
|
+
|
45
|
+
if (!context) {
|
46
|
+
throw new Error('FormProvider must be used within a Form component')
|
47
|
+
}
|
48
|
+
|
49
|
+
return {
|
50
|
+
...hookContext,
|
51
|
+
...context,
|
52
|
+
}
|
53
|
+
}
|
54
|
+
|
55
|
+
export const useFieldProps = <TFieldValues extends FieldValues = FieldValues>(
|
56
|
+
name: string
|
57
|
+
): BaseFieldProps<TFieldValues> | undefined => {
|
58
|
+
const parsedName = name?.replace(/\.[0-9]/g, '.$')
|
59
|
+
const context = useFormContext()
|
60
|
+
return context?.fields?.[parsedName] as any
|
61
|
+
}
|
62
|
+
|
63
|
+
export type UseFormReturn<
|
64
|
+
TFieldValues extends FieldValues = FieldValues,
|
65
|
+
TContext = any,
|
66
|
+
TSchema = any
|
67
|
+
> = ReturnType<typeof useFormContext>
|
68
|
+
|
69
|
+
export const FormProvider = <
|
70
|
+
TFieldValues extends FieldValues = FieldValues,
|
71
|
+
TContext = any,
|
72
|
+
TSchema = any
|
73
|
+
>(
|
74
|
+
props: FormProviderProps<TFieldValues, TContext, TSchema>
|
75
|
+
) => {
|
76
|
+
const { children, fieldResolver, schema, fields, ...rest } = props
|
77
|
+
return (
|
78
|
+
<HookFormProvider {...rest}>
|
79
|
+
<FormContext.Provider value={{ fieldResolver, schema, fields }}>
|
80
|
+
{children}
|
81
|
+
</FormContext.Provider>
|
82
|
+
</HookFormProvider>
|
83
|
+
)
|
84
|
+
}
|