@saas-ui/forms 2.0.0-next.5 → 2.0.0-next.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/ajv/index.d.ts +358 -11
  3. package/dist/ajv/index.js +7 -9
  4. package/dist/ajv/index.js.map +1 -1
  5. package/dist/ajv/index.mjs +7 -10
  6. package/dist/ajv/index.mjs.map +1 -1
  7. package/dist/index.d.ts +296 -194
  8. package/dist/index.js +373 -2613
  9. package/dist/index.js.map +1 -1
  10. package/dist/index.mjs +373 -2607
  11. package/dist/index.mjs.map +1 -1
  12. package/dist/yup/index.d.ts +573 -106
  13. package/dist/yup/index.js +6 -10
  14. package/dist/yup/index.js.map +1 -1
  15. package/dist/yup/index.mjs +4 -8
  16. package/dist/yup/index.mjs.map +1 -1
  17. package/dist/zod/index.d.ts +490 -14
  18. package/dist/zod/index.js +5 -0
  19. package/dist/zod/index.js.map +1 -1
  20. package/dist/zod/index.mjs +5 -1
  21. package/dist/zod/index.mjs.map +1 -1
  22. package/package.json +15 -8
  23. package/src/array-field.tsx +34 -17
  24. package/src/base-field.tsx +4 -9
  25. package/src/create-field.tsx +2 -1
  26. package/src/create-form.tsx +33 -10
  27. package/src/default-fields.tsx +21 -4
  28. package/src/display-field.tsx +1 -2
  29. package/src/display-if.tsx +14 -8
  30. package/src/field-resolver.ts +10 -8
  31. package/src/field.tsx +6 -3
  32. package/src/fields.tsx +16 -13
  33. package/src/form-context.tsx +84 -0
  34. package/src/form.tsx +44 -17
  35. package/src/index.ts +17 -15
  36. package/src/object-field.tsx +6 -2
  37. package/src/password-input/password-input.tsx +1 -1
  38. package/src/select/select-context.tsx +130 -0
  39. package/src/select/select.stories.tsx +116 -85
  40. package/src/select/select.tsx +152 -142
  41. package/src/types.ts +59 -6
  42. package/src/use-array-field.tsx +9 -3
  43. package/src/utils.ts +8 -1
  44. package/src/watch-field.tsx +2 -6
@@ -10,180 +10,190 @@ import {
10
10
  MenuListProps,
11
11
  MenuItemOption,
12
12
  MenuOptionGroup,
13
- MenuOptionGroupProps,
14
13
  Button,
15
14
  ButtonProps,
16
15
  omitThemingProps,
17
16
  useMultiStyleConfig,
18
17
  SystemStyleObject,
19
- useFormControl,
20
- HTMLChakraProps,
21
18
  MenuItemOptionProps,
19
+ useFormControlContext,
22
20
  } from '@chakra-ui/react'
23
- import { cx } from '@chakra-ui/utils'
21
+ import { cx, dataAttr } from '@chakra-ui/utils'
24
22
  import { ChevronDownIcon } from '@saas-ui/core'
25
23
 
26
- import { FieldOptions, FieldOption } from '../types'
27
- import { mapOptions } from '../utils'
24
+ import { FieldOption } from '../types'
25
+
26
+ import {
27
+ SelectOptions,
28
+ SelectProvider,
29
+ useSelect,
30
+ useSelectContext,
31
+ } from './select-context'
28
32
 
29
33
  export interface SelectOption
30
34
  extends Omit<MenuItemOptionProps, 'value'>,
31
35
  FieldOption {}
32
36
 
33
- interface SelectOptions {
34
- /**
35
- * An array of options
36
- * If you leave this empty the children prop will be rendered.
37
- */
38
- options?: FieldOptions<SelectOption>
39
- /**
40
- * Props passed to the MenuList.
41
- */
42
- menuListProps?: MenuListProps
43
- /**
44
- * Customize how the value is rendered.
45
- * @type (value?: string[]) => React.ReactElement
46
- */
47
- renderValue?: (value?: string[]) => React.ReactElement | undefined
48
- /**
49
- * Enable multiple select.
50
- */
51
- multiple?: boolean
52
- }
53
-
54
37
  export interface SelectProps
55
38
  extends Omit<MenuProps, 'children'>,
56
- Pick<ButtonProps, 'isDisabled' | 'leftIcon' | 'rightIcon'>,
57
- Pick<MenuOptionGroupProps, 'onChange'>,
58
39
  SelectOptions {}
59
40
 
60
- const SelectButton = forwardRef((props, ref) => {
61
- const styles = useMultiStyleConfig('Input', props)
62
-
63
- /* @ts-ignore */
64
- const focusStyles = styles.field._focusVisible
65
-
66
- const height = styles.field.h || styles.field.height
67
-
68
- const buttonStyles: SystemStyleObject = {
69
- fontWeight: 'normal',
70
- textAlign: 'left',
71
- color: 'inherit',
72
- _active: {
73
- bg: 'transparent',
74
- },
75
- minH: height,
76
- _focus: focusStyles,
77
- _expanded: focusStyles,
78
- ...styles.field,
79
- h: 'auto',
41
+ export interface SelectButtonProps extends ButtonProps {}
42
+
43
+ /**
44
+ * Button that opens the select menu and displays the selected value.
45
+ *
46
+ * @see https://saas-ui.dev/docs/components/forms/select
47
+ */
48
+ export const SelectButton = forwardRef<SelectButtonProps, 'button'>(
49
+ (props, ref) => {
50
+ const styles = useMultiStyleConfig('SuiSelect', props)
51
+
52
+ const {
53
+ displayValue,
54
+ renderValue,
55
+ placeholder,
56
+ isDisabled: isSelectDisabled,
57
+ } = useSelectContext()
58
+
59
+ const {
60
+ isInvalid,
61
+ isReadOnly,
62
+ isDisabled,
63
+ isFocused,
64
+ isRequired,
65
+ id,
66
+ onBlur,
67
+ onFocus,
68
+ } = useFormControlContext()
69
+
70
+ const { rightIcon = <ChevronDownIcon />, ...rest } = props
71
+
72
+ /* @ts-ignore */
73
+ const focusStyles = styles.field?._focusVisible
74
+ /* @ts-ignore */
75
+ const readOnlyStyles = styles.field?._readOnly
76
+ /* @ts-ignore */
77
+ const invalid = styles.field?._invalid
78
+
79
+ const height = styles.field?.h || styles.field?.height
80
+
81
+ const buttonStyles: SystemStyleObject = {
82
+ fontWeight: 'normal',
83
+ textAlign: 'left',
84
+ color: 'inherit',
85
+ _active: {
86
+ bg: 'transparent',
87
+ },
88
+ minH: height,
89
+ _focus: focusStyles,
90
+ _expanded: focusStyles,
91
+ _readOnly: readOnlyStyles,
92
+ _invalid: invalid,
93
+ ...styles.field,
94
+ h: 'auto',
95
+ }
96
+
97
+ // Using a Button, so we can simply use leftIcon and rightIcon
98
+ return (
99
+ <MenuButton
100
+ as={Button}
101
+ id={id}
102
+ {...rest}
103
+ onFocus={onFocus}
104
+ onBlur={onBlur}
105
+ isDisabled={isDisabled || isSelectDisabled}
106
+ data-invalid={dataAttr(isInvalid)}
107
+ data-read-only={dataAttr(isReadOnly)}
108
+ data-focus={dataAttr(isFocused)}
109
+ data-required={dataAttr(isRequired)}
110
+ rightIcon={rightIcon}
111
+ ref={ref}
112
+ sx={buttonStyles}
113
+ >
114
+ {renderValue(displayValue) || placeholder}
115
+ </MenuButton>
116
+ )
80
117
  }
81
-
82
- // Using a Button, so we can simply use leftIcon and rightIcon
83
- return <MenuButton as={Button} {...props} ref={ref} sx={buttonStyles} />
84
- })
118
+ )
85
119
 
86
120
  SelectButton.displayName = 'SelectButton'
87
121
 
122
+ /**
123
+ * Allow users to select a value from a list of options.
124
+ *
125
+ * @see https://saas-ui.dev/docs/components/forms/select
126
+ */
88
127
  export const Select = forwardRef<SelectProps, 'select'>((props, ref) => {
89
- const {
90
- name,
91
- options: optionsProp,
92
- children,
93
- onChange,
94
- defaultValue,
95
- value,
96
- placeholder,
97
- isDisabled,
98
- leftIcon,
99
- rightIcon = <ChevronDownIcon />,
100
- multiple,
101
- size,
102
- variant,
103
- menuListProps,
104
- renderValue = (value) => value?.join(', '),
105
- ...rest
106
- } = props
128
+ const { name, children, isDisabled, multiple, ...rest } = props
129
+
107
130
  const menuProps = omitThemingProps(rest)
108
131
 
109
- const [currentValue, setCurrentValue] = React.useState(value || defaultValue)
132
+ const context = useSelect(props)
110
133
 
111
- const controlProps = useFormControl({ name } as HTMLChakraProps<'input'>)
134
+ const { value, controlProps } = context
112
135
 
113
- const options = React.useMemo(
114
- () => optionsProp && mapOptions(optionsProp),
115
- [optionsProp]
136
+ return (
137
+ <SelectProvider value={context}>
138
+ <Menu {...menuProps} closeOnSelect={!multiple}>
139
+ <chakra.div className={cx('sui-select')}>
140
+ {children}
141
+ <chakra.input
142
+ {...controlProps}
143
+ ref={ref}
144
+ name={name}
145
+ type="hidden"
146
+ value={value || ''}
147
+ className="saas-select__input"
148
+ />
149
+ </chakra.div>
150
+ </Menu>
151
+ </SelectProvider>
116
152
  )
153
+ })
117
154
 
118
- const handleChange = (value: string | string[]) => {
119
- setCurrentValue(value)
120
- onChange?.(value)
121
- }
122
-
123
- const buttonProps = {
124
- isDisabled,
125
- leftIcon,
126
- rightIcon,
127
- size,
128
- variant,
129
- }
130
-
131
- const getDisplayValue = React.useCallback(
132
- (value: string) => {
133
- if (!options) {
134
- return value
135
- }
136
-
137
- for (const option of options) {
138
- if (option.label && option.value === value) {
139
- return option.label
140
- }
141
- }
142
-
143
- return value
144
- },
145
- [options]
146
- )
155
+ export interface SelectListProps extends MenuListProps {}
147
156
 
148
- const displayValue = currentValue
149
- ? (Array.isArray(currentValue) ? currentValue : [currentValue]).map(
150
- getDisplayValue
151
- )
152
- : []
157
+ /**
158
+ * The list of options to choose from.
159
+ *
160
+ * @see https://saas-ui.dev/docs/components/forms/select
161
+ */
162
+ export const SelectList: React.FC<SelectListProps> = (props) => {
163
+ const { defaultValue, value, options, multiple, onChange } =
164
+ useSelectContext()
153
165
 
154
166
  return (
155
- <Menu {...menuProps} closeOnSelect={!multiple}>
156
- <chakra.div className={cx('sui-select')}>
157
- <SelectButton ref={ref} {...buttonProps}>
158
- {renderValue(displayValue) || placeholder}
159
- </SelectButton>
160
- <MenuList maxH="60vh" overflowY="auto" {...menuListProps}>
161
- <MenuOptionGroup
162
- defaultValue={
163
- (defaultValue || value) as string | string[] | undefined
164
- }
165
- onChange={handleChange}
166
- type={multiple ? 'checkbox' : 'radio'}
167
- >
168
- {options
169
- ? options.map(({ value, label, ...rest }, i) => (
170
- <MenuItemOption key={i} value={value} {...rest}>
171
- {label || value}
172
- </MenuItemOption>
173
- ))
174
- : children}
175
- </MenuOptionGroup>
176
- </MenuList>
177
- <chakra.input
178
- {...controlProps}
179
- name={name}
180
- type="hidden"
181
- value={currentValue}
182
- className="saas-select__input"
183
- />
184
- </chakra.div>
185
- </Menu>
167
+ <MenuList maxH="100vh" overflowY="auto" {...props}>
168
+ <MenuOptionGroup
169
+ defaultValue={(defaultValue || value) as string | string[] | undefined}
170
+ value={value}
171
+ onChange={onChange}
172
+ type={multiple ? 'checkbox' : 'radio'}
173
+ >
174
+ {options
175
+ ? options.map(({ value, label, ...rest }, i) => (
176
+ <SelectOption key={i} value={value} {...rest}>
177
+ {label || value}
178
+ </SelectOption>
179
+ ))
180
+ : props.children}
181
+ </MenuOptionGroup>
182
+ </MenuList>
186
183
  )
187
- })
184
+ }
188
185
 
189
186
  Select.displayName = 'Select'
187
+
188
+ /**
189
+ * An option in a select list
190
+ *
191
+ * @see https://saas-ui.dev/docs/components/forms/select
192
+ */
193
+ export const SelectOption = forwardRef<MenuItemOptionProps, 'button'>(
194
+ (props, ref) => {
195
+ return <MenuItemOption ref={ref} {...props} />
196
+ }
197
+ )
198
+ SelectOption.id = 'MenuItemOption'
199
+ SelectOption.displayName = 'SelectOption'
package/src/types.ts CHANGED
@@ -3,8 +3,9 @@ import { MaybeRenderProp } from '@chakra-ui/react-utils'
3
3
  import { FieldPath, FieldValues, RegisterOptions } from 'react-hook-form'
4
4
  import { DefaultFields } from './default-fields'
5
5
  import { FormProps, FormRenderContext } from './form'
6
+ import { SubmitButtonProps } from './submit-button'
6
7
 
7
- export type FieldOption = { label: string; value: string }
8
+ export type FieldOption = { label?: string; value: string }
8
9
  export type FieldOptions<TOption extends FieldOption = FieldOption> =
9
10
  | Array<string>
10
11
  | Array<TOption>
@@ -12,6 +13,28 @@ export type FieldOptions<TOption extends FieldOption = FieldOption> =
12
13
  export type ValueOf<T> = T[keyof T]
13
14
  export type ShallowMerge<A, B> = Omit<A, keyof B> & B
14
15
 
16
+ type Split<S extends string, D extends string> = string extends S
17
+ ? string[]
18
+ : S extends ''
19
+ ? []
20
+ : S extends `${infer T}${D}${infer U}`
21
+ ? [T, ...Split<U, D>]
22
+ : [S]
23
+
24
+ type MapPath<T extends string[]> = T extends [infer U, ...infer R]
25
+ ? U extends string
26
+ ? `${U extends `${number}` ? '$' : U}${R[0] extends string
27
+ ? '.'
28
+ : ''}${R extends string[] ? MapPath<R> : ''}`
29
+ : ''
30
+ : ''
31
+
32
+ type TransformPath<T extends string> = MapPath<Split<T, '.'>>
33
+
34
+ export type ArrayFieldPath<Name extends string> = Name extends string
35
+ ? TransformPath<Name>
36
+ : never
37
+
15
38
  export interface BaseFieldProps<
16
39
  TFieldValues extends FieldValues = FieldValues,
17
40
  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
@@ -19,7 +42,7 @@ export interface BaseFieldProps<
19
42
  /**
20
43
  * The field name
21
44
  */
22
- name: TName
45
+ name: TName | ArrayFieldPath<TName>
23
46
  /**
24
47
  * The field label
25
48
  */
@@ -52,12 +75,18 @@ export interface BaseFieldProps<
52
75
  placeholder?: string
53
76
  }
54
77
 
78
+ type FieldPathWithArray<
79
+ TFieldValues extends FieldValues,
80
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
81
+ > = TName | ArrayFieldPath<TName>
82
+
55
83
  type MergeFieldProps<
56
84
  FieldDefs,
57
- TFieldValues extends FieldValues = FieldValues
85
+ TFieldValues extends FieldValues = FieldValues,
86
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
58
87
  > = ValueOf<{
59
88
  [K in keyof FieldDefs]: FieldDefs[K] extends React.FC<infer Props>
60
- ? { type?: K } & ShallowMerge<Props, BaseFieldProps<TFieldValues>>
89
+ ? { type?: K } & ShallowMerge<Props, BaseFieldProps<TFieldValues, TName>>
61
90
  : never
62
91
  }>
63
92
 
@@ -81,11 +110,35 @@ export type FormChildren<
81
110
  >
82
111
  >
83
112
 
113
+ export type DefaultFieldOverrides = {
114
+ submit?: SubmitButtonProps
115
+ [key: string]: any
116
+ }
117
+
118
+ export type FieldOverrides<
119
+ FieldDefs,
120
+ TFieldValues extends FieldValues = FieldValues,
121
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
122
+ > = {
123
+ [K in FieldPathWithArray<TFieldValues, TName>]?: Omit<
124
+ MergeFieldProps<
125
+ FieldDefs extends never
126
+ ? DefaultFields
127
+ : ShallowMerge<DefaultFields, FieldDefs>,
128
+ TFieldValues
129
+ >,
130
+ 'name'
131
+ >
132
+ }
133
+
84
134
  export type WithFields<
85
135
  TFormProps extends FormProps<any, any, any, any>,
86
136
  FieldDefs
87
137
  > = TFormProps extends FormProps<infer TFieldValues, infer TContext>
88
- ? Omit<TFormProps, 'children'> & {
89
- children: FormChildren<FieldDefs, TFieldValues, TContext>
138
+ ? Omit<TFormProps, 'children' | 'fields'> & {
139
+ children?: FormChildren<FieldDefs, TFieldValues, TContext>
140
+ fields?: FieldOverrides<FieldDefs, TFieldValues> & {
141
+ submit?: SubmitButtonProps
142
+ }
90
143
  }
91
144
  : never
@@ -1,10 +1,13 @@
1
1
  import * as React from 'react'
2
2
  import {
3
3
  useFieldArray,
4
- useFormContext,
5
4
  UseFieldArrayReturn,
5
+ FieldValues,
6
+ FieldPath,
6
7
  } from 'react-hook-form'
7
8
 
9
+ import { useFormContext } from './form-context'
10
+
8
11
  import { createContext } from '@chakra-ui/react-utils'
9
12
 
10
13
  export interface UseArrayFieldReturn extends UseFieldArrayReturn {
@@ -59,11 +62,14 @@ export const [ArrayFieldRowProvider, useArrayFieldRowContext] =
59
62
  name: 'ArrayFieldRowContext',
60
63
  })
61
64
 
62
- export interface ArrayFieldOptions {
65
+ export interface ArrayFieldOptions<
66
+ TFieldValues extends FieldValues = FieldValues,
67
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
68
+ > {
63
69
  /**
64
70
  * The field name
65
71
  */
66
- name: string
72
+ name: TName
67
73
  /**
68
74
  * Default value for new values in the array
69
75
  */
package/src/utils.ts CHANGED
@@ -4,9 +4,16 @@ import { FieldOption, FieldOptions } from './types'
4
4
  export const mapNestedFields = (name: string, children: React.ReactNode) => {
5
5
  return React.Children.map(children, (child) => {
6
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
+
7
14
  return React.cloneElement(child, {
8
15
  ...child.props,
9
- name: `${name}.${child.props.name}`,
16
+ name: `${name}.${childName}`,
10
17
  })
11
18
  }
12
19
  return child
@@ -1,9 +1,5 @@
1
- import {
2
- FieldValues,
3
- useFormContext,
4
- UseFormReturn,
5
- useWatch,
6
- } from 'react-hook-form'
1
+ import { FieldValues, useWatch } from 'react-hook-form'
2
+ import { useFormContext, UseFormReturn } from './form-context'
7
3
 
8
4
  export interface WatchFieldProps<
9
5
  Value = unknown,