@saas-ui/forms 1.0.0-rc.7 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,11 +1,11 @@
1
1
  import * as React from 'react'
2
2
 
3
- import { chakra, ResponsiveValue } from '@chakra-ui/system'
3
+ import { chakra, ResponsiveValue, forwardRef } from '@chakra-ui/system'
4
4
  import { __DEV__ } from '@chakra-ui/utils'
5
5
  import { AddIcon, MinusIcon } from '@chakra-ui/icons'
6
6
  import { IconButton, ButtonProps } from '@saas-ui/button'
7
7
 
8
- import { FormLayout } from './layout'
8
+ import { FormLayout, FormLayoutProps } from './layout'
9
9
  import { BaseField, FieldProps } from './field'
10
10
 
11
11
  import { mapNestedFields } from './utils'
@@ -28,7 +28,7 @@ interface ArrayField {
28
28
  [key: string]: unknown
29
29
  }
30
30
 
31
- interface ArrayFieldRowProps {
31
+ interface ArrayFieldRowProps extends FormLayoutProps {
32
32
  /**
33
33
  * Amount of field columns
34
34
  */
@@ -41,21 +41,20 @@ interface ArrayFieldRowProps {
41
41
  * The array index
42
42
  */
43
43
  index: number
44
-
44
+ /**
45
+ * The fields
46
+ */
45
47
  children: React.ReactNode
46
48
  }
47
49
 
48
50
  export const ArrayFieldRow: React.FC<ArrayFieldRowProps> = ({
49
51
  children,
50
- columns,
51
- spacing,
52
52
  index,
53
+ ...rowFieldsProps
53
54
  }) => {
54
55
  return (
55
56
  <ArrayFieldRowContainer index={index}>
56
- <ArrayFieldRowFields columns={columns} spacing={spacing}>
57
- {children}
58
- </ArrayFieldRowFields>
57
+ <ArrayFieldRowFields {...rowFieldsProps}>{children}</ArrayFieldRowFields>
59
58
  <ArrayFieldRemoveButton />
60
59
  </ArrayFieldRowContainer>
61
60
  )
@@ -65,7 +64,7 @@ if (__DEV__) {
65
64
  ArrayFieldRow.displayName = 'ArrayFieldRow'
66
65
  }
67
66
 
68
- export interface ArrayFieldRowFieldsProps {
67
+ export interface ArrayFieldRowFieldsProps extends FormLayoutProps {
69
68
  /**
70
69
  * Amount of field columns
71
70
  */
@@ -74,25 +73,19 @@ export interface ArrayFieldRowFieldsProps {
74
73
  * Spacing between fields
75
74
  */
76
75
  spacing?: ResponsiveValue<string | number>
77
-
76
+ /**
77
+ * The fields
78
+ */
78
79
  children: React.ReactNode
79
80
  }
80
81
 
81
82
  export const ArrayFieldRowFields: React.FC<ArrayFieldRowFieldsProps> = ({
82
83
  children,
83
- columns,
84
- spacing,
85
84
  ...layoutProps
86
85
  }) => {
87
86
  const { name } = useArrayFieldRowContext()
88
87
  return (
89
- <FormLayout
90
- flex="1"
91
- columns={columns}
92
- gridGap={spacing}
93
- mr="2"
94
- {...layoutProps}
95
- >
88
+ <FormLayout flex="1" mr="2" {...layoutProps}>
96
89
  {mapNestedFields(name, children)}
97
90
  </FormLayout>
98
91
  )
@@ -162,7 +155,7 @@ export interface ArrayFieldProps
162
155
  extends ArrayFieldOptions,
163
156
  Omit<FieldProps, 'defaultValue'> {}
164
157
 
165
- export const ArrayField = React.forwardRef(
158
+ export const ArrayField = forwardRef(
166
159
  (props: ArrayFieldProps, ref: React.ForwardedRef<UseArrayFieldReturn>) => {
167
160
  const { children, ...containerProps } = props
168
161
 
@@ -183,7 +176,13 @@ export const ArrayField = React.forwardRef(
183
176
  </ArrayFieldContainer>
184
177
  )
185
178
  }
186
- )
179
+ ) as ((
180
+ props: ArrayFieldProps & {
181
+ ref?: React.ForwardedRef<UseArrayFieldReturn>
182
+ }
183
+ ) => React.ReactElement) & {
184
+ displayName: string
185
+ }
187
186
 
188
187
  if (__DEV__) {
189
188
  ArrayField.displayName = 'ArrayField'
package/src/auto-form.tsx CHANGED
@@ -6,7 +6,7 @@ import { __DEV__ } from '@chakra-ui/utils'
6
6
  import { Form, FormProps } from './form'
7
7
  import { FormLayout } from './layout'
8
8
  import { Fields } from './fields'
9
- import { SubmitButton, SubmitButtonProps } from './submit-button'
9
+ import { SubmitButton } from './submit-button'
10
10
  import { FieldResolver } from '.'
11
11
 
12
12
  interface AutoFormOptions {
@@ -48,7 +48,7 @@ export const AutoForm = forwardRef(
48
48
  <Form {...rest} schema={schema} ref={ref}>
49
49
  <FormLayout>
50
50
  {<Fields schema={schema} fieldResolver={fieldResolver} />}
51
- {submitLabel && <SubmitButton label={submitLabel} />}
51
+ {submitLabel && <SubmitButton>{submitLabel}</SubmitButton>}
52
52
  {children}
53
53
  </FormLayout>
54
54
  </Form>
package/src/field.tsx CHANGED
@@ -22,15 +22,29 @@ import {
22
22
  Checkbox,
23
23
  Switch,
24
24
  useMergeRefs,
25
+ InputGroup,
26
+ InputProps,
27
+ TextareaProps,
28
+ SwitchProps,
29
+ CheckboxProps,
30
+ PinInputField,
31
+ HStack,
32
+ PinInput,
33
+ UsePinInputProps,
34
+ SystemProps,
25
35
  } from '@chakra-ui/react'
26
- import { __DEV__ } from '@chakra-ui/utils'
36
+ import { __DEV__, FocusableElement } from '@chakra-ui/utils'
27
37
 
28
- import { NumberInput } from '@saas-ui/number-input'
29
- import { PasswordInput } from '@saas-ui/password-input'
30
- import { RadioInput } from '@saas-ui/radio'
31
- import { PinInput } from '@saas-ui/pin-input'
32
- import { Select, NativeSelect } from '@saas-ui/select'
33
- import { FocusableElement } from '@chakra-ui/utils'
38
+ import { NumberInput, NumberInputProps } from '@saas-ui/number-input'
39
+ import { PasswordInput, PasswordInputProps } from '@saas-ui/password-input'
40
+ import { RadioInput, RadioInputProps } from '@saas-ui/radio'
41
+
42
+ import {
43
+ Select,
44
+ SelectProps,
45
+ NativeSelect,
46
+ NativeSelectProps,
47
+ } from '@saas-ui/select'
34
48
 
35
49
  export interface Option {
36
50
  value: string
@@ -43,19 +57,6 @@ export type FieldRules = Pick<
43
57
  'required' | 'min' | 'max' | 'maxLength' | 'minLength' | 'pattern'
44
58
  >
45
59
 
46
- export type FieldTypes =
47
- | 'text'
48
- | 'number'
49
- | 'password'
50
- | 'textarea'
51
- | 'select'
52
- | 'native-select'
53
- | 'checkbox'
54
- | 'radio'
55
- | 'switch'
56
- | 'pin'
57
- | string
58
-
59
60
  export interface FieldProps<
60
61
  TFieldValues extends FieldValues = FieldValues,
61
62
  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
@@ -84,11 +85,6 @@ export interface FieldProps<
84
85
  'valueAsNumber' | 'valueAsDate' | 'setValueAs' | 'disabled'
85
86
  >
86
87
  /**
87
- * Options used for selects and radio fields
88
- */
89
- options?: Option[]
90
- /**
91
- * The field type
92
88
  * Build-in types:
93
89
  * - text
94
90
  * - number
@@ -102,16 +98,15 @@ export interface FieldProps<
102
98
  * - pin
103
99
  *
104
100
  * Will default to a text field if there is no matching type.
105
- * @default 'text'
106
101
  */
107
- type?: FieldTypes
102
+ type?: string
108
103
  /**
109
104
  * The input placeholder
110
105
  */
111
106
  placeholder?: string
112
107
  }
113
108
 
114
- const inputTypes: Record<FieldTypes, any> = {}
109
+ const inputTypes: Record<string, React.FC<any>> = {}
115
110
 
116
111
  const defaultInputType = 'text'
117
112
 
@@ -160,11 +155,30 @@ if (__DEV__) {
160
155
  BaseField.displayName = 'BaseField'
161
156
  }
162
157
 
163
- export const Field = forwardRef(
158
+ export type As<Props = any> = React.ElementType<Props>
159
+
160
+ export type PropsOf<T extends As> = React.ComponentPropsWithoutRef<T> & {
161
+ type?: FieldTypes
162
+ }
163
+
164
+ /**
165
+ * Build-in types:
166
+ * - text
167
+ * - number
168
+ * - password
169
+ * - textarea
170
+ * - select
171
+ * - native-select
172
+ * - checkbox
173
+ * - radio
174
+ * - switch
175
+ * - pin
176
+ *
177
+ * Will default to a text field if there is no matching type.
178
+ */
179
+ export const Field = React.forwardRef(
164
180
  <TFieldValues extends FieldValues = FieldValues>(
165
- props: FieldProps<TFieldValues> & {
166
- [key: string]: unknown // Make sure attributes of custom components work. Need to change this to a global typedef at some point.
167
- },
181
+ props: FieldProps<TFieldValues> | FieldTypeProps,
168
182
  ref: React.ForwardedRef<FocusableElement>
169
183
  ) => {
170
184
  const { type = defaultInputType } = props
@@ -172,13 +186,14 @@ export const Field = forwardRef(
172
186
 
173
187
  return <InputComponent ref={ref} {...props} />
174
188
  }
175
- ) as <TFieldValues extends FieldValues>(
176
- props: FieldProps<TFieldValues> & {
177
- [key: string]: unknown
178
- } & {
179
- ref?: React.ForwardedRef<FocusableElement>
180
- }
181
- ) => React.ReactElement
189
+ ) as (<TFieldValues extends FieldValues>(
190
+ props: FieldProps<TFieldValues> &
191
+ FieldTypeProps & {
192
+ ref?: React.ForwardedRef<FocusableElement>
193
+ }
194
+ ) => React.ReactElement) & {
195
+ displayName?: string
196
+ }
182
197
 
183
198
  interface CreateFieldProps {
184
199
  displayName: string
@@ -190,7 +205,7 @@ const createField = (
190
205
  InputComponent: React.FC<any>,
191
206
  { displayName, hideLabel, BaseField }: CreateFieldProps
192
207
  ) => {
193
- const Field = forwardRef<FieldProps, typeof FormControl>((props, ref) => {
208
+ const Field = forwardRef((props, ref) => {
194
209
  const {
195
210
  id,
196
211
  name,
@@ -239,7 +254,7 @@ const createField = (
239
254
  return Field
240
255
  }
241
256
 
242
- export const withControlledInput = (InputComponent: any) => {
257
+ export const withControlledInput = (InputComponent: React.FC<any>) => {
243
258
  return forwardRef<FieldProps, typeof InputComponent>(
244
259
  ({ name, rules, ...inputProps }, ref) => {
245
260
  const { control } = useFormContext()
@@ -262,7 +277,7 @@ export const withControlledInput = (InputComponent: any) => {
262
277
  )
263
278
  }
264
279
 
265
- export const withUncontrolledInput = (InputComponent: any) => {
280
+ export const withUncontrolledInput = (InputComponent: React.FC<any>) => {
266
281
  return forwardRef<FieldProps, typeof InputComponent>(
267
282
  ({ name, rules, ...inputProps }, ref) => {
268
283
  const { register } = useFormContext()
@@ -294,9 +309,9 @@ export interface RegisterFieldTypeOptions {
294
309
  * @param options.isControlled Set this to true if this is a controlled field.
295
310
  * @param options.hideLabel Hide the field label, for example for the checkbox field.
296
311
  */
297
- export const registerFieldType = (
312
+ export const registerFieldType = <T extends object>(
298
313
  type: string,
299
- component: React.FC<any>,
314
+ component: React.FC<T>,
300
315
  options?: RegisterFieldTypeOptions
301
316
  ) => {
302
317
  let InputComponent
@@ -313,25 +328,59 @@ export const registerFieldType = (
313
328
  .join('')}Field`,
314
329
  hideLabel: options?.hideLabel,
315
330
  BaseField: options?.BaseField || BaseField,
316
- })
331
+ }) as React.FC<T & FieldProps>
317
332
 
318
333
  inputTypes[type] = Field
319
334
 
320
335
  return Field
321
336
  }
322
337
 
323
- export const InputField = registerFieldType(
338
+ export interface InputFieldProps extends InputProps {
339
+ type?: string
340
+ leftAddon?: React.ReactNode
341
+ rightAddon?: React.ReactNode
342
+ }
343
+
344
+ export const InputField = registerFieldType<InputFieldProps>(
324
345
  'text',
325
- forwardRef(({ type = 'text', ...rest }, ref) => {
326
- return <Input type={type} {...rest} ref={ref} />
346
+ forwardRef(({ type = 'text', leftAddon, rightAddon, size, ...rest }, ref) => {
347
+ const input = <Input type={type} size={size} {...rest} ref={ref} />
348
+ if (leftAddon || rightAddon) {
349
+ return (
350
+ <InputGroup size={size}>
351
+ {leftAddon}
352
+ {input}
353
+ {rightAddon}
354
+ </InputGroup>
355
+ )
356
+ }
357
+ return input
327
358
  })
328
359
  )
329
- export const NumberInputField = registerFieldType('number', NumberInput, {
330
- isControlled: true,
331
- })
332
- export const PasswordInputFIeld = registerFieldType('password', PasswordInput)
333
- export const TextareaField = registerFieldType('textarea', Textarea)
334
- export const SwitchField = registerFieldType(
360
+
361
+ export interface NumberInputFieldProps extends NumberInputProps {
362
+ type: 'number'
363
+ }
364
+
365
+ export const NumberInputField = registerFieldType<NumberInputFieldProps>(
366
+ 'number',
367
+ NumberInput,
368
+ {
369
+ isControlled: true,
370
+ }
371
+ )
372
+
373
+ export const PasswordInputField = registerFieldType<PasswordInputProps>(
374
+ 'password',
375
+ forwardRef((props, ref) => <PasswordInput ref={ref} {...props} />)
376
+ )
377
+
378
+ export const TextareaField = registerFieldType<TextareaProps>(
379
+ 'textarea',
380
+ Textarea
381
+ )
382
+
383
+ export const SwitchField = registerFieldType<SwitchProps>(
335
384
  'switch',
336
385
  forwardRef(({ type, ...rest }, ref) => {
337
386
  return <Switch {...rest} ref={ref} />
@@ -340,32 +389,94 @@ export const SwitchField = registerFieldType(
340
389
  isControlled: true,
341
390
  }
342
391
  )
343
- export const SelectField = registerFieldType('select', Select, {
392
+
393
+ export const SelectField = registerFieldType<SelectProps>('select', Select, {
344
394
  isControlled: true,
345
395
  })
346
- export const CheckboxField = registerFieldType(
396
+
397
+ export const CheckboxField = registerFieldType<CheckboxProps>(
347
398
  'checkbox',
348
- forwardRef(
349
- ({ label, type, ...props }: { label?: string; type: string }, ref) => {
350
- return (
351
- <Checkbox ref={ref} {...props}>
352
- {label}
353
- </Checkbox>
354
- )
355
- }
356
- ),
399
+ forwardRef(({ label, type, ...props }, ref) => {
400
+ return (
401
+ <Checkbox ref={ref} {...props}>
402
+ {label}
403
+ </Checkbox>
404
+ )
405
+ }),
357
406
  {
358
407
  hideLabel: true,
359
408
  }
360
409
  )
361
- export const RadioField = registerFieldType('radio', RadioInput, {
362
- isControlled: true,
363
- })
364
- export const PinField = registerFieldType('pin', PinInput, {
365
- isControlled: true,
366
- })
367
- export const NativeSelectField = registerFieldType(
410
+
411
+ export const RadioField = registerFieldType<RadioInputProps>(
412
+ 'radio',
413
+ RadioInput,
414
+ {
415
+ isControlled: true,
416
+ }
417
+ )
418
+
419
+ export const NativeSelectField = registerFieldType<NativeSelectProps>(
368
420
  'native-select',
369
421
  NativeSelect,
370
422
  { isControlled: true }
371
423
  )
424
+
425
+ export interface PinFieldProps extends Omit<UsePinInputProps, 'type'> {
426
+ pinLength?: number
427
+ pinType?: 'alphanumeric' | 'number'
428
+ spacing?: SystemProps['margin']
429
+ }
430
+
431
+ export const PinField = registerFieldType<PinFieldProps>(
432
+ 'pin',
433
+ forwardRef((props, ref) => {
434
+ const { pinLength = 4, pinType, spacing, ...inputProps } = props
435
+
436
+ const inputs: React.ReactNode[] = []
437
+ for (let i = 0; i < pinLength; i++) {
438
+ inputs.push(<PinInputField key={i} ref={ref} />)
439
+ }
440
+
441
+ return (
442
+ <HStack spacing={spacing}>
443
+ <PinInput {...inputProps} type={pinType}>
444
+ {inputs}
445
+ </PinInput>
446
+ </HStack>
447
+ )
448
+ }),
449
+ {
450
+ isControlled: true,
451
+ }
452
+ )
453
+
454
+ const fieldTypes = {
455
+ text: InputField,
456
+ email: InputField,
457
+ url: InputField,
458
+ phone: InputField,
459
+ number: NumberInputField,
460
+ password: PasswordInputField,
461
+ textarea: TextareaField,
462
+ switch: SwitchField,
463
+ checkbox: CheckboxField,
464
+ radio: RadioField,
465
+ pin: PinField,
466
+ select: SelectField,
467
+ 'native-select': NativeSelectField,
468
+ }
469
+
470
+ type FieldTypes = typeof fieldTypes
471
+
472
+ type FieldType<Props = any> = React.ElementType<Props>
473
+
474
+ type TypeProps<P extends FieldType, T> = React.ComponentPropsWithoutRef<P> & {
475
+ type: T
476
+ }
477
+
478
+ type FieldTypeProps =
479
+ | {
480
+ [Property in keyof FieldTypes]: TypeProps<FieldTypes[Property], Property>
481
+ }[keyof FieldTypes]
482
+ | { type?: string }
package/src/form.tsx CHANGED
@@ -16,7 +16,6 @@ import {
16
16
  ResolverResult,
17
17
  } from 'react-hook-form'
18
18
  import { objectFieldResolver, FieldResolver } from './field-resolver'
19
- import { css } from '@emotion/react'
20
19
 
21
20
  export type { UseFormReturn, FieldValues, SubmitHandler }
22
21
 
package/src/step-form.tsx CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  } from '@saas-ui/stepper'
17
17
  import { Button, ButtonProps } from '@saas-ui/button'
18
18
 
19
- import { Form, FormProps } from './form'
19
+ import { Form } from './form'
20
20
  import { SubmitButton } from './submit-button'
21
21
 
22
22
  import {
@@ -24,6 +24,7 @@ import {
24
24
  useFormStep,
25
25
  StepFormProvider,
26
26
  UseStepFormProps,
27
+ FormStepSubmitHandler,
27
28
  } from './use-step-form'
28
29
 
29
30
  export interface StepFormProps<TFieldValues extends FieldValues = FieldValues>
@@ -53,7 +54,7 @@ export const StepForm = React.forwardRef(
53
54
  )
54
55
  }
55
56
  ) as <TFieldValues extends FieldValues>(
56
- props: FormProps<TFieldValues> & {
57
+ props: StepFormProps<TFieldValues> & {
57
58
  ref?: React.ForwardedRef<UseFormReturn<TFieldValues>>
58
59
  }
59
60
  ) => React.ReactElement
@@ -113,11 +114,14 @@ export const FormStepper: React.FC<StepperStepsProps> = (props) => {
113
114
 
114
115
  export interface FormStepProps
115
116
  extends FormStepOptions,
116
- HTMLChakraProps<'div'> {}
117
+ Omit<HTMLChakraProps<'div'>, 'onSubmit'> {
118
+ onSubmit?: FormStepSubmitHandler
119
+ }
117
120
 
118
121
  export const FormStep: React.FC<FormStepProps> = (props) => {
119
- const { name, schema, resolver, children, className, ...rest } = props
120
- const step = useFormStep({ name, schema, resolver })
122
+ const { name, schema, resolver, children, className, onSubmit, ...rest } =
123
+ props
124
+ const step = useFormStep({ name, schema, resolver, onSubmit })
121
125
 
122
126
  const { isActive } = step
123
127
 
@@ -160,11 +164,12 @@ export const NextButton: React.FC<NextButtonProps> = (props) => {
160
164
 
161
165
  return (
162
166
  <SubmitButton
163
- isDisabled={isCompleted}
164
- label={isLastStep || isCompleted ? submitLabel : label}
165
167
  {...rest}
168
+ isDisabled={isCompleted}
166
169
  className={cx('saas-form__next-button', props.className)}
167
- />
170
+ >
171
+ {isLastStep || isCompleted ? submitLabel : label}
172
+ </SubmitButton>
168
173
  )
169
174
  }
170
175
 
@@ -4,7 +4,6 @@ import { useFormContext } from 'react-hook-form'
4
4
 
5
5
  import { Button, ButtonProps } from '@saas-ui/button'
6
6
 
7
- import { forwardRef } from '@chakra-ui/system'
8
7
  import { __DEV__ } from '@chakra-ui/utils'
9
8
 
10
9
  export interface SubmitButtonProps extends ButtonProps {
@@ -24,29 +23,38 @@ export interface SubmitButtonProps extends ButtonProps {
24
23
  disableIfInvalid?: boolean
25
24
  }
26
25
 
27
- export const SubmitButton = forwardRef<SubmitButtonProps, 'button'>(
28
- (props, ref) => {
29
- const { children, disableIfUntouched, disableIfInvalid, ...rest } = props
30
- const { formState } = useFormContext()
31
-
32
- const isDisabled =
33
- (disableIfUntouched && !formState.isDirty) ||
34
- (disableIfInvalid && !formState.isValid)
35
-
36
- return (
37
- <Button
38
- type="submit"
39
- isLoading={formState.isSubmitting}
40
- isPrimary
41
- ref={ref}
42
- isDisabled={isDisabled}
43
- {...rest}
44
- >
45
- {children}
46
- </Button>
47
- )
48
- }
49
- )
26
+ export const SubmitButton = React.forwardRef<
27
+ HTMLButtonElement,
28
+ SubmitButtonProps
29
+ >((props, ref) => {
30
+ const {
31
+ children,
32
+ disableIfUntouched,
33
+ disableIfInvalid,
34
+ isDisabled: isDisabledProp,
35
+ isLoading,
36
+ ...rest
37
+ } = props
38
+ const { formState } = useFormContext()
39
+
40
+ const isDisabled =
41
+ (disableIfUntouched && !formState.isDirty) ||
42
+ (disableIfInvalid && !formState.isValid) ||
43
+ isDisabledProp
44
+
45
+ return (
46
+ <Button
47
+ {...rest}
48
+ ref={ref}
49
+ variant="primary"
50
+ type="submit"
51
+ isLoading={formState.isSubmitting || isLoading}
52
+ isDisabled={isDisabled}
53
+ >
54
+ {children}
55
+ </Button>
56
+ )
57
+ })
50
58
 
51
59
  SubmitButton.defaultProps = {
52
60
  label: 'Submit',