@saas-ui/forms 2.0.0-next.2 → 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.
Files changed (49) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +53 -6
  3. package/dist/ajv/index.d.ts +1 -1
  4. package/dist/ajv/index.js.map +1 -1
  5. package/dist/ajv/index.mjs.map +1 -1
  6. package/dist/index.d.ts +265 -166
  7. package/dist/index.js +2821 -556
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +2814 -555
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/yup/index.d.ts +98 -6
  12. package/dist/yup/index.js.map +1 -1
  13. package/dist/yup/index.mjs.map +1 -1
  14. package/dist/zod/index.d.ts +97 -4
  15. package/dist/zod/index.js.map +1 -1
  16. package/dist/zod/index.mjs.map +1 -1
  17. package/package.json +5 -3
  18. package/src/array-field.tsx +50 -30
  19. package/src/auto-form.tsx +7 -3
  20. package/src/base-field.tsx +59 -0
  21. package/src/create-field.tsx +143 -0
  22. package/src/create-form.tsx +31 -0
  23. package/src/default-fields.tsx +146 -0
  24. package/src/display-field.tsx +8 -9
  25. package/src/display-if.tsx +6 -5
  26. package/src/field-resolver.ts +1 -1
  27. package/src/field.tsx +14 -444
  28. package/src/fields-context.tsx +23 -0
  29. package/src/fields.tsx +18 -8
  30. package/src/form.tsx +27 -37
  31. package/src/index.ts +38 -0
  32. package/src/input-right-button/input-right-button.stories.tsx +1 -1
  33. package/src/input-right-button/input-right-button.tsx +0 -2
  34. package/src/layout.tsx +16 -11
  35. package/src/number-input/number-input.tsx +9 -5
  36. package/src/object-field.tsx +8 -7
  37. package/src/password-input/password-input.stories.tsx +23 -2
  38. package/src/password-input/password-input.tsx +5 -5
  39. package/src/pin-input/pin-input.tsx +1 -5
  40. package/src/radio/radio-input.stories.tsx +1 -1
  41. package/src/radio/radio-input.tsx +12 -10
  42. package/src/select/native-select.tsx +1 -4
  43. package/src/select/select.test.tsx +1 -1
  44. package/src/select/select.tsx +18 -14
  45. package/src/step-form.tsx +29 -11
  46. package/src/submit-button.tsx +5 -1
  47. package/src/types.ts +91 -0
  48. package/src/utils.ts +15 -0
  49. /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
@@ -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 './field'
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
- if (__DEV__) {
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
- if (__DEV__) {
44
- FormValue.displayName = 'FormValue'
45
- }
44
+ FormValue.displayName = 'FormValue'
@@ -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
- if (__DEV__) {
40
- DisplayIf.displayName = 'DisplayIf'
41
- }
42
+ DisplayIf.displayName = 'DisplayIf'
@@ -1,4 +1,4 @@
1
- import { FieldProps } from './field'
1
+ import { BaseFieldProps as FieldProps } from './types'
2
2
 
3
3
  import { get } from '@chakra-ui/utils'
4
4