@saas-ui/forms 2.0.0-next.3 → 2.0.0-next.5
Sign up to get free protection for your applications and to get access to all the features.
- 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