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

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 +20 -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 +345 -2584
  9. package/dist/index.js.map +1 -1
  10. package/dist/index.mjs +347 -2580
  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 +16 -9
  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 +154 -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,192 @@ 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 context = useFormControlContext()
60
+
61
+ const {
62
+ isInvalid,
63
+ isReadOnly,
64
+ isDisabled,
65
+ isFocused,
66
+ isRequired,
67
+ id,
68
+ onBlur,
69
+ onFocus,
70
+ } = context || {}
71
+
72
+ const { rightIcon = <ChevronDownIcon />, ...rest } = props
73
+
74
+ /* @ts-ignore */
75
+ const focusStyles = styles.field?._focusVisible
76
+ /* @ts-ignore */
77
+ const readOnlyStyles = styles.field?._readOnly
78
+ /* @ts-ignore */
79
+ const invalid = styles.field?._invalid
80
+
81
+ const height = styles.field?.h || styles.field?.height
82
+
83
+ const buttonStyles: SystemStyleObject = {
84
+ fontWeight: 'normal',
85
+ textAlign: 'left',
86
+ color: 'inherit',
87
+ _active: {
88
+ bg: 'transparent',
89
+ },
90
+ minH: height,
91
+ _focus: focusStyles,
92
+ _expanded: focusStyles,
93
+ _readOnly: readOnlyStyles,
94
+ _invalid: invalid,
95
+ ...styles.field,
96
+ h: 'auto',
97
+ }
98
+
99
+ // Using a Button, so we can simply use leftIcon and rightIcon
100
+ return (
101
+ <MenuButton
102
+ as={Button}
103
+ id={id || React.useId()}
104
+ {...rest}
105
+ onFocus={onFocus}
106
+ onBlur={onBlur}
107
+ isDisabled={isDisabled || isSelectDisabled}
108
+ data-invalid={dataAttr(isInvalid)}
109
+ data-read-only={dataAttr(isReadOnly)}
110
+ data-focus={dataAttr(isFocused)}
111
+ data-required={dataAttr(isRequired)}
112
+ rightIcon={rightIcon}
113
+ ref={ref}
114
+ sx={buttonStyles}
115
+ >
116
+ {renderValue(displayValue) || placeholder}
117
+ </MenuButton>
118
+ )
80
119
  }
81
-
82
- // Using a Button, so we can simply use leftIcon and rightIcon
83
- return <MenuButton as={Button} {...props} ref={ref} sx={buttonStyles} />
84
- })
120
+ )
85
121
 
86
122
  SelectButton.displayName = 'SelectButton'
87
123
 
124
+ /**
125
+ * Allow users to select a value from a list of options.
126
+ *
127
+ * @see https://saas-ui.dev/docs/components/forms/select
128
+ */
88
129
  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
130
+ const { name, children, isDisabled, multiple, ...rest } = props
131
+
107
132
  const menuProps = omitThemingProps(rest)
108
133
 
109
- const [currentValue, setCurrentValue] = React.useState(value || defaultValue)
134
+ const context = useSelect(props)
110
135
 
111
- const controlProps = useFormControl({ name } as HTMLChakraProps<'input'>)
136
+ const { value, controlProps } = context
112
137
 
113
- const options = React.useMemo(
114
- () => optionsProp && mapOptions(optionsProp),
115
- [optionsProp]
138
+ return (
139
+ <SelectProvider value={context}>
140
+ <Menu {...menuProps} closeOnSelect={!multiple}>
141
+ <chakra.div className={cx('sui-select')}>
142
+ {children}
143
+ <chakra.input
144
+ {...controlProps}
145
+ ref={ref}
146
+ name={name}
147
+ type="hidden"
148
+ value={value || ''}
149
+ className="saas-select__input"
150
+ />
151
+ </chakra.div>
152
+ </Menu>
153
+ </SelectProvider>
116
154
  )
155
+ })
117
156
 
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
- )
157
+ export interface SelectListProps extends MenuListProps {}
147
158
 
148
- const displayValue = currentValue
149
- ? (Array.isArray(currentValue) ? currentValue : [currentValue]).map(
150
- getDisplayValue
151
- )
152
- : []
159
+ /**
160
+ * The list of options to choose from.
161
+ *
162
+ * @see https://saas-ui.dev/docs/components/forms/select
163
+ */
164
+ export const SelectList: React.FC<SelectListProps> = (props) => {
165
+ const { defaultValue, value, options, multiple, onChange } =
166
+ useSelectContext()
153
167
 
154
168
  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>
169
+ <MenuList maxH="100vh" overflowY="auto" {...props}>
170
+ <MenuOptionGroup
171
+ defaultValue={(defaultValue || value) as string | string[] | undefined}
172
+ value={value}
173
+ onChange={onChange}
174
+ type={multiple ? 'checkbox' : 'radio'}
175
+ >
176
+ {options
177
+ ? options.map(({ value, label, ...rest }, i) => (
178
+ <SelectOption key={i} value={value} {...rest}>
179
+ {label || value}
180
+ </SelectOption>
181
+ ))
182
+ : props.children}
183
+ </MenuOptionGroup>
184
+ </MenuList>
186
185
  )
187
- })
186
+ }
188
187
 
189
188
  Select.displayName = 'Select'
189
+
190
+ /**
191
+ * An option in a select list
192
+ *
193
+ * @see https://saas-ui.dev/docs/components/forms/select
194
+ */
195
+ export const SelectOption = forwardRef<MenuItemOptionProps, 'button'>(
196
+ (props, ref) => {
197
+ return <MenuItemOption ref={ref} {...props} />
198
+ }
199
+ )
200
+ SelectOption.id = 'MenuItemOption'
201
+ 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,