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

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/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
- }