@saas-ui/forms 3.0.0-alpha.1 → 3.0.0-alpha.2

Sign up to get free protection for your applications and to get access to all the features.
package/src/types.ts DELETED
@@ -1,242 +0,0 @@
1
- import type { MaybeRenderProp } from '@saas-ui/core/utils'
2
- import type { FieldPath, FieldValues, RegisterOptions } from 'react-hook-form'
3
-
4
- import type { ArrayFieldProps } from './array-field'
5
- import type { DefaultFields } from './default-fields'
6
- import type { FormProps, FormRenderContext } from './form'
7
- import type { ObjectFieldProps } from './object-field'
8
- import type { StepsOptions } from './step-form'
9
- import type { SubmitButtonProps } from './submit-button'
10
- import type { StepFormRenderContext, UseStepFormProps } from './use-step-form'
11
-
12
- export interface FocusableElement {
13
- focus(options?: FocusOptions): void
14
- }
15
-
16
- export type FieldOption = { label?: string; value: string }
17
- export type FieldOptions<TOption extends FieldOption = FieldOption> =
18
- Array<TOption>
19
-
20
- export type ValueOf<T> = T[keyof T]
21
- export type ShallowMerge<A, B> = Omit<A, keyof B> & B
22
-
23
- type Split<S extends string, D extends string> = string extends S
24
- ? string[]
25
- : S extends ''
26
- ? []
27
- : S extends `${infer T}${D}${infer U}`
28
- ? [T, ...Split<U, D>]
29
- : [S]
30
-
31
- type MapPath<T extends string[]> = T extends [infer U, ...infer R]
32
- ? U extends string
33
- ? `${U extends `${number}` ? '$' : U}${R[0] extends string
34
- ? '.'
35
- : ''}${R extends string[] ? MapPath<R> : ''}`
36
- : ''
37
- : ''
38
-
39
- type TransformPath<T extends string> = MapPath<Split<T, '.'>>
40
-
41
- export type ArrayFieldPath<Name extends string> = Name extends string
42
- ? TransformPath<Name>
43
- : never
44
-
45
- export interface BaseFieldProps<
46
- TFieldValues extends FieldValues = FieldValues,
47
- TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
48
- > {
49
- /**
50
- * The field name
51
- */
52
- name: TName | ArrayFieldPath<TName>
53
- /**
54
- * The field label
55
- */
56
- label?: string
57
- /**
58
- * Hide the field label
59
- */
60
- hideLabel?: boolean
61
- /**
62
- * Field help text
63
- */
64
- help?: string
65
- /**
66
- * React hook form rules
67
- */
68
- rules?: Omit<
69
- RegisterOptions<TFieldValues, TName>,
70
- 'valueAsNumber' | 'valueAsDate' | 'setValueAs' | 'disabled'
71
- >
72
- /**
73
- * Build-in types:
74
- * text, number, password, textarea, select, native-select, checkbox, radio, switch, pin
75
- *
76
- * Will default to a text field if there is no matching type.
77
- */
78
- type?: string
79
- /**
80
- * The input placeholder
81
- */
82
- placeholder?: string
83
- /**
84
- * React children
85
- */
86
- children?: React.ReactNode
87
- }
88
-
89
- export type GetBaseField<TProps extends object = object> = () => {
90
- extraProps: string[]
91
- BaseField: React.FC<
92
- Omit<BaseFieldProps, 'name'> & {
93
- name: string
94
- children: React.ReactNode
95
- } & TProps
96
- >
97
- }
98
-
99
- type FieldPathWithArray<
100
- TFieldValues extends FieldValues,
101
- TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
102
- > = TName | ArrayFieldPath<TName>
103
-
104
- export type MergeFieldProps<
105
- FieldDefs,
106
- TFieldValues extends FieldValues = FieldValues,
107
- TExtraFieldProps extends object = object,
108
- TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
109
- > = ValueOf<{
110
- [K in keyof FieldDefs]: FieldDefs[K] extends React.FC<infer Props>
111
- ? { type?: K } & ShallowMerge<
112
- Props,
113
- BaseFieldProps<TFieldValues, TName>
114
- > & { [key in keyof TExtraFieldProps]: TExtraFieldProps[key] }
115
- : never
116
- }>
117
-
118
- export type FieldProps<
119
- TFieldValues extends FieldValues = FieldValues,
120
- TExtraFieldProps extends object = object,
121
- > = MergeFieldProps<DefaultFields, TFieldValues, TExtraFieldProps>
122
-
123
- export type FormChildren<
124
- FieldDefs,
125
- TFieldValues extends FieldValues = FieldValues,
126
- TContext extends object = object,
127
- TExtraFieldProps extends object = object,
128
- > = MaybeRenderProp<
129
- FormRenderContext<
130
- TFieldValues,
131
- TContext,
132
- MergeFieldProps<
133
- FieldDefs extends never
134
- ? DefaultFields
135
- : ShallowMerge<DefaultFields, FieldDefs>,
136
- TFieldValues,
137
- TExtraFieldProps
138
- >
139
- >
140
- >
141
-
142
- export type DefaultFieldOverrides = {
143
- submit?: SubmitButtonProps
144
- [key: string]: any
145
- }
146
-
147
- type MergeOverrideFieldProps<
148
- FieldDefs,
149
- TFieldValues extends FieldValues = FieldValues,
150
- TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
151
- > = ValueOf<{
152
- [K in keyof FieldDefs]: FieldDefs[K] extends React.FC<infer Props>
153
- ? { type?: K } & Omit<
154
- ShallowMerge<Props, BaseFieldProps<TFieldValues, TName>>,
155
- 'name'
156
- >
157
- : never
158
- }>
159
-
160
- export type FieldOverrides<
161
- FieldDefs,
162
- TFieldValues extends FieldValues = FieldValues,
163
- TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
164
- > = {
165
- [K in FieldPathWithArray<TFieldValues, TName>]?:
166
- | MergeOverrideFieldProps<
167
- FieldDefs extends never
168
- ? DefaultFields
169
- : ShallowMerge<DefaultFields, FieldDefs>,
170
- TFieldValues
171
- >
172
- | ({ type?: 'object' } & Omit<
173
- ObjectFieldProps<TFieldValues>,
174
- 'name' | 'children'
175
- >)
176
- | ({ type?: 'array' } & Omit<
177
- ArrayFieldProps<TFieldValues>,
178
- 'name' | 'children'
179
- >)
180
- }
181
-
182
- export type WithFields<
183
- TFormProps extends FormProps<any, any, any, any, any>,
184
- FieldDefs,
185
- ExtraOverrides = object,
186
- > =
187
- TFormProps extends FormProps<
188
- infer _TSchema,
189
- infer TFieldValues,
190
- infer TContext,
191
- infer TExtraFieldProps
192
- >
193
- ? Omit<TFormProps, 'children' | 'fields'> & {
194
- children?: FormChildren<
195
- FieldDefs,
196
- TFieldValues,
197
- TContext,
198
- TExtraFieldProps
199
- >
200
- fields?: FieldOverrides<FieldDefs, TFieldValues> & {
201
- submit?: SubmitButtonProps
202
- } & ExtraOverrides
203
- }
204
- : never
205
-
206
- // StepForm types
207
- export type StepFormChildren<
208
- FieldDefs,
209
- TSteps extends StepsOptions<any> = StepsOptions<any>,
210
- TFieldValues extends FieldValues = FieldValues,
211
- TContext extends object = object,
212
- > = MaybeRenderProp<
213
- StepFormRenderContext<
214
- TSteps,
215
- TFieldValues,
216
- TContext,
217
- MergeFieldProps<
218
- FieldDefs extends never
219
- ? DefaultFields
220
- : ShallowMerge<DefaultFields, FieldDefs>,
221
- TFieldValues
222
- >
223
- >
224
- >
225
-
226
- export type WithStepFields<
227
- TStepFormProps extends UseStepFormProps<any, any, any>,
228
- FieldDefs,
229
- ExtraOverrides = object,
230
- > =
231
- TStepFormProps extends UseStepFormProps<
232
- infer TSteps,
233
- infer TFieldValues,
234
- infer TContext
235
- >
236
- ? Omit<TStepFormProps, 'children' | 'fields'> & {
237
- children?: StepFormChildren<FieldDefs, TSteps, TFieldValues, TContext>
238
- fields?: FieldOverrides<FieldDefs, TFieldValues> & {
239
- submit?: SubmitButtonProps
240
- } & ExtraOverrides
241
- }
242
- : never
@@ -1,158 +0,0 @@
1
- import * as React from 'react'
2
-
3
- import { createContext } from '@saas-ui/core/utils'
4
- import {
5
- FieldPath,
6
- FieldValues,
7
- UseFieldArrayReturn,
8
- useFieldArray,
9
- } from 'react-hook-form'
10
-
11
- import { useFormContext } from './form-context'
12
-
13
- export interface UseArrayFieldReturn extends UseFieldArrayReturn {
14
- /**
15
- * The array field name
16
- */
17
- name: string
18
- /**
19
- * The default value for new items
20
- */
21
- defaultValue: Record<string, any>
22
- /**
23
- * Min amount of items
24
- */
25
- min?: number
26
- /**
27
- * Max amount of items
28
- */
29
- max?: number
30
- }
31
-
32
- export const [ArrayFieldProvider, useArrayFieldContext] =
33
- createContext<UseArrayFieldReturn>({
34
- name: 'ArrayFieldContext',
35
- })
36
-
37
- export interface UseArrayFieldRowReturn {
38
- /**
39
- * Name of the array field including the index, eg 'field.0'
40
- */
41
- name: string
42
- /**
43
- * The field index
44
- */
45
- index: number
46
- /**
47
- * Remove this array item
48
- */
49
- remove: () => void
50
- /**
51
- * True if this is the first item
52
- */
53
- isFirst: boolean
54
- /**
55
- * True if this is the last item
56
- */
57
- isLast: boolean
58
- }
59
-
60
- export const [ArrayFieldRowProvider, useArrayFieldRowContext] =
61
- createContext<UseArrayFieldRowReturn>({
62
- name: 'ArrayFieldRowContext',
63
- })
64
-
65
- export interface ArrayFieldOptions<
66
- TFieldValues extends FieldValues = FieldValues,
67
- TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
68
- > {
69
- /**
70
- * The field name
71
- */
72
- name: TName
73
- /**
74
- * Default value for new values in the array
75
- */
76
- defaultValue?: Record<string, any>
77
- /**
78
- * Default key name for rows, change this if your data uses a different 'id' field
79
- * @default "id"
80
- */
81
- keyName?: string
82
- min?: number
83
- max?: number
84
- }
85
-
86
- export const useArrayField = ({
87
- name,
88
- defaultValue = {},
89
- keyName,
90
- min,
91
- max,
92
- }: ArrayFieldOptions) => {
93
- const { control } = useFormContext()
94
-
95
- const context = useFieldArray({
96
- control,
97
- name,
98
- keyName,
99
- })
100
- return {
101
- ...context,
102
- name,
103
- defaultValue,
104
- min,
105
- max,
106
- }
107
- }
108
-
109
- export interface UseArrayFieldRowProps {
110
- index: number
111
- }
112
-
113
- export const useArrayFieldRow = ({ index }: UseArrayFieldRowProps) => {
114
- const { clearErrors } = useFormContext()
115
- const { name, remove, fields } = useArrayFieldContext()
116
-
117
- React.useEffect(() => {
118
- // reset errors, to make sure min/max errors reset correctly
119
- clearErrors(name)
120
- }, [])
121
-
122
- return {
123
- index,
124
- isFirst: index === 0,
125
- isLast: index === fields.length - 1,
126
- name: `${name}.${index}`,
127
- remove: React.useCallback(() => {
128
- clearErrors(name)
129
- remove(index)
130
- }, [index]),
131
- }
132
- }
133
-
134
- export const useArrayFieldRemoveButton = () => {
135
- const { isFirst, remove } = useArrayFieldRowContext()
136
- const { min, fields } = useArrayFieldContext()
137
-
138
- const isDisabled = isFirst && !!(min && fields.length <= min)
139
-
140
- return {
141
- onClick: () => remove(),
142
- isDisabled,
143
- }
144
- }
145
-
146
- export const useArrayFieldAddButton = () => {
147
- const { append, defaultValue, max, fields } = useArrayFieldContext()
148
-
149
- const isDisabled = !!(max && fields.length >= max)
150
-
151
- return {
152
- onClick: () =>
153
- append(defaultValue, {
154
- shouldFocus: false,
155
- }),
156
- isDisabled,
157
- }
158
- }
package/src/use-form.tsx DELETED
@@ -1,24 +0,0 @@
1
- import {
2
- type FieldValues,
3
- UseFormProps as UseHookFormProps,
4
- useForm as useHookForm,
5
- } from 'react-hook-form'
6
-
7
- import { Field } from './field.tsx'
8
-
9
- export interface UseFormProps<
10
- TFieldValues extends FieldValues,
11
- TContext extends object,
12
- > extends UseHookFormProps<TFieldValues, TContext> {}
13
-
14
- export function useForm<
15
- TFieldValues extends FieldValues,
16
- TContext extends object,
17
- >(props: UseFormProps<TFieldValues, TContext> = {}) {
18
- const form = useHookForm<TFieldValues, TContext>(props)
19
-
20
- return {
21
- ...form,
22
- Field,
23
- }
24
- }
@@ -1,201 +0,0 @@
1
- import * as React from 'react'
2
-
3
- import { createContext } from '@chakra-ui/react'
4
- import {
5
- type UseStepperProps,
6
- type UseStepperReturn,
7
- useStep,
8
- useStepper,
9
- } from '@saas-ui/core'
10
- import { FieldValues, SubmitHandler } from 'react-hook-form'
11
-
12
- import { ArrayFieldProps } from './array-field'
13
- import { DisplayIfProps } from './display-if'
14
- import { FormProps } from './form'
15
- import { ObjectFieldProps } from './object-field'
16
- import { FormStepProps, StepsOptions } from './step-form'
17
- import { FieldProps, type FocusableElement, StepFormChildren } from './types'
18
- import { UseArrayFieldReturn } from './use-array-field'
19
-
20
- export interface StepState {
21
- name: string
22
- schema?: any
23
- resolver?: any
24
- isActive?: boolean
25
- isCompleted?: boolean
26
- onSubmit?: FormStepSubmitHandler
27
- }
28
-
29
- export type FormStepSubmitHandler<
30
- TFieldValues extends FieldValues = FieldValues,
31
- > = (data: TFieldValues, stepper: UseStepperReturn) => Promise<void>
32
-
33
- export interface StepFormContext extends UseStepperReturn {
34
- updateStep(state: StepState): void
35
- steps: Record<string, StepState>
36
- }
37
-
38
- export const [StepFormProvider, useStepFormContext] =
39
- createContext<StepFormContext>({
40
- name: 'StepFormContext',
41
- errorMessage:
42
- 'useStepFormContext: `context` is undefined. Seems you forgot to wrap step form components in `<StepForm />`',
43
- })
44
-
45
- type StepName<T extends { [k: number]: { readonly name: string } }> =
46
- T[number]['name']
47
-
48
- export interface StepFormRenderContext<
49
- TSteps extends StepsOptions<any> = StepsOptions<any>,
50
- TFieldValues extends FieldValues = FieldValues,
51
- TContext extends object = object,
52
- TFieldTypes = FieldProps<TFieldValues>,
53
- > extends UseStepFormReturn<TFieldValues> {
54
- Field: React.FC<TFieldTypes & React.RefAttributes<FocusableElement>>
55
- FormStep: React.FC<FormStepProps<StepName<TSteps>>>
56
- DisplayIf: React.FC<DisplayIfProps<TFieldValues>>
57
- ArrayField: React.FC<
58
- ArrayFieldProps<TFieldValues> & React.RefAttributes<UseArrayFieldReturn>
59
- >
60
- ObjectField: React.FC<ObjectFieldProps<TFieldValues>>
61
- }
62
-
63
- export interface UseStepFormProps<
64
- TSteps extends StepsOptions<any> = StepsOptions<any>,
65
- TFieldValues extends FieldValues = FieldValues,
66
- TContext extends object = object,
67
- > extends Omit<UseStepperProps, 'onChange'>,
68
- Omit<FormProps<any, TFieldValues, TContext>, 'children'> {
69
- steps?: TSteps
70
- children: StepFormChildren<any, TSteps, TFieldValues, TContext>
71
- }
72
-
73
- export interface UseStepFormReturn<
74
- TFieldValues extends FieldValues = FieldValues,
75
- > extends UseStepperReturn {
76
- getFormProps(): {
77
- onSubmit: SubmitHandler<TFieldValues>
78
- schema?: any
79
- resolver?: any
80
- }
81
- updateStep(step: any): void
82
- steps: Record<string, any>
83
- }
84
-
85
- export function useStepForm<
86
- TSteps extends StepsOptions<any> = StepsOptions<any>,
87
- TFieldValues extends FieldValues = FieldValues,
88
- TContext extends object = object,
89
- >(
90
- props: UseStepFormProps<TSteps, TFieldValues, TContext>,
91
- ): UseStepFormReturn<TFieldValues> {
92
- const {
93
- onChange,
94
- steps: stepsOptions,
95
- resolver,
96
- fieldResolver,
97
- ...rest
98
- } = props
99
- const stepper = useStepper(rest)
100
-
101
- const [options] = React.useState<TSteps | undefined>(stepsOptions)
102
-
103
- const { activeStep, isLastStep, nextStep } = stepper
104
-
105
- const [steps, updateSteps] = React.useState<Record<string, StepState>>({})
106
-
107
- const mergedData = React.useRef<TFieldValues>({} as any)
108
-
109
- const onSubmitStep: SubmitHandler<TFieldValues> = React.useCallback(
110
- async (data) => {
111
- try {
112
- const step = steps[activeStep]
113
-
114
- mergedData.current = {
115
- ...mergedData.current,
116
- ...data,
117
- }
118
-
119
- if (isLastStep) {
120
- await props.onSubmit?.(mergedData.current)
121
-
122
- updateStep({
123
- ...step,
124
- isCompleted: true,
125
- })
126
-
127
- nextStep() // show completed step
128
- return
129
- }
130
-
131
- await step.onSubmit?.(data, stepper)
132
-
133
- nextStep()
134
- } catch (e) {
135
- // Step submission failed.
136
- }
137
- },
138
- [steps, activeStep, isLastStep, mergedData],
139
- )
140
-
141
- const getFormProps = React.useCallback(() => {
142
- const step = steps[activeStep]
143
-
144
- return {
145
- onSubmit: onSubmitStep,
146
- schema: step?.schema,
147
- resolver: step?.schema
148
- ? /* @todo fix resolver type */ (resolver as any)?.(step.schema)
149
- : undefined,
150
- fieldResolver: step?.schema
151
- ? (fieldResolver as any)?.(step.schema)
152
- : undefined,
153
- }
154
- }, [steps, onSubmitStep, activeStep, resolver, fieldResolver])
155
-
156
- const updateStep = React.useCallback(
157
- (step: StepState) => {
158
- const stepOptions = options?.find((s) => s.name === step.name)
159
- updateSteps((steps) => {
160
- return {
161
- ...steps,
162
- [step.name]: {
163
- ...step,
164
- schema: stepOptions?.schema,
165
- },
166
- }
167
- })
168
- },
169
- [steps, options],
170
- )
171
-
172
- return {
173
- getFormProps,
174
- updateStep,
175
- steps,
176
- ...stepper,
177
- }
178
- }
179
-
180
- export interface UseFormStepProps {
181
- name: string
182
- schema?: any
183
- resolver?: any
184
- onSubmit?: FormStepSubmitHandler
185
- }
186
-
187
- export function useFormStep(props: UseFormStepProps): StepState {
188
- const { name, schema, resolver, onSubmit } = props
189
- const step = useStep({ name })
190
-
191
- const { steps, updateStep } = useStepFormContext()
192
-
193
- React.useEffect(() => {
194
- updateStep({ name, schema, resolver, onSubmit })
195
- }, [name, schema])
196
-
197
- return {
198
- ...step,
199
- ...(steps[name] || { name, schema }),
200
- }
201
- }
package/src/utils.ts DELETED
@@ -1,35 +0,0 @@
1
- import * as React from 'react'
2
- import { FieldOption, FieldOptions } from './types'
3
-
4
- export const mapNestedFields = (name: string, children: React.ReactNode) => {
5
- return React.Children.map(children, (child) => {
6
- if (React.isValidElement(child) && child.props.name) {
7
- let childName = child.props.name
8
- if (childName.includes('.')) {
9
- childName = childName.replace(/^.*\.(.*)/, '$1')
10
- } else if (childName.includes('.$')) {
11
- childName = childName.replace(/^.*\.\$(.*)/, '$1')
12
- }
13
-
14
- return React.cloneElement(child, {
15
- ...child.props,
16
- name: `${name}.${childName}`,
17
- })
18
- }
19
- return child
20
- })
21
- }
22
-
23
- export const mapOptions = <TOption extends FieldOption = FieldOption>(
24
- options: FieldOptions<TOption>
25
- ) => {
26
- return options.map((option) => {
27
- if (typeof option === 'string') {
28
- return {
29
- label: option,
30
- value: option,
31
- }
32
- }
33
- return option
34
- })
35
- }
@@ -1,37 +0,0 @@
1
- import { FieldValues, useWatch } from 'react-hook-form'
2
- import { useFormContext, UseFormReturn } from './form-context'
3
-
4
- export interface WatchFieldProps<
5
- Value = unknown,
6
- TFieldValues extends FieldValues = FieldValues,
7
- TContext extends object = object,
8
- > {
9
- name: string
10
- defaultValue?: Value
11
- isDisabled?: boolean
12
- isExact?: boolean
13
- children: (
14
- value: Value,
15
- form: UseFormReturn<TFieldValues, TContext>
16
- ) => React.ReactElement | void
17
- }
18
-
19
- export const WatchField = <
20
- Value = unknown,
21
- TFieldValues extends FieldValues = FieldValues,
22
- TContext extends object = object,
23
- >(
24
- props: WatchFieldProps<Value, TFieldValues, TContext>
25
- ) => {
26
- const { name, defaultValue, isDisabled, isExact } = props
27
- const form = useFormContext<TFieldValues, TContext>()
28
-
29
- const field = useWatch({
30
- name,
31
- defaultValue,
32
- disabled: isDisabled,
33
- exact: isExact,
34
- })
35
-
36
- return props.children(field, form) || null
37
- }