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

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,100 +0,0 @@
1
- import React, { ForwardedRef, forwardRef, useMemo } from 'react'
2
-
3
- import { defaultFieldTypes } from './default-fields'
4
- import { objectFieldResolver } from './field-resolver'
5
- import { GetFieldResolver } from './field-resolver'
6
- import { FieldsProvider } from './fields-context'
7
- import { FieldValues, Form, FormProps, GetResolver } from './form'
8
- import { GetBaseField, WithFields } from './types'
9
-
10
- export interface CreateFormProps<
11
- FieldDefs,
12
- TGetBaseField extends GetBaseField = GetBaseField,
13
- > {
14
- resolver?: GetResolver
15
- fieldResolver?: GetFieldResolver
16
- fields?: FieldDefs extends Record<string, React.FC<any>> ? FieldDefs : never
17
- getBaseField?: TGetBaseField
18
- }
19
-
20
- export type FormType<
21
- FieldDefs,
22
- ExtraProps = object,
23
- ExtraFieldProps extends object = object,
24
- ExtraOverrides = object,
25
- > = (<
26
- TSchema = unknown,
27
- TFieldValues extends FieldValues = FieldValues,
28
- TContext extends object = object,
29
- >(
30
- props: WithFields<
31
- FormProps<TSchema, TFieldValues, TContext, ExtraFieldProps>,
32
- FieldDefs,
33
- ExtraOverrides
34
- > & {
35
- ref?: React.ForwardedRef<HTMLFormElement>
36
- } & ExtraProps,
37
- ) => React.ReactElement) & {
38
- displayName?: string
39
- id?: string
40
- }
41
-
42
- export function createForm<
43
- FieldDefs,
44
- TGetBaseField extends GetBaseField<any> = GetBaseField<any>,
45
- >({
46
- resolver,
47
- fieldResolver = objectFieldResolver,
48
- fields,
49
- getBaseField,
50
- }: CreateFormProps<FieldDefs, TGetBaseField> = {}) {
51
- type ExtraFieldProps =
52
- TGetBaseField extends GetBaseField<infer ExtraFieldProps>
53
- ? ExtraFieldProps
54
- : object
55
-
56
- const DefaultForm = forwardRef(
57
- <
58
- TSchema = any,
59
- TFieldValues extends FieldValues = FieldValues,
60
- TContext extends object = object,
61
- >(
62
- props: WithFields<
63
- FormProps<TSchema, TFieldValues, TContext, ExtraFieldProps>,
64
- FieldDefs
65
- >,
66
- ref: ForwardedRef<HTMLFormElement>,
67
- ) => {
68
- const {
69
- schema,
70
- resolver: resolverProp,
71
- fieldResolver: fieldResolverProp,
72
- ...rest
73
- } = props
74
-
75
- const fieldsContext = useMemo(
76
- () => ({
77
- fields: { ...defaultFieldTypes, ...fields },
78
- getBaseField,
79
- }),
80
- [fields, getBaseField],
81
- )
82
-
83
- return (
84
- <FieldsProvider value={fieldsContext}>
85
- <Form
86
- ref={ref}
87
- resolver={resolverProp ?? resolver?.(props.schema)}
88
- fieldResolver={fieldResolverProp ?? fieldResolver?.(schema)}
89
- {...rest}
90
- />
91
- </FieldsProvider>
92
- )
93
- },
94
- ) as FormType<FieldDefs, object, ExtraFieldProps>
95
-
96
- DefaultForm.displayName = 'Form'
97
- DefaultForm.id = 'Form'
98
-
99
- return DefaultForm
100
- }
@@ -1,118 +0,0 @@
1
- import React, { forwardRef, useMemo } from 'react'
2
-
3
- import { StepperProvider } from '@saas-ui/core'
4
- import { runIfFn } from '@saas-ui/core/utils'
5
-
6
- import {
7
- ArrayField,
8
- DisplayIf,
9
- FieldProps,
10
- FieldValues,
11
- FieldsProvider,
12
- GetFieldResolver,
13
- ObjectField,
14
- defaultFieldTypes,
15
- } from './'
16
- import { Field } from './field'
17
- import { Form } from './form'
18
- import { GetResolver } from './form'
19
- import { FormStep, StepsOptions } from './step-form'
20
- import { GetBaseField, WithStepFields } from './types'
21
- import {
22
- StepFormProvider,
23
- UseStepFormProps,
24
- useStepForm,
25
- } from './use-step-form'
26
-
27
- export type StepFormType<
28
- FieldDefs,
29
- ExtraProps = object,
30
- ExtraFieldProps extends object = object,
31
- ExtraOverrides = object,
32
- > = (<
33
- TSteps extends StepsOptions<any> = StepsOptions<any>,
34
- TFieldValues extends FieldValues = FieldValues,
35
- TContext extends object = object,
36
- TFieldTypes = FieldProps<TFieldValues, ExtraFieldProps>,
37
- >(
38
- props: WithStepFields<
39
- UseStepFormProps<TSteps, TFieldValues, TContext>,
40
- FieldDefs,
41
- ExtraOverrides
42
- > & {
43
- ref?: React.ForwardedRef<HTMLFormElement>
44
- } & ExtraProps,
45
- ) => React.ReactElement) & {
46
- displayName?: string
47
- id?: string
48
- }
49
-
50
- export interface CreateStepFormProps<
51
- FieldDefs,
52
- TGetBaseField extends GetBaseField = GetBaseField,
53
- > {
54
- resolver?: GetResolver
55
- fieldResolver?: GetFieldResolver
56
- fields?: FieldDefs extends Record<string, React.FC<any>> ? FieldDefs : never
57
- getBaseField?: TGetBaseField
58
- }
59
-
60
- export function createStepForm<
61
- FieldDefs,
62
- TGetBaseField extends GetBaseField<any> = GetBaseField<any>,
63
- >({
64
- fields,
65
- resolver,
66
- fieldResolver,
67
- getBaseField,
68
- }: CreateStepFormProps<FieldDefs, TGetBaseField> = {}) {
69
- type ExtraFieldProps =
70
- TGetBaseField extends GetBaseField<infer ExtraFieldProps>
71
- ? ExtraFieldProps
72
- : object
73
-
74
- const StepForm = forwardRef<HTMLFormElement, any>((props, ref) => {
75
- const { children, steps, ...rest } = props
76
-
77
- const stepper = useStepForm({
78
- // resolver: resolver(props),
79
- // fieldResolver: fieldResolver(,
80
- ...props,
81
- })
82
-
83
- const { getFormProps, ...ctx } = stepper
84
-
85
- const context = useMemo(() => ctx, [ctx])
86
-
87
- const fieldsContext = {
88
- fields: {
89
- ...defaultFieldTypes,
90
- ...fields,
91
- },
92
- getBaseField,
93
- }
94
-
95
- return (
96
- <StepperProvider value={context}>
97
- <StepFormProvider value={context}>
98
- <FieldsProvider value={fieldsContext}>
99
- <Form ref={ref} {...rest} {...getFormProps()}>
100
- {runIfFn(children, {
101
- ...stepper,
102
- Field: Field as any,
103
- FormStep: FormStep as any,
104
- DisplayIf: DisplayIf as any,
105
- ArrayField: ArrayField as any,
106
- ObjectField: ObjectField as any,
107
- })}
108
- </Form>
109
- </FieldsProvider>
110
- </StepFormProvider>
111
- </StepperProvider>
112
- )
113
- }) as StepFormType<FieldDefs, object, ExtraFieldProps>
114
-
115
- StepForm.displayName = `Step${Form.displayName || Form.name}`
116
-
117
- return StepForm
118
- }
@@ -1,233 +0,0 @@
1
- import React from 'react'
2
-
3
- import {
4
- Input,
5
- InputProps,
6
- Stack,
7
- type SystemStyleObject,
8
- Textarea,
9
- TextareaProps,
10
- createListCollection,
11
- } from '@chakra-ui/react'
12
- import { Checkbox, type CheckboxProps } from '@saas-ui/react/checkbox'
13
- import { InputGroup } from '@saas-ui/react/input-group'
14
- import { NumberInput, type NumberInputProps } from '@saas-ui/react/number-input'
15
- import {
16
- PasswordInput,
17
- type PasswordInputProps,
18
- } from '@saas-ui/react/password-input'
19
- import { PinInput, type PinInputProps } from '@saas-ui/react/pin-input'
20
- import { Radio, RadioGroup, type RadioGroupProps } from '@saas-ui/react/radio'
21
- import { Select } from '@saas-ui/react/select'
22
- import { Switch, type SwitchProps } from '@saas-ui/react/switch'
23
-
24
- import { createField } from './create-field.tsx'
25
- import type { FieldOption, FieldOptions } from './types.ts'
26
-
27
- export interface InputFieldProps extends InputProps {
28
- type?: string
29
- startElement?: React.ReactNode
30
- endElement?: React.ReactNode
31
- }
32
-
33
- export const InputField = createField<HTMLInputElement, InputFieldProps>(
34
- ({ type = 'text', startElement, endElement, size, ...rest }, ref) => {
35
- return (
36
- <InputGroup startElement={startElement} endElement={endElement}>
37
- <Input type={type} size={size} {...rest} ref={ref} />
38
- </InputGroup>
39
- )
40
- },
41
- )
42
-
43
- export interface NumberInputFieldProps extends NumberInputProps {
44
- type: 'number'
45
- }
46
-
47
- export const NumberInputField = createField<
48
- HTMLInputElement,
49
- NumberInputFieldProps
50
- >((props, ref) => <NumberInput {...props} ref={ref} />, {
51
- isControlled: true,
52
- })
53
-
54
- export const PasswordInputField = createField<
55
- HTMLInputElement,
56
- PasswordInputProps
57
- >(({ type = 'password', ...props }, ref) => (
58
- <PasswordInput ref={ref} {...props} />
59
- ))
60
-
61
- export interface TextareaFieldProps extends TextareaProps {}
62
-
63
- export const TextareaField = createField<
64
- HTMLTextAreaElement,
65
- TextareaFieldProps
66
- >((props, ref) => <Textarea {...props} ref={ref} />)
67
-
68
- export interface SwitchFieldProps extends SwitchProps {
69
- type: 'switch'
70
- }
71
-
72
- export const SwitchField = createField<HTMLInputElement, SwitchFieldProps>(
73
- ({ type, value, ...rest }, ref) => {
74
- return <Switch checked={!!value} {...rest} ref={ref} />
75
- },
76
- {
77
- isControlled: true,
78
- },
79
- )
80
-
81
- export interface SelectFieldProps
82
- extends Omit<Select.RootProps<FieldOption>, 'collection'> {
83
- options: FieldOptions
84
- placeholder?: string
85
- triggerProps?: Select.TriggerProps
86
- contentProps?: Select.ContentProps
87
- }
88
-
89
- export const SelectField = createField<HTMLDivElement, SelectFieldProps>(
90
- (props, ref) => {
91
- const {
92
- triggerProps,
93
- contentProps,
94
- options,
95
- placeholder,
96
- onChange,
97
- onValueChange,
98
- onBlur,
99
- ...rest
100
- } = props
101
-
102
- const collection = createListCollection({
103
- items: options,
104
- })
105
-
106
- return (
107
- <Select.Root
108
- ref={ref}
109
- collection={collection}
110
- onValueChange={(details) => {
111
- onChange(details.value)
112
- }}
113
- onInteractOutside={() => onBlur()}
114
- {...rest}
115
- >
116
- <Select.Trigger {...triggerProps}>
117
- <Select.ValueText placeholder={placeholder} />
118
- </Select.Trigger>
119
- <Select.Content {...contentProps}>
120
- {collection.items.map((option) => (
121
- <Select.Item key={option.value} item={option}>
122
- {option.label || option.value}
123
- </Select.Item>
124
- ))}
125
- </Select.Content>
126
- </Select.Root>
127
- )
128
- },
129
- {
130
- isControlled: true,
131
- },
132
- )
133
-
134
- export interface CheckboxFieldProps extends CheckboxProps {
135
- type: 'checkbox'
136
- label?: string
137
- }
138
-
139
- export const CheckboxField = createField<HTMLInputElement, CheckboxFieldProps>(
140
- ({ label, type, ...props }, ref) => {
141
- return (
142
- <Checkbox ref={ref} {...props}>
143
- {label}
144
- </Checkbox>
145
- )
146
- },
147
- {
148
- hideLabel: true,
149
- },
150
- )
151
-
152
- export interface RadioFieldProps extends RadioGroupProps {
153
- type: 'radio'
154
- options: FieldOptions
155
- flexDirection?: SystemStyleObject['flexDirection']
156
- gap?: SystemStyleObject['gap']
157
- }
158
-
159
- export const RadioField = createField<HTMLInputElement, RadioFieldProps>(
160
- (props, ref) => {
161
- const { options, onChange, flexDirection = 'column', gap, ...rest } = props
162
- return (
163
- <RadioGroup
164
- ref={ref}
165
- onValueChange={({ value }) => {
166
- onChange?.(value)
167
- }}
168
- {...rest}
169
- >
170
- <Stack flexDirection={flexDirection} gap={gap}>
171
- {options.map((option) => (
172
- <Radio key={option.value} value={option.value}>
173
- {option.label || option.value}
174
- </Radio>
175
- ))}
176
- </Stack>
177
- </RadioGroup>
178
- )
179
- },
180
- {
181
- isControlled: true,
182
- },
183
- )
184
-
185
- export interface PinFieldProps
186
- extends Omit<PinInputProps, 'type' | 'value' | 'onChange'> {
187
- type: 'pin'
188
- pinLength?: number
189
- pinType?: PinInputProps['type']
190
- value?: string
191
- onChange?: (value: string) => void
192
- }
193
-
194
- export const PinField = createField<HTMLInputElement, PinFieldProps>(
195
- (props, ref) => {
196
- const { pinType, value: valueProp, onChange, ...inputProps } = props
197
-
198
- const value = valueProp?.split('') || []
199
-
200
- return (
201
- <PinInput
202
- ref={ref}
203
- {...inputProps}
204
- value={value}
205
- onValueChange={(details) => {
206
- onChange(details.valueAsString)
207
- }}
208
- type={pinType}
209
- />
210
- )
211
- },
212
- {
213
- isControlled: true,
214
- },
215
- )
216
-
217
- export const defaultFieldTypes = {
218
- text: InputField,
219
- email: InputField,
220
- url: InputField,
221
- phone: InputField,
222
- time: InputField,
223
- number: NumberInputField,
224
- pin: PinField,
225
- checkbox: CheckboxField,
226
- radio: RadioField,
227
- password: PasswordInputField,
228
- select: SelectField,
229
- switch: SwitchField,
230
- textarea: TextareaField,
231
- }
232
-
233
- export type DefaultFields = typeof defaultFieldTypes
@@ -1,46 +0,0 @@
1
- import * as React from 'react'
2
-
3
- import { Field as FieldPrimivite, Text } from '@chakra-ui/react'
4
- import type { FieldPath, FieldValues } from 'react-hook-form'
5
-
6
- import { useFormContext } from './form-context.tsx'
7
- import type { ArrayFieldPath } from './types.ts'
8
-
9
- export interface DisplayFieldProps<
10
- TFieldValues extends FieldValues = FieldValues,
11
- TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
12
- > extends Omit<FieldPrimivite.RootProps, 'type' | 'onChange' | 'defaultValue'> {
13
- name: TName | ArrayFieldPath<TName>
14
- label?: string
15
- }
16
-
17
- /**
18
- * Display a field value.
19
- *
20
- * @see Docs https://saas-ui.dev/
21
- */
22
- export const DisplayField: React.FC<DisplayFieldProps> = ({
23
- name,
24
- label,
25
- ...props
26
- }) => {
27
- return (
28
- <FieldPrimivite.Root {...props}>
29
- {label ? (
30
- <FieldPrimivite.Label htmlFor={name}>{label}</FieldPrimivite.Label>
31
- ) : null}
32
- <Text fontSize="md">
33
- <FormValue name={name} />
34
- </Text>
35
- </FieldPrimivite.Root>
36
- )
37
- }
38
-
39
- DisplayField.displayName = 'DisplayField'
40
-
41
- export const FormValue: React.FC<{ name: string }> = ({ name }) => {
42
- const { getValues } = useFormContext()
43
- return getValues(name) || null
44
- }
45
-
46
- FormValue.displayName = 'FormValue'
@@ -1,69 +0,0 @@
1
- import * as React from 'react'
2
- import {
3
- useWatch,
4
- FieldValues,
5
- UseFormReturn,
6
- FieldPath,
7
- } from 'react-hook-form'
8
-
9
- import { useFormContext } from './form-context'
10
-
11
- export interface DisplayIfProps<
12
- TFieldValues extends FieldValues = FieldValues,
13
- TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
14
- > {
15
- children: React.ReactElement
16
- name: TName
17
- defaultValue?: unknown
18
- isDisabled?: boolean
19
- isExact?: boolean
20
- condition?: (value: unknown, context: UseFormReturn<TFieldValues>) => boolean
21
- onToggle?: (
22
- conditionMatched: boolean,
23
- context: UseFormReturn<TFieldValues>
24
- ) => void
25
- }
26
- /**
27
- * Conditionally render parts of a form.
28
- *
29
- * @see Docs https://saas-ui.dev/docs/components/forms/form
30
- */
31
- export const DisplayIf = <
32
- TFieldValues extends FieldValues = FieldValues,
33
- TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
34
- >({
35
- children,
36
- name,
37
- defaultValue,
38
- isDisabled,
39
- isExact,
40
- condition = (value) => !!value,
41
- onToggle,
42
- }: DisplayIfProps<TFieldValues, TName>) => {
43
- const initializedRef = React.useRef(false)
44
- const matchesRef = React.useRef(false)
45
-
46
- const value = useWatch<TFieldValues>({
47
- name,
48
- defaultValue: defaultValue as any,
49
- disabled: isDisabled,
50
- exact: isExact,
51
- })
52
- const context = useFormContext() as any
53
-
54
- const matches = condition(value, context)
55
-
56
- React.useEffect(() => {
57
- if (!initializedRef.current) {
58
- initializedRef.current = true
59
- return
60
- }
61
- if (matchesRef.current === matches) return
62
- matchesRef.current = matches
63
- onToggle?.(matches, context)
64
- }, [value])
65
-
66
- return matches ? children : null
67
- }
68
-
69
- DisplayIf.displayName = 'DisplayIf'
@@ -1,66 +0,0 @@
1
- import { get } from '@saas-ui/core/utils'
2
-
3
- import { ArrayFieldProps } from './array-field'
4
- import { DefaultFields } from './default-fields'
5
- import { ObjectFieldProps } from './object-field'
6
- import { BaseFieldProps, ValueOf } from './types'
7
-
8
- export type FieldResolver = {
9
- getFields(): BaseFieldProps[]
10
- getNestedFields(name: string): BaseFieldProps[]
11
- }
12
-
13
- export type GetFieldResolver<TSchema = any> = (schema: TSchema) => FieldResolver
14
-
15
- type FieldTypes<FieldDefs = DefaultFields> = ValueOf<{
16
- [K in keyof FieldDefs]: FieldDefs[K] extends React.FC<infer Props>
17
- ? { type?: K } & Omit<Props, 'name'>
18
- : never
19
- }>
20
-
21
- type SchemaField<FieldDefs = DefaultFields> =
22
- | FieldTypes<FieldDefs>
23
- | (Omit<ObjectFieldProps, 'name' | 'children'> & {
24
- type: 'object'
25
- properties?: Record<string, SchemaField<FieldDefs>>
26
- })
27
- | (Omit<ArrayFieldProps, 'name' | 'children'> & {
28
- type: 'array'
29
- items?: SchemaField<FieldDefs>
30
- })
31
-
32
- export type ObjectSchema<FieldDefs = DefaultFields> = Record<
33
- string,
34
- SchemaField<FieldDefs>
35
- >
36
-
37
- const mapFields = (schema: ObjectSchema): BaseFieldProps[] =>
38
- schema &&
39
- Object.entries(schema).map(([name, props]) => {
40
- const { items, label, title, ...field } = props as any
41
- return {
42
- ...field,
43
- name,
44
- label: label || title || name, // json schema compatibility
45
- }
46
- })
47
-
48
- export const objectFieldResolver: GetFieldResolver<ObjectSchema> = (schema) => {
49
- const getFields = (): BaseFieldProps[] => {
50
- return mapFields(schema)
51
- }
52
- const getNestedFields = (name: string): BaseFieldProps[] => {
53
- const field = get(schema, name)
54
-
55
- if (!field) return []
56
-
57
- if (field.items?.type === 'object') {
58
- return mapFields(field.items.properties)
59
- } else if (field.type === 'object') {
60
- return mapFields(field.properties)
61
- }
62
- return [field.items]
63
- }
64
-
65
- return { getFields, getNestedFields }
66
- }
package/src/field.tsx DELETED
@@ -1,44 +0,0 @@
1
- import * as React from 'react'
2
-
3
- import { FieldValues, RegisterOptions } from 'react-hook-form'
4
-
5
- import { InputField } from './default-fields'
6
- import { useField } from './fields-context'
7
- import { useFieldProps } from './form-context'
8
- import { FieldProps, type FocusableElement } from './types'
9
-
10
- export type FieldRules = Pick<
11
- RegisterOptions,
12
- 'required' | 'min' | 'max' | 'maxLength' | 'minLength' | 'pattern'
13
- >
14
-
15
- const defaultInputType = 'text'
16
-
17
- /**
18
- * Form field component.
19
- *
20
- * Build-in types:
21
- * text, number, password, textarea, select, native-select, checkbox, radio, switch, pin
22
- *
23
- * Will default to a text field if there is no matching type.
24
-
25
- * @see Docs https://saas-ui.dev/docs/components/forms/field
26
- */
27
- export const Field = React.forwardRef(
28
- <TFieldValues extends FieldValues = FieldValues>(
29
- props: FieldProps<TFieldValues>,
30
- ref: React.ForwardedRef<FocusableElement>,
31
- ) => {
32
- const { type = defaultInputType, name } = props
33
- const overrides = useFieldProps(name)
34
- const InputComponent = useField(overrides?.type || type, InputField)
35
-
36
- return <InputComponent ref={ref} {...props} {...overrides} />
37
- },
38
- ) as (<TFieldValues extends FieldValues>(
39
- props: FieldProps<TFieldValues> & {
40
- ref?: React.ForwardedRef<FocusableElement>
41
- },
42
- ) => React.ReactElement) & {
43
- displayName?: string
44
- }
@@ -1,33 +0,0 @@
1
- import React from 'react'
2
-
3
- import type { GetBaseField } from './types'
4
-
5
- export interface FieldsContextValue {
6
- fields: Record<string, React.FC<any>>
7
- getBaseField?: GetBaseField<any>
8
- }
9
-
10
- const FieldsContext = React.createContext<FieldsContextValue | null>(null)
11
-
12
- export const FieldsProvider: React.FC<{
13
- value: FieldsContextValue
14
- children: React.ReactNode
15
- }> = (props) => {
16
- return (
17
- <FieldsContext.Provider value={props.value}>
18
- {props.children}
19
- </FieldsContext.Provider>
20
- )
21
- }
22
-
23
- export const useFieldsContext = () => {
24
- return React.useContext(FieldsContext)
25
- }
26
-
27
- export const useField = (
28
- type: string,
29
- fallback: React.FC<any>,
30
- ): React.FC<any> => {
31
- const context = React.useContext(FieldsContext)
32
- return context?.fields?.[type] || fallback
33
- }