@saas-ui/forms 2.0.0-next.3 → 2.0.0-next.5
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 +30 -0
- package/README.md +53 -6
- package/dist/ajv/index.d.ts +1 -1
- package/dist/ajv/index.js.map +1 -1
- package/dist/ajv/index.mjs.map +1 -1
- package/dist/index.d.ts +265 -166
- package/dist/index.js +2821 -556
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2814 -555
- package/dist/index.mjs.map +1 -1
- package/dist/yup/index.d.ts +98 -6
- package/dist/yup/index.js.map +1 -1
- package/dist/yup/index.mjs.map +1 -1
- package/dist/zod/index.d.ts +97 -4
- package/dist/zod/index.js.map +1 -1
- package/dist/zod/index.mjs.map +1 -1
- package/package.json +5 -3
- package/src/array-field.tsx +50 -30
- package/src/auto-form.tsx +7 -3
- package/src/base-field.tsx +59 -0
- package/src/create-field.tsx +143 -0
- package/src/create-form.tsx +31 -0
- package/src/default-fields.tsx +146 -0
- package/src/display-field.tsx +8 -9
- package/src/display-if.tsx +6 -5
- package/src/field-resolver.ts +1 -1
- package/src/field.tsx +14 -444
- package/src/fields-context.tsx +23 -0
- package/src/fields.tsx +18 -8
- package/src/form.tsx +27 -37
- package/src/index.ts +38 -0
- 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 +8 -7
- package/src/password-input/password-input.stories.tsx +23 -2
- package/src/password-input/password-input.tsx +5 -5
- 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.test.tsx +1 -1
- package/src/select/select.tsx +18 -14
- package/src/step-form.tsx +29 -11
- package/src/submit-button.tsx +5 -1
- package/src/types.ts +91 -0
- package/src/utils.ts +15 -0
- /package/src/radio/{radio.test.tsx → radio-input.test.tsx} +0 -0
@@ -0,0 +1,59 @@
|
|
1
|
+
import * as React from 'react'
|
2
|
+
import {
|
3
|
+
useFormContext,
|
4
|
+
FormState,
|
5
|
+
get,
|
6
|
+
RegisterOptions,
|
7
|
+
FieldValues,
|
8
|
+
} from 'react-hook-form'
|
9
|
+
|
10
|
+
import {
|
11
|
+
Box,
|
12
|
+
FormControl,
|
13
|
+
FormLabel,
|
14
|
+
FormHelperText,
|
15
|
+
FormErrorMessage,
|
16
|
+
} from '@chakra-ui/react'
|
17
|
+
import { FocusableElement } from '@chakra-ui/utils'
|
18
|
+
import { useField } from './fields-context'
|
19
|
+
import { BaseFieldProps, FieldProps } from './types'
|
20
|
+
|
21
|
+
const getError = (name: string, formState: FormState<{ [x: string]: any }>) => {
|
22
|
+
return get(formState.errors, name)
|
23
|
+
}
|
24
|
+
|
25
|
+
const isTouched = (
|
26
|
+
name: string,
|
27
|
+
formState: FormState<{ [x: string]: any }>
|
28
|
+
) => {
|
29
|
+
return get(formState.touchedFields, name)
|
30
|
+
}
|
31
|
+
|
32
|
+
/**
|
33
|
+
* The default BaseField component
|
34
|
+
* Composes the Chakra UI FormControl component, with FormLabel, FormHelperText and FormErrorMessage.
|
35
|
+
*/
|
36
|
+
export const BaseField: React.FC<BaseFieldProps> = (props) => {
|
37
|
+
const { name, label, help, hideLabel, children, ...controlProps } = props
|
38
|
+
|
39
|
+
const { formState } = useFormContext()
|
40
|
+
|
41
|
+
const error = getError(name, formState)
|
42
|
+
|
43
|
+
return (
|
44
|
+
<FormControl {...controlProps} isInvalid={!!error}>
|
45
|
+
{label && !hideLabel ? <FormLabel>{label}</FormLabel> : null}
|
46
|
+
<Box>
|
47
|
+
{children}
|
48
|
+
{help && !error?.message ? (
|
49
|
+
<FormHelperText>{help}</FormHelperText>
|
50
|
+
) : null}
|
51
|
+
{error?.message && (
|
52
|
+
<FormErrorMessage>{error?.message}</FormErrorMessage>
|
53
|
+
)}
|
54
|
+
</Box>
|
55
|
+
</FormControl>
|
56
|
+
)
|
57
|
+
}
|
58
|
+
|
59
|
+
BaseField.displayName = 'BaseField'
|
@@ -0,0 +1,143 @@
|
|
1
|
+
import * as React from 'react'
|
2
|
+
import { useFormContext, Controller } from 'react-hook-form'
|
3
|
+
|
4
|
+
import { forwardRef, useMergeRefs } from '@chakra-ui/react'
|
5
|
+
import { callAllHandlers } from '@chakra-ui/utils'
|
6
|
+
import { BaseFieldProps, FieldProps } from './types'
|
7
|
+
import { BaseField } from './base-field'
|
8
|
+
|
9
|
+
interface CreateFieldProps {
|
10
|
+
displayName: string
|
11
|
+
hideLabel?: boolean
|
12
|
+
BaseField: React.FC<any>
|
13
|
+
}
|
14
|
+
|
15
|
+
const _createField = (
|
16
|
+
InputComponent: React.FC<any>,
|
17
|
+
{ displayName, hideLabel, BaseField }: CreateFieldProps
|
18
|
+
) => {
|
19
|
+
const Field = forwardRef((props, ref) => {
|
20
|
+
const {
|
21
|
+
id,
|
22
|
+
name,
|
23
|
+
label,
|
24
|
+
help,
|
25
|
+
isDisabled,
|
26
|
+
isInvalid,
|
27
|
+
isReadOnly,
|
28
|
+
isRequired,
|
29
|
+
rules,
|
30
|
+
...inputProps
|
31
|
+
} = props
|
32
|
+
|
33
|
+
const inputRules = {
|
34
|
+
required: isRequired,
|
35
|
+
...rules,
|
36
|
+
}
|
37
|
+
|
38
|
+
return (
|
39
|
+
<BaseField
|
40
|
+
id={id}
|
41
|
+
name={name}
|
42
|
+
label={label}
|
43
|
+
help={help}
|
44
|
+
hideLabel={hideLabel}
|
45
|
+
isDisabled={isDisabled}
|
46
|
+
isInvalid={isInvalid}
|
47
|
+
isReadOnly={isReadOnly}
|
48
|
+
isRequired={isRequired}
|
49
|
+
>
|
50
|
+
<InputComponent
|
51
|
+
ref={ref}
|
52
|
+
id={id}
|
53
|
+
name={name}
|
54
|
+
label={hideLabel ? label : undefined} // Only pass down the label when it should be inline.
|
55
|
+
rules={inputRules}
|
56
|
+
{...inputProps}
|
57
|
+
/>
|
58
|
+
</BaseField>
|
59
|
+
)
|
60
|
+
})
|
61
|
+
Field.displayName = displayName
|
62
|
+
|
63
|
+
return Field
|
64
|
+
}
|
65
|
+
|
66
|
+
const withControlledInput = (InputComponent: React.FC<any>) => {
|
67
|
+
return forwardRef<FieldProps, typeof InputComponent>(
|
68
|
+
({ name, rules, ...inputProps }, ref) => {
|
69
|
+
const { control } = useFormContext()
|
70
|
+
|
71
|
+
return (
|
72
|
+
<Controller
|
73
|
+
name={name}
|
74
|
+
control={control}
|
75
|
+
rules={rules}
|
76
|
+
render={({ field: { ref: _ref, ...field } }) => (
|
77
|
+
<InputComponent
|
78
|
+
{...field}
|
79
|
+
{...inputProps}
|
80
|
+
onChange={callAllHandlers(inputProps.onChange, field.onChange)}
|
81
|
+
onBlur={callAllHandlers(inputProps.onBlur, field.onBlur)}
|
82
|
+
ref={useMergeRefs(ref, _ref)}
|
83
|
+
/>
|
84
|
+
)}
|
85
|
+
/>
|
86
|
+
)
|
87
|
+
}
|
88
|
+
)
|
89
|
+
}
|
90
|
+
|
91
|
+
const withUncontrolledInput = (InputComponent: React.FC<any>) => {
|
92
|
+
return forwardRef<FieldProps, typeof InputComponent>(
|
93
|
+
({ name, rules, ...inputProps }, ref) => {
|
94
|
+
const { register } = useFormContext()
|
95
|
+
|
96
|
+
const { ref: _ref, ...field } = register(name, rules)
|
97
|
+
|
98
|
+
return (
|
99
|
+
<InputComponent
|
100
|
+
{...field}
|
101
|
+
{...inputProps}
|
102
|
+
onChange={callAllHandlers(inputProps.onChange, field.onChange)}
|
103
|
+
onBlur={callAllHandlers(inputProps.onBlur, field.onBlur)}
|
104
|
+
ref={useMergeRefs(ref, _ref)}
|
105
|
+
/>
|
106
|
+
)
|
107
|
+
}
|
108
|
+
)
|
109
|
+
}
|
110
|
+
|
111
|
+
export interface CreateFieldOptions {
|
112
|
+
isControlled?: boolean
|
113
|
+
hideLabel?: boolean
|
114
|
+
BaseField?: React.FC<any>
|
115
|
+
}
|
116
|
+
|
117
|
+
/**
|
118
|
+
* Register a new field type
|
119
|
+
* @param type The name for this field in kebab-case, eg `email` or `array-field`
|
120
|
+
* @param component The React component
|
121
|
+
* @param options
|
122
|
+
* @param options.isControlled Set this to true if this is a controlled field.
|
123
|
+
* @param options.hideLabel Hide the field label, for example for the checkbox field.
|
124
|
+
*/
|
125
|
+
export const createField = <TProps extends object>(
|
126
|
+
component: React.FC<TProps>,
|
127
|
+
options?: CreateFieldOptions
|
128
|
+
) => {
|
129
|
+
let InputComponent
|
130
|
+
if (options?.isControlled) {
|
131
|
+
InputComponent = withControlledInput(component)
|
132
|
+
} else {
|
133
|
+
InputComponent = withUncontrolledInput(component)
|
134
|
+
}
|
135
|
+
|
136
|
+
const Field = _createField(InputComponent, {
|
137
|
+
displayName: `${component.displayName ?? 'Custom'}Field`,
|
138
|
+
hideLabel: options?.hideLabel,
|
139
|
+
BaseField: options?.BaseField || BaseField,
|
140
|
+
}) as React.FC<TProps & BaseFieldProps>
|
141
|
+
|
142
|
+
return Field
|
143
|
+
}
|
@@ -0,0 +1,31 @@
|
|
1
|
+
import React from 'react'
|
2
|
+
import { FieldsProvider } from './fields-context'
|
3
|
+
import { Form, FieldValues, FormProps, GetResolver } from './form'
|
4
|
+
import { WithFields } from './types'
|
5
|
+
|
6
|
+
export interface CreateFormProps<FieldDefs> {
|
7
|
+
resolver?: GetResolver
|
8
|
+
fields?: FieldDefs extends Record<string, React.FC<any>> ? FieldDefs : never
|
9
|
+
}
|
10
|
+
|
11
|
+
export function createForm<FieldDefs, Schema = any>({
|
12
|
+
resolver,
|
13
|
+
fields,
|
14
|
+
}: CreateFormProps<FieldDefs> = {}) {
|
15
|
+
const CreateForm = <
|
16
|
+
TFieldValues extends FieldValues,
|
17
|
+
TContext extends object = object,
|
18
|
+
TSchema extends Schema = Schema
|
19
|
+
>(
|
20
|
+
props: WithFields<FormProps<TFieldValues, TContext, TSchema>, FieldDefs>
|
21
|
+
) => {
|
22
|
+
const { schema, ...rest } = props
|
23
|
+
return (
|
24
|
+
<FieldsProvider value={fields || {}}>
|
25
|
+
<Form resolver={resolver?.(props.schema)} {...rest} />
|
26
|
+
</FieldsProvider>
|
27
|
+
)
|
28
|
+
}
|
29
|
+
|
30
|
+
return CreateForm
|
31
|
+
}
|
@@ -0,0 +1,146 @@
|
|
1
|
+
import * as React from 'react'
|
2
|
+
|
3
|
+
import {
|
4
|
+
forwardRef,
|
5
|
+
Input,
|
6
|
+
Textarea,
|
7
|
+
Checkbox,
|
8
|
+
Switch,
|
9
|
+
InputGroup,
|
10
|
+
InputProps,
|
11
|
+
TextareaProps,
|
12
|
+
SwitchProps,
|
13
|
+
CheckboxProps,
|
14
|
+
PinInputField,
|
15
|
+
HStack,
|
16
|
+
PinInput,
|
17
|
+
UsePinInputProps,
|
18
|
+
SystemProps,
|
19
|
+
} from '@chakra-ui/react'
|
20
|
+
|
21
|
+
import { NumberInput, NumberInputProps } from './number-input'
|
22
|
+
import { PasswordInput, PasswordInputProps } from './password-input'
|
23
|
+
import { RadioInput, RadioInputProps } from './radio'
|
24
|
+
|
25
|
+
import { Select, SelectProps, NativeSelect, NativeSelectProps } from './select'
|
26
|
+
|
27
|
+
import { createField } from './create-field'
|
28
|
+
|
29
|
+
export interface InputFieldProps extends InputProps {
|
30
|
+
type?: string
|
31
|
+
leftAddon?: React.ReactNode
|
32
|
+
rightAddon?: React.ReactNode
|
33
|
+
}
|
34
|
+
|
35
|
+
export const InputField = createField<InputFieldProps>(
|
36
|
+
forwardRef(({ type = 'text', leftAddon, rightAddon, size, ...rest }, ref) => {
|
37
|
+
const input = <Input type={type} size={size} {...rest} ref={ref} />
|
38
|
+
if (leftAddon || rightAddon) {
|
39
|
+
return (
|
40
|
+
<InputGroup size={size}>
|
41
|
+
{leftAddon}
|
42
|
+
{input}
|
43
|
+
{rightAddon}
|
44
|
+
</InputGroup>
|
45
|
+
)
|
46
|
+
}
|
47
|
+
return input
|
48
|
+
})
|
49
|
+
)
|
50
|
+
|
51
|
+
export interface NumberInputFieldProps extends NumberInputProps {
|
52
|
+
type: 'number'
|
53
|
+
}
|
54
|
+
|
55
|
+
export const NumberInputField = createField<NumberInputFieldProps>(
|
56
|
+
NumberInput,
|
57
|
+
{
|
58
|
+
isControlled: true,
|
59
|
+
}
|
60
|
+
)
|
61
|
+
|
62
|
+
export const PasswordInputField = createField<PasswordInputProps>(
|
63
|
+
forwardRef((props, ref) => <PasswordInput ref={ref} {...props} />)
|
64
|
+
)
|
65
|
+
|
66
|
+
export const TextareaField = createField<TextareaProps>(Textarea)
|
67
|
+
|
68
|
+
export const SwitchField = createField<SwitchProps>(
|
69
|
+
forwardRef(({ type, value, ...rest }, ref) => {
|
70
|
+
return <Switch isChecked={!!value} {...rest} ref={ref} />
|
71
|
+
}),
|
72
|
+
{
|
73
|
+
isControlled: true,
|
74
|
+
}
|
75
|
+
)
|
76
|
+
|
77
|
+
export const SelectField = createField<SelectProps>(Select, {
|
78
|
+
isControlled: true,
|
79
|
+
})
|
80
|
+
|
81
|
+
export const CheckboxField = createField<CheckboxProps>(
|
82
|
+
forwardRef(({ label, type, ...props }, ref) => {
|
83
|
+
return (
|
84
|
+
<Checkbox ref={ref} {...props}>
|
85
|
+
{label}
|
86
|
+
</Checkbox>
|
87
|
+
)
|
88
|
+
}),
|
89
|
+
{
|
90
|
+
hideLabel: true,
|
91
|
+
}
|
92
|
+
)
|
93
|
+
|
94
|
+
export const RadioField = createField<RadioInputProps>(RadioInput, {
|
95
|
+
isControlled: true,
|
96
|
+
})
|
97
|
+
|
98
|
+
export const NativeSelectField = createField<NativeSelectProps>(NativeSelect, {
|
99
|
+
isControlled: true,
|
100
|
+
})
|
101
|
+
|
102
|
+
export interface PinFieldProps extends Omit<UsePinInputProps, 'type'> {
|
103
|
+
pinLength?: number
|
104
|
+
pinType?: 'alphanumeric' | 'number'
|
105
|
+
spacing?: SystemProps['margin']
|
106
|
+
}
|
107
|
+
|
108
|
+
export const PinField = createField<PinFieldProps>(
|
109
|
+
forwardRef((props, ref) => {
|
110
|
+
const { pinLength = 4, pinType, spacing, ...inputProps } = props
|
111
|
+
|
112
|
+
const inputs: React.ReactNode[] = []
|
113
|
+
for (let i = 0; i < pinLength; i++) {
|
114
|
+
inputs.push(<PinInputField key={i} ref={ref} />)
|
115
|
+
}
|
116
|
+
|
117
|
+
return (
|
118
|
+
<HStack spacing={spacing}>
|
119
|
+
<PinInput {...inputProps} type={pinType}>
|
120
|
+
{inputs}
|
121
|
+
</PinInput>
|
122
|
+
</HStack>
|
123
|
+
)
|
124
|
+
}),
|
125
|
+
{
|
126
|
+
isControlled: true,
|
127
|
+
}
|
128
|
+
)
|
129
|
+
|
130
|
+
export const defaultFieldTypes = {
|
131
|
+
text: InputField,
|
132
|
+
email: InputField,
|
133
|
+
url: InputField,
|
134
|
+
phone: InputField,
|
135
|
+
number: NumberInputField,
|
136
|
+
password: PasswordInputField,
|
137
|
+
textarea: TextareaField,
|
138
|
+
switch: SwitchField,
|
139
|
+
select: SelectField,
|
140
|
+
checkbox: CheckboxField,
|
141
|
+
radio: RadioField,
|
142
|
+
pin: PinField,
|
143
|
+
'native-select': NativeSelectField,
|
144
|
+
}
|
145
|
+
|
146
|
+
export type DefaultFields = typeof defaultFieldTypes
|
package/src/display-field.tsx
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
import * as React from 'react'
|
2
|
-
import { __DEV__ } from '@chakra-ui/utils'
|
3
2
|
import { useFormContext } from 'react-hook-form'
|
4
3
|
|
5
4
|
import {
|
@@ -9,12 +8,16 @@ import {
|
|
9
8
|
FormLabel,
|
10
9
|
} from '@chakra-ui/react'
|
11
10
|
|
12
|
-
import { FieldProps } from './
|
11
|
+
import { FieldProps } from './types'
|
13
12
|
|
14
13
|
export interface DisplayFieldProps
|
15
14
|
extends FormControlProps,
|
16
15
|
Omit<FieldProps, 'type' | 'label'> {}
|
17
|
-
|
16
|
+
/**
|
17
|
+
*
|
18
|
+
*
|
19
|
+
* @see Docs https://saas-ui.dev/
|
20
|
+
*/
|
18
21
|
export const DisplayField: React.FC<DisplayFieldProps> = ({
|
19
22
|
name,
|
20
23
|
label,
|
@@ -31,15 +34,11 @@ export const DisplayField: React.FC<DisplayFieldProps> = ({
|
|
31
34
|
)
|
32
35
|
}
|
33
36
|
|
34
|
-
|
35
|
-
DisplayField.displayName = 'DisplayField'
|
36
|
-
}
|
37
|
+
DisplayField.displayName = 'DisplayField'
|
37
38
|
|
38
39
|
export const FormValue: React.FC<{ name: string }> = ({ name }) => {
|
39
40
|
const { getValues } = useFormContext()
|
40
41
|
return getValues(name) || null
|
41
42
|
}
|
42
43
|
|
43
|
-
|
44
|
-
FormValue.displayName = 'FormValue'
|
45
|
-
}
|
44
|
+
FormValue.displayName = 'FormValue'
|
package/src/display-if.tsx
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
import * as React from 'react'
|
2
|
-
import { __DEV__ } from '@chakra-ui/utils'
|
3
2
|
import {
|
4
3
|
useFormContext,
|
5
4
|
useWatch,
|
@@ -17,7 +16,11 @@ export interface DisplayIfProps<
|
|
17
16
|
isExact?: boolean
|
18
17
|
condition?: (value: unknown, context: UseFormReturn<TFieldValues>) => boolean
|
19
18
|
}
|
20
|
-
|
19
|
+
/**
|
20
|
+
* Conditionally render parts of a form.
|
21
|
+
*
|
22
|
+
* @see Docs https://saas-ui.dev/docs/components/forms/form
|
23
|
+
*/
|
21
24
|
export const DisplayIf = <TFieldValues extends FieldValues = FieldValues>({
|
22
25
|
children,
|
23
26
|
name,
|
@@ -36,6 +39,4 @@ export const DisplayIf = <TFieldValues extends FieldValues = FieldValues>({
|
|
36
39
|
return condition(value, context) ? children : null
|
37
40
|
}
|
38
41
|
|
39
|
-
|
40
|
-
DisplayIf.displayName = 'DisplayIf'
|
41
|
-
}
|
42
|
+
DisplayIf.displayName = 'DisplayIf'
|
package/src/field-resolver.ts
CHANGED