@saas-ui/forms 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/Field.d.ts +79 -0
  3. package/dist/Field.d.ts.map +1 -0
  4. package/dist/array-field.d.ts +48 -0
  5. package/dist/array-field.d.ts.map +1 -0
  6. package/dist/auto-form.d.ts +10 -0
  7. package/dist/auto-form.d.ts.map +1 -0
  8. package/dist/display-field.d.ts +7 -0
  9. package/dist/display-field.d.ts.map +1 -0
  10. package/dist/fields.d.ts +6 -0
  11. package/dist/fields.d.ts.map +1 -0
  12. package/dist/form.d.ts +16 -0
  13. package/dist/form.d.ts.map +1 -0
  14. package/dist/index.d.ts +13 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +2 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/index.modern.js +2 -0
  19. package/dist/index.modern.js.map +1 -0
  20. package/dist/layout.d.ts +5 -0
  21. package/dist/layout.d.ts.map +1 -0
  22. package/dist/object-field.d.ts +11 -0
  23. package/dist/object-field.d.ts.map +1 -0
  24. package/dist/resolvers/yup.d.ts +12 -0
  25. package/dist/resolvers/yup.d.ts.map +1 -0
  26. package/dist/submit-button.d.ts +3 -0
  27. package/dist/submit-button.d.ts.map +1 -0
  28. package/dist/use-array-field.d.ts +95 -0
  29. package/dist/use-array-field.d.ts.map +1 -0
  30. package/dist/utils.d.ts +3 -0
  31. package/dist/utils.d.ts.map +1 -0
  32. package/package.json +75 -0
  33. package/src/array-field.tsx +196 -0
  34. package/src/auto-form.tsx +35 -0
  35. package/src/display-field.tsx +31 -0
  36. package/src/field.tsx +295 -0
  37. package/src/fields.tsx +51 -0
  38. package/src/form.tsx +49 -0
  39. package/src/index.ts +14 -0
  40. package/src/layout.tsx +37 -0
  41. package/src/object-field.tsx +25 -0
  42. package/src/resolvers/yup.ts +81 -0
  43. package/src/submit-button.tsx +25 -0
  44. package/src/use-array-field.tsx +152 -0
  45. package/src/utils.ts +13 -0
package/src/field.tsx ADDED
@@ -0,0 +1,295 @@
1
+ import * as React from 'react'
2
+ import { useFormContext, FormState, Controller, get } from 'react-hook-form'
3
+
4
+ import {
5
+ forwardRef,
6
+ Box,
7
+ FormControl,
8
+ FormControlProps,
9
+ FormLabel,
10
+ FormHelperText,
11
+ FormErrorMessage,
12
+ Input,
13
+ Textarea,
14
+ Checkbox,
15
+ Switch,
16
+ useMergeRefs,
17
+ } from '@chakra-ui/react'
18
+
19
+ import { NumberInput } from '@saas-ui/number-input'
20
+ import { PasswordInput } from '@saas-ui/password-input'
21
+ import { RadioInput } from '@saas-ui/radio'
22
+ import { PinInput } from '@saas-ui/pin-input'
23
+ import { Select, NativeSelect } from '@saas-ui/select'
24
+
25
+ export interface FieldProps extends Omit<FormControlProps, 'label'> {
26
+ /**
27
+ * The field name
28
+ */
29
+ name: string
30
+ /**
31
+ * The field label
32
+ */
33
+ label?: string
34
+ /**
35
+ * Hide the field label
36
+ */
37
+ hideLabel?: boolean
38
+ /**
39
+ * Field help text
40
+ */
41
+ help?: string
42
+ /**
43
+ * React hook form rules
44
+ */
45
+ rules?: any
46
+ /**
47
+ * Options used for selects and radio fields
48
+ */
49
+ options?: any
50
+ /**
51
+ * The field type
52
+ * Build-in types:
53
+ * - text
54
+ * - number
55
+ * - password
56
+ * - textarea
57
+ * - select
58
+ * - nativeselect
59
+ * - checkbox
60
+ * - radio
61
+ * - switch
62
+ * - pin
63
+ *
64
+ * Will default to a text field if there is no matching type.
65
+ * @default 'text'
66
+ */
67
+ type?: string
68
+ /**
69
+ * The input placeholder
70
+ */
71
+ placeholder?: string
72
+ }
73
+
74
+ const inputTypes: Record<string, any> = {}
75
+
76
+ const defaultInputType = 'text'
77
+
78
+ const getInput = (type: string) => {
79
+ return inputTypes[type] || inputTypes[defaultInputType]
80
+ }
81
+
82
+ const getError = (name: string, formState: FormState<{ [x: string]: any }>) => {
83
+ return get(formState.errors, name)
84
+ }
85
+
86
+ const isTouched = (
87
+ name: string,
88
+ formState: FormState<{ [x: string]: any }>
89
+ ) => {
90
+ return get(formState.touchedFields, name)
91
+ }
92
+
93
+ export const BaseField: React.FC<FieldProps> = (props) => {
94
+ const { name, label, help, variant, hideLabel, children, ...controlProps } =
95
+ props
96
+
97
+ const { formState } = useFormContext()
98
+
99
+ const error = getError(name, formState)
100
+
101
+ return (
102
+ <FormControl isInvalid={!!error} variant={variant} {...controlProps}>
103
+ {label && !hideLabel ? (
104
+ <FormLabel variant={variant}>{label}</FormLabel>
105
+ ) : null}
106
+ <Box>
107
+ {children}
108
+ {help && !error?.message ? (
109
+ <FormHelperText>{help}</FormHelperText>
110
+ ) : null}
111
+ {error?.message && (
112
+ <FormErrorMessage>{error?.message}</FormErrorMessage>
113
+ )}
114
+ </Box>
115
+ </FormControl>
116
+ )
117
+ }
118
+
119
+ export const Field = forwardRef<FieldProps, typeof FormControl>(
120
+ (props, ref) => {
121
+ const { type = defaultInputType } = props
122
+ const InputComponent = getInput(type)
123
+
124
+ return <InputComponent ref={ref} {...props} />
125
+ }
126
+ )
127
+
128
+ interface CreateFieldProps {
129
+ displayName: string
130
+ hideLabel?: boolean
131
+ BaseField: React.FC<any>
132
+ }
133
+
134
+ const createField = (
135
+ InputComponent: React.FC<any>,
136
+ { displayName, hideLabel, BaseField }: CreateFieldProps
137
+ ) => {
138
+ const Field = forwardRef<FieldProps, typeof FormControl>((props, ref) => {
139
+ const {
140
+ label,
141
+ isDisabled,
142
+ isInvalid,
143
+ isReadOnly,
144
+ isRequired,
145
+ isOptional,
146
+ variant,
147
+ ...inputProps
148
+ } = props
149
+
150
+ return (
151
+ <BaseField
152
+ label={label}
153
+ hideLabel={hideLabel}
154
+ isDiabled={isDisabled}
155
+ isInvalid={isInvalid}
156
+ isReadOnly={isReadOnly}
157
+ isRequired={isRequired}
158
+ isOptional={isOptional}
159
+ variant={variant}
160
+ >
161
+ <InputComponent ref={ref} label={label} {...inputProps} />
162
+ </BaseField>
163
+ )
164
+ })
165
+ Field.displayName = displayName
166
+
167
+ return Field
168
+ }
169
+
170
+ export const withControlledInput = (InputComponent: any) => {
171
+ return forwardRef(({ name, rules, ...inputProps }, ref) => {
172
+ const { control } = useFormContext()
173
+
174
+ return (
175
+ <Controller
176
+ name={name}
177
+ control={control}
178
+ rules={rules}
179
+ render={({ field: { ref: _ref, ...field } }) => (
180
+ <InputComponent
181
+ {...field}
182
+ {...inputProps}
183
+ ref={useMergeRefs(ref, _ref)}
184
+ />
185
+ )}
186
+ />
187
+ )
188
+ })
189
+ }
190
+
191
+ export const withUncontrolledInput = (InputComponent: any) => {
192
+ return forwardRef(({ name, rules, ...inputProps }, ref) => {
193
+ const { register } = useFormContext()
194
+
195
+ const { ref: _ref, ...field } = register(name, rules)
196
+
197
+ return (
198
+ <InputComponent
199
+ {...field}
200
+ {...inputProps}
201
+ ref={useMergeRefs(ref, _ref)}
202
+ />
203
+ )
204
+ })
205
+ }
206
+
207
+ export interface RegisterFieldTypeOptions {
208
+ isControlled?: boolean
209
+ hideLabel?: boolean
210
+ BaseField?: React.FC<any>
211
+ }
212
+
213
+ /**
214
+ * Register a new field type
215
+ * @param type The name for this field in kebab-case, eg `email` or `array-field`
216
+ * @param component The React component
217
+ * @param options
218
+ * @param options.isControlled Set this to true if this is a controlled field.
219
+ * @param options.hideLabel Hide the field label, for example for checkbox or switch field.
220
+ */
221
+ export const registerFieldType = (
222
+ type: string,
223
+ component: any,
224
+ options?: RegisterFieldTypeOptions
225
+ ) => {
226
+ let InputComponent
227
+ if (options?.isControlled) {
228
+ InputComponent = withControlledInput(component)
229
+ } else {
230
+ InputComponent = withUncontrolledInput(component)
231
+ }
232
+
233
+ const Field = createField(InputComponent, {
234
+ displayName: `${type
235
+ .split('-')
236
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
237
+ .join('')}Field`,
238
+ hideLabel: options?.hideLabel,
239
+ BaseField: options?.BaseField || BaseField,
240
+ })
241
+
242
+ inputTypes[type] = Field
243
+
244
+ return Field
245
+ }
246
+
247
+ // @todo Consider not registering all fields by default to lower the package size and computations.
248
+ // Not all types may be required in a project.
249
+ export const InputField = registerFieldType('text', Input)
250
+ export const NumberInputField = registerFieldType('number', NumberInput, {
251
+ isControlled: true,
252
+ })
253
+ export const PasswordInputFIeld = registerFieldType('password', PasswordInput)
254
+ export const TextareaField = registerFieldType('textarea', Textarea)
255
+ export const SwitchField = registerFieldType(
256
+ 'switch',
257
+ forwardRef(({ label, ...props }: { label?: string }, ref) => {
258
+ return (
259
+ <Switch ref={ref} {...props}>
260
+ {label}
261
+ </Switch>
262
+ )
263
+ }),
264
+ {
265
+ isControlled: true,
266
+ hideLabel: true,
267
+ }
268
+ )
269
+ export const SelectField = registerFieldType('select', Select, {
270
+ isControlled: true,
271
+ })
272
+ export const CheckboxField = registerFieldType(
273
+ 'checkbox',
274
+ forwardRef(({ label, ...props }: { label?: string }, ref) => {
275
+ return (
276
+ <Checkbox ref={ref} {...props}>
277
+ {label}
278
+ </Checkbox>
279
+ )
280
+ }),
281
+ {
282
+ hideLabel: true,
283
+ }
284
+ )
285
+ export const RadioField = registerFieldType('radio', RadioInput, {
286
+ isControlled: true,
287
+ })
288
+ export const PinField = registerFieldType('pin', PinInput, {
289
+ isControlled: true,
290
+ })
291
+ export const NativeSelectField = registerFieldType(
292
+ 'native-select',
293
+ NativeSelect,
294
+ { isControlled: true }
295
+ )
package/src/fields.tsx ADDED
@@ -0,0 +1,51 @@
1
+ import * as React from 'react'
2
+ import { getFieldsFromSchema, getNestedSchema } from './resolvers/yup'
3
+
4
+ import { FormLayout } from './layout'
5
+ import { Field, FieldProps } from './field'
6
+
7
+ import { ArrayField } from './array-field'
8
+ import { ObjectField } from './object-field'
9
+
10
+ export interface FieldsProps {
11
+ schema: any
12
+ }
13
+
14
+ const getNestedFields = (schema: any, name: string) => {
15
+ return getFieldsFromSchema(getNestedSchema(schema, name)).map(
16
+ ({ name, type, ...nestedFieldProps }: FieldProps): React.ReactNode => (
17
+ <Field key={name} name={name} type={type} {...nestedFieldProps} />
18
+ )
19
+ )
20
+ }
21
+
22
+ export const Fields: React.FC<FieldsProps> = ({ schema, ...props }) => {
23
+ return (
24
+ <FormLayout {...props}>
25
+ {getFieldsFromSchema(schema).map(
26
+ ({
27
+ name,
28
+ type,
29
+ defaultValue,
30
+ ...fieldProps
31
+ }: FieldProps): React.ReactNode => {
32
+ if (type === 'array') {
33
+ return (
34
+ <ArrayField name={name} {...fieldProps}>
35
+ {getNestedFields(schema, name)}
36
+ </ArrayField>
37
+ )
38
+ } else if (type === 'object') {
39
+ return (
40
+ <ObjectField name={name} {...fieldProps}>
41
+ {getNestedFields(schema, name)}
42
+ </ObjectField>
43
+ )
44
+ }
45
+
46
+ return <Field key={name} name={name} type={type} {...fieldProps} />
47
+ }
48
+ )}
49
+ </FormLayout>
50
+ )
51
+ }
package/src/form.tsx ADDED
@@ -0,0 +1,49 @@
1
+ import * as React from 'react'
2
+
3
+ import { chakra, HTMLChakraProps, forwardRef } from '@chakra-ui/react'
4
+
5
+ import {
6
+ useForm,
7
+ FormProvider,
8
+ UseFormProps,
9
+ FieldErrors,
10
+ } from 'react-hook-form'
11
+
12
+ import { resolver } from './resolvers/yup'
13
+
14
+ interface FormOptions {
15
+ schema?: any
16
+ submitLabel?: false | string
17
+ }
18
+
19
+ export interface FormProps extends HTMLChakraProps<'form'>, FormOptions {
20
+ defaultValues: Record<string, any>
21
+ onSubmit: (arg: any) => Promise<any> | void
22
+ onError?: (errors: FieldErrors) => void
23
+ children?: React.ReactNode
24
+ }
25
+
26
+ export const Form = forwardRef<FormProps, 'form'>(
27
+ ({ schema, defaultValues, onSubmit, onError, children, ...props }, ref) => {
28
+ const form: UseFormProps = { mode: 'all', defaultValues }
29
+
30
+ if (schema) {
31
+ form.resolver = resolver(schema)
32
+ }
33
+
34
+ const methods = useForm(form)
35
+ const { handleSubmit } = methods
36
+
37
+ return (
38
+ <FormProvider {...methods}>
39
+ <chakra.form
40
+ ref={ref}
41
+ onSubmit={handleSubmit(onSubmit, onError)}
42
+ {...props}
43
+ >
44
+ {children}
45
+ </chakra.form>
46
+ </FormProvider>
47
+ )
48
+ }
49
+ )
package/src/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ export * from './display-field'
2
+ export * from './field'
3
+ export * from './fields'
4
+ export * from './form'
5
+ export * from './auto-form'
6
+ export * from './layout'
7
+ export * from './submit-button'
8
+ export * from './array-field'
9
+ export * from './use-array-field'
10
+ export * from './object-field'
11
+
12
+ export * from '@saas-ui/input-right-button'
13
+
14
+ export type { FieldErrors } from 'react-hook-form'
package/src/layout.tsx ADDED
@@ -0,0 +1,37 @@
1
+ import * as React from 'react'
2
+
3
+ import { chakra, SimpleGrid, SimpleGridProps, useTheme } from '@chakra-ui/react'
4
+
5
+ export type FormLayoutProps = SimpleGridProps
6
+
7
+ interface FormLayoutItemProps {
8
+ children: React.ReactNode
9
+ }
10
+
11
+ const FormLayoutItem: React.FC<FormLayoutItemProps> = ({ children }) => {
12
+ return <chakra.div>{children}</chakra.div>
13
+ }
14
+
15
+ export const FormLayout = ({ children, ...props }: FormLayoutProps) => {
16
+ const theme = useTheme()
17
+
18
+ const defaultProps = theme.components?.FormLayout?.defaultProps ?? {
19
+ spacing: 4,
20
+ }
21
+
22
+ const gridProps = {
23
+ ...defaultProps,
24
+ ...props,
25
+ }
26
+
27
+ return (
28
+ <SimpleGrid {...gridProps}>
29
+ {React.Children.map(children, (child) => {
30
+ if (React.isValidElement(child)) {
31
+ return <FormLayoutItem>{child}</FormLayoutItem>
32
+ }
33
+ return child
34
+ })}
35
+ </SimpleGrid>
36
+ )
37
+ }
@@ -0,0 +1,25 @@
1
+ import * as React from 'react'
2
+ import { ResponsiveValue } from '@chakra-ui/system'
3
+
4
+ import { FormLayout } from './layout'
5
+ import { BaseField, FieldProps } from './field'
6
+
7
+ import { mapNestedFields } from './utils'
8
+
9
+ export interface ObjectFieldProps extends FieldProps {
10
+ name: string
11
+ children: React.ReactNode
12
+ columns?: ResponsiveValue<number>
13
+ spacing?: ResponsiveValue<string | number>
14
+ }
15
+
16
+ export const ObjectField: React.FC<ObjectFieldProps> = (props) => {
17
+ const { name, children, columns, spacing, ...fieldProps } = props
18
+ return (
19
+ <BaseField name={name} {...fieldProps}>
20
+ <FormLayout columns={columns} gridGap={spacing}>
21
+ {mapNestedFields(name, children)}
22
+ </FormLayout>
23
+ </BaseField>
24
+ )
25
+ }
@@ -0,0 +1,81 @@
1
+ import { SchemaOf, AnySchema, reach } from 'yup'
2
+ import { yupResolver } from '@hookform/resolvers/yup'
3
+
4
+ import { FieldProps } from '../Field'
5
+
6
+ export const resolver = yupResolver
7
+
8
+ // @TODO get proper typings for the schema fields
9
+
10
+ const getType = (field: any) => {
11
+ if (field.spec.meta?.type) {
12
+ return field.spec.meta.type
13
+ }
14
+
15
+ switch (field.type) {
16
+ case 'array':
17
+ return 'array'
18
+ case 'object':
19
+ return 'object'
20
+ case 'number':
21
+ return 'number'
22
+ case 'date':
23
+ return 'date'
24
+ case 'string':
25
+ default:
26
+ return 'text'
27
+ }
28
+ }
29
+
30
+ type Options = {
31
+ min?: number
32
+ max?: number
33
+ }
34
+
35
+ const getArrayOption = (field: any, name: string) => {
36
+ for (const test of field.tests) {
37
+ if (test.OPTIONS?.params[name]) return test.OPTIONS.params[name]
38
+ }
39
+ }
40
+
41
+ /**
42
+ * A helper function to render forms automatically based on a Yup schema
43
+ *
44
+ * @param schema The Yup schema
45
+ * @returns {FieldProps[]}
46
+ */
47
+ export const getFieldsFromSchema = (
48
+ schema: SchemaOf<AnySchema>
49
+ ): FieldProps[] => {
50
+ const fields = []
51
+
52
+ let schemaFields: Record<string, any> = {}
53
+ if (schema.type === 'array') {
54
+ /* @ts-ignore this is actually valid */
55
+ schemaFields = schema.innerType.fields
56
+ } else {
57
+ schemaFields = schema.fields
58
+ }
59
+
60
+ for (const name in schemaFields) {
61
+ const field = schemaFields[name]
62
+
63
+ const options: Options = {}
64
+ if (field.type === 'array') {
65
+ options.min = getArrayOption(field, 'min')
66
+ options.max = getArrayOption(field, 'max')
67
+ }
68
+
69
+ fields.push({
70
+ name,
71
+ label: field.spec.label || name,
72
+ type: getType(field),
73
+ ...options,
74
+ })
75
+ }
76
+ return fields
77
+ }
78
+
79
+ export const getNestedSchema = (schema: SchemaOf<AnySchema>, path: string) => {
80
+ return reach(schema, path)
81
+ }
@@ -0,0 +1,25 @@
1
+ import * as React from 'react'
2
+
3
+ import { useFormContext } from 'react-hook-form'
4
+
5
+ import { Button, ButtonProps } from '@saas-ui/button'
6
+
7
+ import { forwardRef } from '@chakra-ui/system'
8
+
9
+ export const SubmitButton = forwardRef<ButtonProps, 'button'>(
10
+ ({ children, ...props }, ref) => {
11
+ const data = useFormContext()
12
+
13
+ return (
14
+ <Button
15
+ type="submit"
16
+ isLoading={data.formState.isSubmitting}
17
+ isPrimary
18
+ ref={ref}
19
+ {...props}
20
+ >
21
+ {children}
22
+ </Button>
23
+ )
24
+ }
25
+ )