@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
package/src/form.tsx CHANGED
@@ -5,7 +5,6 @@ import { cx, runIfFn } from '@chakra-ui/utils'
5
5
 
6
6
  import {
7
7
  useForm,
8
- FormProvider,
9
8
  UseFormProps,
10
9
  UseFormReturn,
11
10
  FieldValues,
@@ -15,14 +14,21 @@ import {
15
14
  ResolverResult,
16
15
  WatchObserver,
17
16
  } from 'react-hook-form'
18
- import { objectFieldResolver, FieldResolver } from './field-resolver'
17
+ import { FormProvider } from './form-context'
18
+ import { FieldResolver } from './field-resolver'
19
19
  import { MaybeRenderProp } from '@chakra-ui/react-utils'
20
20
 
21
21
  export type { UseFormReturn, FieldValues, SubmitHandler }
22
22
 
23
- import { FieldProps } from './types'
23
+ import { FieldProps, DefaultFieldOverrides } from './types'
24
24
 
25
25
  import { Field as DefaultField } from './field'
26
+ import { FormLayout } from './layout'
27
+ import { AutoFields } from './fields'
28
+ import { SubmitButton } from './submit-button'
29
+ import { DisplayIf, DisplayIfProps } from './display-if'
30
+ import { ArrayField, ArrayFieldProps } from './array-field'
31
+ import { ObjectField, ObjectFieldProps } from './object-field'
26
32
 
27
33
  export interface FormRenderContext<
28
34
  TFieldValues extends FieldValues = FieldValues,
@@ -30,6 +36,9 @@ export interface FormRenderContext<
30
36
  TFieldTypes = FieldProps<TFieldValues>
31
37
  > extends UseFormReturn<TFieldValues, TContext> {
32
38
  Field: React.FC<TFieldTypes>
39
+ DisplayIf: React.FC<DisplayIfProps<TFieldValues>>
40
+ ArrayField: React.FC<ArrayFieldProps<TFieldValues>>
41
+ ObjectField: React.FC<ObjectFieldProps<TFieldValues>>
33
42
  }
34
43
 
35
44
  interface FormOptions<
@@ -39,7 +48,7 @@ interface FormOptions<
39
48
  TFieldTypes = FieldProps<TFieldValues>
40
49
  > {
41
50
  /**
42
- * The form schema, supports Yup, Zod, and AJV.
51
+ * The form schema.
43
52
  */
44
53
  schema?: TSchema
45
54
  /**
@@ -64,6 +73,14 @@ interface FormOptions<
64
73
  children?: MaybeRenderProp<
65
74
  FormRenderContext<TFieldValues, TContext, TFieldTypes>
66
75
  >
76
+ /**
77
+ * The field resolver, used to resolve the fields from schemas.
78
+ */
79
+ fieldResolver?: FieldResolver
80
+ /**
81
+ * Field overrides
82
+ */
83
+ fields?: DefaultFieldOverrides
67
84
  }
68
85
 
69
86
  export interface FormProps<
@@ -90,12 +107,14 @@ export const Form = forwardRef(
90
107
  TSchema = any,
91
108
  TFieldTypes = FieldProps<TFieldValues>
92
109
  >(
93
- props: FormProps<TFieldValues, TContext, TSchema>,
110
+ props: FormProps<TFieldValues, TContext, TSchema, TFieldTypes>,
94
111
  ref: React.ForwardedRef<HTMLFormElement>
95
112
  ) => {
96
113
  const {
97
114
  mode = 'all',
98
115
  resolver,
116
+ fieldResolver,
117
+ fields,
99
118
  reValidateMode,
100
119
  shouldFocusError,
101
120
  shouldUnregister,
@@ -130,10 +149,6 @@ export const Form = forwardRef(
130
149
  resetOptions,
131
150
  }
132
151
 
133
- if (schema && !resolver) {
134
- form.resolver = Form.getResolver?.(schema)
135
- }
136
-
137
152
  const methods = useForm<TFieldValues, TContext>(form)
138
153
  const { handleSubmit } = methods
139
154
 
@@ -148,16 +163,34 @@ export const Form = forwardRef(
148
163
  return () => subscription?.unsubscribe()
149
164
  }, [methods, onChange])
150
165
 
166
+ let _children = children
167
+ if (!_children && fieldResolver) {
168
+ _children = (
169
+ <FormLayout>
170
+ <AutoFields />
171
+ <SubmitButton {...fields?.submit} />
172
+ </FormLayout>
173
+ )
174
+ }
175
+
151
176
  return (
152
- <FormProvider {...methods}>
177
+ <FormProvider
178
+ {...methods}
179
+ schema={schema}
180
+ fieldResolver={fieldResolver}
181
+ fields={fields}
182
+ >
153
183
  <chakra.form
154
184
  ref={ref}
155
185
  onSubmit={handleSubmit(onSubmit, onError)}
156
186
  {...rest}
157
187
  className={cx('sui-form', props.className)}
158
188
  >
159
- {runIfFn(children, {
189
+ {runIfFn(_children, {
160
190
  Field: DefaultField as any,
191
+ DisplayIf: DisplayIf,
192
+ ArrayField: ArrayField as any,
193
+ ObjectField: ObjectField as any,
161
194
  ...methods,
162
195
  })}
163
196
  </chakra.form>
@@ -175,12 +208,8 @@ export const Form = forwardRef(
175
208
  }
176
209
  ) => React.ReactElement) & {
177
210
  displayName?: string
178
- getResolver?: GetResolver
179
- getFieldResolver: GetFieldResolver
180
211
  }
181
212
 
182
- Form.getFieldResolver = objectFieldResolver
183
-
184
213
  Form.displayName = 'Form'
185
214
 
186
215
  export type GetResolver = <
@@ -193,5 +222,3 @@ export type GetResolver = <
193
222
  context: TContext | undefined,
194
223
  options: ResolverOptions<TFieldValues>
195
224
  ) => Promise<ResolverResult<TFieldValues>>
196
-
197
- export type GetFieldResolver = (schema: any) => FieldResolver
package/src/index.ts CHANGED
@@ -2,8 +2,7 @@ export * from './display-field'
2
2
  export * from './field'
3
3
  export * from './fields'
4
4
  export * from './fields-context'
5
- export * from './form'
6
- export * from './auto-form'
5
+ // export * from './auto-form'
7
6
  export * from './layout'
8
7
  export * from './submit-button'
9
8
  export * from './array-field'
@@ -33,13 +32,10 @@ export {
33
32
  SwitchField,
34
33
  TextareaField,
35
34
  defaultFieldTypes,
36
- } from './default-fields'
37
-
38
- export type {
39
- DefaultFields,
40
- InputFieldProps,
41
- NumberInputFieldProps,
42
- PinFieldProps,
35
+ type DefaultFields,
36
+ type InputFieldProps,
37
+ type NumberInputFieldProps,
38
+ type PinFieldProps,
43
39
  } from './default-fields'
44
40
 
45
41
  export type {
@@ -49,11 +45,19 @@ export type {
49
45
  FieldOptions,
50
46
  } from './types'
51
47
 
52
- export { createForm } from './create-form'
53
- export type { CreateFormProps } from './create-form'
48
+ export { createForm, type CreateFormProps } from './create-form'
49
+ export { createField, type CreateFieldOptions } from './create-field'
50
+
51
+ export {
52
+ Form as BaseForm,
53
+ type FormProps,
54
+ type FormRenderContext,
55
+ } from './form'
56
+
57
+ export { FormProvider, useFormContext } from './form-context'
54
58
 
55
- export { createField } from './create-field'
56
- export type { CreateFieldOptions } from './create-field'
59
+ import { createForm } from './create-form'
60
+ export const Form = createForm()
57
61
 
58
62
  export type {
59
63
  BatchFieldArrayUpdate,
@@ -158,9 +162,7 @@ export {
158
162
  useController,
159
163
  useFieldArray,
160
164
  useForm,
161
- useFormContext,
162
165
  useFormState,
163
166
  useWatch,
164
167
  Controller,
165
- FormProvider,
166
168
  } from 'react-hook-form'
@@ -11,9 +11,13 @@ import { FormLayout } from './layout'
11
11
  import { BaseFieldProps } from './types'
12
12
 
13
13
  import { mapNestedFields } from './utils'
14
+ import { FieldPath, FieldValues } from 'react-hook-form'
14
15
 
15
- export interface ObjectFieldProps extends BaseFieldProps {
16
- name: string
16
+ export interface ObjectFieldProps<
17
+ TFieldValues extends FieldValues = FieldValues,
18
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
19
+ > extends BaseFieldProps {
20
+ name: TName
17
21
  children: React.ReactNode
18
22
  columns?: ResponsiveValue<number>
19
23
  spacing?: ResponsiveValue<string | number>
@@ -2,7 +2,7 @@ import React, { useState } from 'react'
2
2
 
3
3
  import { forwardRef, InputGroup, Input, InputProps } from '@chakra-ui/react'
4
4
 
5
- import { ViewIcon, ViewOffIcon } from '@chakra-ui/icons'
5
+ import { ViewIcon, ViewOffIcon } from '@saas-ui/core/icons'
6
6
 
7
7
  import { InputRightButton } from '../input-right-button'
8
8
 
@@ -0,0 +1,130 @@
1
+ import {
2
+ HTMLChakraProps,
3
+ useControllableState,
4
+ useFormControl,
5
+ } from '@chakra-ui/react'
6
+ import { createContext } from '@chakra-ui/react-utils'
7
+ import React from 'react'
8
+ import { FieldOptions } from '../types'
9
+ import { mapOptions } from '../utils'
10
+ import { SelectOption } from './select'
11
+
12
+ export const [SelectProvider, useSelectContext] = createContext<
13
+ ReturnType<typeof useSelect>
14
+ >({
15
+ strict: true,
16
+ })
17
+
18
+ export interface SelectOptions {
19
+ /**
20
+ * The name of the input field in a native form.
21
+ */
22
+ name: string
23
+ /**
24
+ * The value of the select field.
25
+ */
26
+ value?: string | string[]
27
+ /**
28
+ * The initial value of the select field.
29
+ */
30
+ defaultValue?: string | string[]
31
+ /**
32
+ * The callback invoked when the value of the select field changes.
33
+ * @param value The value of the select field.
34
+ */
35
+ onChange?: (value: string | string[]) => void
36
+ /**
37
+ * The placeholder text when there's no value.
38
+ */
39
+ placeholder?: string
40
+ /**
41
+ * If `true`, the select will be disabled.
42
+ */
43
+ isDisabled?: boolean
44
+ /**
45
+ * An array of options
46
+ * If you leave this empty the children prop will be rendered.
47
+ */
48
+ options?: FieldOptions<SelectOption>
49
+ /**
50
+ * Enable multiple select.
51
+ */
52
+ multiple?: boolean
53
+ /**
54
+ * The function used to render the value of the select field.
55
+ * @param value The value of the select field.
56
+ * @returns The rendered value.
57
+ */
58
+ renderValue?: (value: string | string[]) => React.ReactNode
59
+ }
60
+
61
+ export const useSelect = (props: SelectOptions) => {
62
+ const {
63
+ name,
64
+ value,
65
+ defaultValue,
66
+ onChange,
67
+ multiple,
68
+ placeholder,
69
+ options: optionsProp,
70
+ isDisabled,
71
+ renderValue = (value) =>
72
+ typeof value === 'string' ? value : value?.join(', '),
73
+ } = props
74
+ const [currentValue, setCurrentValue] = useControllableState({
75
+ value,
76
+ defaultValue,
77
+ onChange,
78
+ })
79
+
80
+ const controlProps = useFormControl({ name } as HTMLChakraProps<'input'>)
81
+
82
+ const options = React.useMemo(
83
+ () => optionsProp && mapOptions(optionsProp),
84
+ [optionsProp]
85
+ )
86
+
87
+ const handleChange = (value: string | string[]) => {
88
+ setCurrentValue(value)
89
+ }
90
+
91
+ const getDisplayValue = React.useCallback(
92
+ (value: string) => {
93
+ if (!options) {
94
+ return value
95
+ }
96
+
97
+ for (const option of options) {
98
+ if (option.value === value) {
99
+ return option.label || option.value
100
+ }
101
+ }
102
+
103
+ return value
104
+ },
105
+ [options]
106
+ )
107
+
108
+ const displayValue = React.useMemo(
109
+ () =>
110
+ currentValue
111
+ ? (Array.isArray(currentValue) ? currentValue : [currentValue]).map(
112
+ getDisplayValue
113
+ )
114
+ : [],
115
+ [currentValue, getDisplayValue]
116
+ )
117
+
118
+ return {
119
+ defaultValue,
120
+ value: currentValue,
121
+ displayValue,
122
+ renderValue,
123
+ onChange: handleChange,
124
+ options,
125
+ multiple,
126
+ controlProps,
127
+ placeholder,
128
+ isDisabled,
129
+ }
130
+ }
@@ -8,15 +8,23 @@ import {
8
8
  } from '@chakra-ui/react'
9
9
  import * as React from 'react'
10
10
 
11
- import { ComponentStory } from '@storybook/react'
11
+ import { StoryFn } from '@storybook/react'
12
12
 
13
- import { Select } from './select'
13
+ import { Select, SelectButton, SelectList, SelectOption } from './select'
14
14
  import { NativeSelect } from './native-select'
15
15
 
16
16
  import { FiSmile } from 'react-icons/fi'
17
17
 
18
+ const Template: StoryFn<typeof Select> = (args) => (
19
+ <Select placeholder="Select an option..." {...args}>
20
+ <SelectButton />
21
+ <SelectList />
22
+ </Select>
23
+ )
24
+
18
25
  export default {
19
26
  title: 'Components/Forms/Select',
27
+ component: Template,
20
28
  decorators: [
21
29
  (Story: any) => (
22
30
  <Container mt="40px" maxW="320px">
@@ -34,111 +42,134 @@ const getOptions = (length = 6) =>
34
42
 
35
43
  const options = getOptions()
36
44
 
37
- const Template: ComponentStory<typeof Select> = (args) => (
38
- <Select placeholder="Select an option..." {...args} />
39
- )
40
-
41
- export const Basic = Template.bind({})
42
- Basic.args = {
43
- name: 'select',
44
- options,
45
+ export const Basic = {
46
+ args: {
47
+ name: 'select',
48
+ options,
49
+ },
45
50
  }
46
51
 
47
- export const DefaultValue = Template.bind({})
48
- DefaultValue.args = {
49
- name: 'select',
50
- options,
51
- defaultValue: 1,
52
+ export const DefaultValue = {
53
+ args: {
54
+ name: 'select',
55
+ options,
56
+ defaultValue: '1',
57
+ },
52
58
  }
53
59
 
54
- export const Placeholder = Template.bind({})
55
- Placeholder.args = {
56
- name: 'select',
57
- options,
58
- placeholder: 'Select an option...',
60
+ export const Placeholder = {
61
+ args: {
62
+ name: 'select',
63
+ options,
64
+ placeholder: 'Select an option...',
65
+ },
59
66
  }
60
-
61
- export const Disabled = Template.bind({})
62
- Disabled.args = {
63
- name: 'select',
64
- options,
65
- placeholder: 'Disabled.',
66
- isDisabled: true,
67
+ export const Disabled = {
68
+ args: {
69
+ name: 'select',
70
+ options,
71
+ placeholder: 'Disabled.',
72
+ isDisabled: true,
73
+ },
67
74
  }
68
75
 
69
- export const Multi = Template.bind({})
70
- Multi.args = {
71
- name: 'select',
72
- options,
73
- placeholder: 'Multiple.',
74
- multiple: true,
76
+ export const Multi = {
77
+ args: {
78
+ name: 'select',
79
+ options,
80
+ placeholder: 'Multiple.',
81
+ multiple: true,
82
+ },
75
83
  }
76
84
 
77
- export const MultiWithDefaultValue = Template.bind({})
78
- MultiWithDefaultValue.args = {
79
- name: 'select',
80
- options,
81
- placeholder: 'Select an option...',
82
- multiple: true,
83
- defaultValue: ['1'],
85
+ export const MultiWithDefaultValue = {
86
+ args: {
87
+ name: 'select',
88
+ options,
89
+ placeholder: 'Select an option...',
90
+ multiple: true,
91
+ defaultValue: ['1'],
92
+ },
84
93
  }
85
94
 
86
- export const MultiWithTags = Template.bind({})
87
- MultiWithTags.args = {
88
- name: 'select',
89
- options,
90
- placeholder: 'Select options...',
91
- multiple: true,
92
- renderValue: (selected) => {
93
- if (selected?.length) {
94
- return (
95
- <Wrap py="1">
96
- {selected.map((value) => (
97
- <WrapItem>
98
- <Tag>{value}</Tag>
99
- </WrapItem>
100
- ))}
101
- </Wrap>
102
- )
103
- }
95
+ export const MultiWithTags = {
96
+ args: {
97
+ name: 'select',
98
+ options,
99
+ placeholder: 'Select options...',
100
+ multiple: true,
101
+ renderValue: (selected) => {
102
+ if (selected?.length) {
103
+ return (
104
+ <Wrap py="1">
105
+ {selected.map((value) => (
106
+ <WrapItem>
107
+ <Tag variant="solid">{value}</Tag>
108
+ </WrapItem>
109
+ ))}
110
+ </Wrap>
111
+ )
112
+ }
113
+ },
104
114
  },
105
115
  }
106
116
 
107
- export const WithIcons = Template.bind({})
108
- WithIcons.args = {
109
- name: 'select',
110
- options,
111
- value: 1,
112
- leftIcon: <Icon as={FiSmile} />,
117
+ export const Test = {
118
+ render: () => (
119
+ <Tag variant="outline" colorScheme="teal">
120
+ Test
121
+ </Tag>
122
+ ),
113
123
  }
114
124
 
115
- export const MaxHeight = Template.bind({})
116
- MaxHeight.args = {
117
- name: 'select',
118
- options: getOptions(100),
125
+ export const WithIcons = {
126
+ render: (args) => (
127
+ <Select placeholder="Select an option..." {...args}>
128
+ <SelectButton leftIcon={<Icon as={FiSmile} />} />
129
+ <SelectList />
130
+ </Select>
131
+ ),
132
+ args: {
133
+ name: 'select',
134
+ options,
135
+ value: '1',
136
+ },
119
137
  }
120
138
 
121
- export const WithChildren = () => {
122
- return (
123
- <Select name="select" value="1">
124
- <MenuItemOption value="1">Option 1</MenuItemOption>
125
- <MenuItemOption value="2">Option 1</MenuItemOption>
139
+ export const MaxHeight = {
140
+ args: {
141
+ name: 'select',
142
+ options: getOptions(100),
143
+ },
144
+ }
145
+
146
+ export const WithChildren = {
147
+ render: () => (
148
+ <Select name="select" defaultValue="1">
149
+ <SelectButton />
150
+ <SelectList>
151
+ <SelectOption value="1">Option 1</SelectOption>
152
+ <SelectOption value="2">Option 2</SelectOption>
153
+ </SelectList>
126
154
  </Select>
127
- )
155
+ ),
128
156
  }
129
157
 
130
- export const WithEmptyOption = () => {
131
- return (
132
- <Select name="select" value="1">
133
- <MenuItemOption value="">None</MenuItemOption>
134
- <MenuItemOption value="1">Option 1</MenuItemOption>
135
- <MenuItemOption value="2">Option 1</MenuItemOption>
158
+ export const WithEmptyOption = {
159
+ render: () => (
160
+ <Select name="select" defaultValue="1">
161
+ <SelectButton />
162
+ <SelectList>
163
+ <SelectOption value="">None</SelectOption>
164
+ <SelectOption value="1">Option 1</SelectOption>
165
+ <SelectOption value="2">Option 2</SelectOption>
166
+ </SelectList>
136
167
  </Select>
137
- )
168
+ ),
138
169
  }
139
170
 
140
- export const WithNativeSelect = () => (
141
- <>
171
+ export const WithNativeSelect = {
172
+ render: () => (
142
173
  <NativeSelect name="select" options={options} aria-label="Select" />
143
- </>
144
- )
174
+ ),
175
+ }