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

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +53 -6
  3. package/dist/ajv/index.d.ts +1 -1
  4. package/dist/ajv/index.js.map +1 -1
  5. package/dist/ajv/index.mjs.map +1 -1
  6. package/dist/index.d.ts +265 -166
  7. package/dist/index.js +2821 -556
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +2814 -555
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/yup/index.d.ts +98 -6
  12. package/dist/yup/index.js.map +1 -1
  13. package/dist/yup/index.mjs.map +1 -1
  14. package/dist/zod/index.d.ts +97 -4
  15. package/dist/zod/index.js.map +1 -1
  16. package/dist/zod/index.mjs.map +1 -1
  17. package/package.json +5 -3
  18. package/src/array-field.tsx +50 -30
  19. package/src/auto-form.tsx +7 -3
  20. package/src/base-field.tsx +59 -0
  21. package/src/create-field.tsx +143 -0
  22. package/src/create-form.tsx +31 -0
  23. package/src/default-fields.tsx +146 -0
  24. package/src/display-field.tsx +8 -9
  25. package/src/display-if.tsx +6 -5
  26. package/src/field-resolver.ts +1 -1
  27. package/src/field.tsx +14 -444
  28. package/src/fields-context.tsx +23 -0
  29. package/src/fields.tsx +18 -8
  30. package/src/form.tsx +27 -37
  31. package/src/index.ts +38 -0
  32. package/src/input-right-button/input-right-button.stories.tsx +1 -1
  33. package/src/input-right-button/input-right-button.tsx +0 -2
  34. package/src/layout.tsx +16 -11
  35. package/src/number-input/number-input.tsx +9 -5
  36. package/src/object-field.tsx +8 -7
  37. package/src/password-input/password-input.stories.tsx +23 -2
  38. package/src/password-input/password-input.tsx +5 -5
  39. package/src/pin-input/pin-input.tsx +1 -5
  40. package/src/radio/radio-input.stories.tsx +1 -1
  41. package/src/radio/radio-input.tsx +12 -10
  42. package/src/select/native-select.tsx +1 -4
  43. package/src/select/select.test.tsx +1 -1
  44. package/src/select/select.tsx +18 -14
  45. package/src/step-form.tsx +29 -11
  46. package/src/submit-button.tsx +5 -1
  47. package/src/types.ts +91 -0
  48. package/src/utils.ts +15 -0
  49. /package/src/radio/{radio.test.tsx → radio-input.test.tsx} +0 -0
@@ -0,0 +1,59 @@
1
+ import * as React from 'react'
2
+ import {
3
+ useFormContext,
4
+ FormState,
5
+ get,
6
+ RegisterOptions,
7
+ FieldValues,
8
+ } from 'react-hook-form'
9
+
10
+ import {
11
+ Box,
12
+ FormControl,
13
+ FormLabel,
14
+ FormHelperText,
15
+ FormErrorMessage,
16
+ } from '@chakra-ui/react'
17
+ import { FocusableElement } from '@chakra-ui/utils'
18
+ import { useField } from './fields-context'
19
+ import { BaseFieldProps, FieldProps } from './types'
20
+
21
+ const getError = (name: string, formState: FormState<{ [x: string]: any }>) => {
22
+ return get(formState.errors, name)
23
+ }
24
+
25
+ const isTouched = (
26
+ name: string,
27
+ formState: FormState<{ [x: string]: any }>
28
+ ) => {
29
+ return get(formState.touchedFields, name)
30
+ }
31
+
32
+ /**
33
+ * The default BaseField component
34
+ * Composes the Chakra UI FormControl component, with FormLabel, FormHelperText and FormErrorMessage.
35
+ */
36
+ export const BaseField: React.FC<BaseFieldProps> = (props) => {
37
+ const { name, label, help, hideLabel, children, ...controlProps } = props
38
+
39
+ const { formState } = useFormContext()
40
+
41
+ const error = getError(name, formState)
42
+
43
+ return (
44
+ <FormControl {...controlProps} isInvalid={!!error}>
45
+ {label && !hideLabel ? <FormLabel>{label}</FormLabel> : null}
46
+ <Box>
47
+ {children}
48
+ {help && !error?.message ? (
49
+ <FormHelperText>{help}</FormHelperText>
50
+ ) : null}
51
+ {error?.message && (
52
+ <FormErrorMessage>{error?.message}</FormErrorMessage>
53
+ )}
54
+ </Box>
55
+ </FormControl>
56
+ )
57
+ }
58
+
59
+ BaseField.displayName = 'BaseField'
@@ -0,0 +1,143 @@
1
+ import * as React from 'react'
2
+ import { useFormContext, Controller } from 'react-hook-form'
3
+
4
+ import { forwardRef, useMergeRefs } from '@chakra-ui/react'
5
+ import { callAllHandlers } from '@chakra-ui/utils'
6
+ import { BaseFieldProps, FieldProps } from './types'
7
+ import { BaseField } from './base-field'
8
+
9
+ interface CreateFieldProps {
10
+ displayName: string
11
+ hideLabel?: boolean
12
+ BaseField: React.FC<any>
13
+ }
14
+
15
+ const _createField = (
16
+ InputComponent: React.FC<any>,
17
+ { displayName, hideLabel, BaseField }: CreateFieldProps
18
+ ) => {
19
+ const Field = forwardRef((props, ref) => {
20
+ const {
21
+ id,
22
+ name,
23
+ label,
24
+ help,
25
+ isDisabled,
26
+ isInvalid,
27
+ isReadOnly,
28
+ isRequired,
29
+ rules,
30
+ ...inputProps
31
+ } = props
32
+
33
+ const inputRules = {
34
+ required: isRequired,
35
+ ...rules,
36
+ }
37
+
38
+ return (
39
+ <BaseField
40
+ id={id}
41
+ name={name}
42
+ label={label}
43
+ help={help}
44
+ hideLabel={hideLabel}
45
+ isDisabled={isDisabled}
46
+ isInvalid={isInvalid}
47
+ isReadOnly={isReadOnly}
48
+ isRequired={isRequired}
49
+ >
50
+ <InputComponent
51
+ ref={ref}
52
+ id={id}
53
+ name={name}
54
+ label={hideLabel ? label : undefined} // Only pass down the label when it should be inline.
55
+ rules={inputRules}
56
+ {...inputProps}
57
+ />
58
+ </BaseField>
59
+ )
60
+ })
61
+ Field.displayName = displayName
62
+
63
+ return Field
64
+ }
65
+
66
+ const withControlledInput = (InputComponent: React.FC<any>) => {
67
+ return forwardRef<FieldProps, typeof InputComponent>(
68
+ ({ name, rules, ...inputProps }, ref) => {
69
+ const { control } = useFormContext()
70
+
71
+ return (
72
+ <Controller
73
+ name={name}
74
+ control={control}
75
+ rules={rules}
76
+ render={({ field: { ref: _ref, ...field } }) => (
77
+ <InputComponent
78
+ {...field}
79
+ {...inputProps}
80
+ onChange={callAllHandlers(inputProps.onChange, field.onChange)}
81
+ onBlur={callAllHandlers(inputProps.onBlur, field.onBlur)}
82
+ ref={useMergeRefs(ref, _ref)}
83
+ />
84
+ )}
85
+ />
86
+ )
87
+ }
88
+ )
89
+ }
90
+
91
+ const withUncontrolledInput = (InputComponent: React.FC<any>) => {
92
+ return forwardRef<FieldProps, typeof InputComponent>(
93
+ ({ name, rules, ...inputProps }, ref) => {
94
+ const { register } = useFormContext()
95
+
96
+ const { ref: _ref, ...field } = register(name, rules)
97
+
98
+ return (
99
+ <InputComponent
100
+ {...field}
101
+ {...inputProps}
102
+ onChange={callAllHandlers(inputProps.onChange, field.onChange)}
103
+ onBlur={callAllHandlers(inputProps.onBlur, field.onBlur)}
104
+ ref={useMergeRefs(ref, _ref)}
105
+ />
106
+ )
107
+ }
108
+ )
109
+ }
110
+
111
+ export interface CreateFieldOptions {
112
+ isControlled?: boolean
113
+ hideLabel?: boolean
114
+ BaseField?: React.FC<any>
115
+ }
116
+
117
+ /**
118
+ * Register a new field type
119
+ * @param type The name for this field in kebab-case, eg `email` or `array-field`
120
+ * @param component The React component
121
+ * @param options
122
+ * @param options.isControlled Set this to true if this is a controlled field.
123
+ * @param options.hideLabel Hide the field label, for example for the checkbox field.
124
+ */
125
+ export const createField = <TProps extends object>(
126
+ component: React.FC<TProps>,
127
+ options?: CreateFieldOptions
128
+ ) => {
129
+ let InputComponent
130
+ if (options?.isControlled) {
131
+ InputComponent = withControlledInput(component)
132
+ } else {
133
+ InputComponent = withUncontrolledInput(component)
134
+ }
135
+
136
+ const Field = _createField(InputComponent, {
137
+ displayName: `${component.displayName ?? 'Custom'}Field`,
138
+ hideLabel: options?.hideLabel,
139
+ BaseField: options?.BaseField || BaseField,
140
+ }) as React.FC<TProps & BaseFieldProps>
141
+
142
+ return Field
143
+ }
@@ -0,0 +1,31 @@
1
+ import React from 'react'
2
+ import { FieldsProvider } from './fields-context'
3
+ import { Form, FieldValues, FormProps, GetResolver } from './form'
4
+ import { WithFields } from './types'
5
+
6
+ export interface CreateFormProps<FieldDefs> {
7
+ resolver?: GetResolver
8
+ fields?: FieldDefs extends Record<string, React.FC<any>> ? FieldDefs : never
9
+ }
10
+
11
+ export function createForm<FieldDefs, Schema = any>({
12
+ resolver,
13
+ fields,
14
+ }: CreateFormProps<FieldDefs> = {}) {
15
+ const CreateForm = <
16
+ TFieldValues extends FieldValues,
17
+ TContext extends object = object,
18
+ TSchema extends Schema = Schema
19
+ >(
20
+ props: WithFields<FormProps<TFieldValues, TContext, TSchema>, FieldDefs>
21
+ ) => {
22
+ const { schema, ...rest } = props
23
+ return (
24
+ <FieldsProvider value={fields || {}}>
25
+ <Form resolver={resolver?.(props.schema)} {...rest} />
26
+ </FieldsProvider>
27
+ )
28
+ }
29
+
30
+ return CreateForm
31
+ }
@@ -0,0 +1,146 @@
1
+ import * as React from 'react'
2
+
3
+ import {
4
+ forwardRef,
5
+ Input,
6
+ Textarea,
7
+ Checkbox,
8
+ Switch,
9
+ InputGroup,
10
+ InputProps,
11
+ TextareaProps,
12
+ SwitchProps,
13
+ CheckboxProps,
14
+ PinInputField,
15
+ HStack,
16
+ PinInput,
17
+ UsePinInputProps,
18
+ SystemProps,
19
+ } from '@chakra-ui/react'
20
+
21
+ import { NumberInput, NumberInputProps } from './number-input'
22
+ import { PasswordInput, PasswordInputProps } from './password-input'
23
+ import { RadioInput, RadioInputProps } from './radio'
24
+
25
+ import { Select, SelectProps, NativeSelect, NativeSelectProps } from './select'
26
+
27
+ import { createField } from './create-field'
28
+
29
+ export interface InputFieldProps extends InputProps {
30
+ type?: string
31
+ leftAddon?: React.ReactNode
32
+ rightAddon?: React.ReactNode
33
+ }
34
+
35
+ export const InputField = createField<InputFieldProps>(
36
+ forwardRef(({ type = 'text', leftAddon, rightAddon, size, ...rest }, ref) => {
37
+ const input = <Input type={type} size={size} {...rest} ref={ref} />
38
+ if (leftAddon || rightAddon) {
39
+ return (
40
+ <InputGroup size={size}>
41
+ {leftAddon}
42
+ {input}
43
+ {rightAddon}
44
+ </InputGroup>
45
+ )
46
+ }
47
+ return input
48
+ })
49
+ )
50
+
51
+ export interface NumberInputFieldProps extends NumberInputProps {
52
+ type: 'number'
53
+ }
54
+
55
+ export const NumberInputField = createField<NumberInputFieldProps>(
56
+ NumberInput,
57
+ {
58
+ isControlled: true,
59
+ }
60
+ )
61
+
62
+ export const PasswordInputField = createField<PasswordInputProps>(
63
+ forwardRef((props, ref) => <PasswordInput ref={ref} {...props} />)
64
+ )
65
+
66
+ export const TextareaField = createField<TextareaProps>(Textarea)
67
+
68
+ export const SwitchField = createField<SwitchProps>(
69
+ forwardRef(({ type, value, ...rest }, ref) => {
70
+ return <Switch isChecked={!!value} {...rest} ref={ref} />
71
+ }),
72
+ {
73
+ isControlled: true,
74
+ }
75
+ )
76
+
77
+ export const SelectField = createField<SelectProps>(Select, {
78
+ isControlled: true,
79
+ })
80
+
81
+ export const CheckboxField = createField<CheckboxProps>(
82
+ forwardRef(({ label, type, ...props }, ref) => {
83
+ return (
84
+ <Checkbox ref={ref} {...props}>
85
+ {label}
86
+ </Checkbox>
87
+ )
88
+ }),
89
+ {
90
+ hideLabel: true,
91
+ }
92
+ )
93
+
94
+ export const RadioField = createField<RadioInputProps>(RadioInput, {
95
+ isControlled: true,
96
+ })
97
+
98
+ export const NativeSelectField = createField<NativeSelectProps>(NativeSelect, {
99
+ isControlled: true,
100
+ })
101
+
102
+ export interface PinFieldProps extends Omit<UsePinInputProps, 'type'> {
103
+ pinLength?: number
104
+ pinType?: 'alphanumeric' | 'number'
105
+ spacing?: SystemProps['margin']
106
+ }
107
+
108
+ export const PinField = createField<PinFieldProps>(
109
+ forwardRef((props, ref) => {
110
+ const { pinLength = 4, pinType, spacing, ...inputProps } = props
111
+
112
+ const inputs: React.ReactNode[] = []
113
+ for (let i = 0; i < pinLength; i++) {
114
+ inputs.push(<PinInputField key={i} ref={ref} />)
115
+ }
116
+
117
+ return (
118
+ <HStack spacing={spacing}>
119
+ <PinInput {...inputProps} type={pinType}>
120
+ {inputs}
121
+ </PinInput>
122
+ </HStack>
123
+ )
124
+ }),
125
+ {
126
+ isControlled: true,
127
+ }
128
+ )
129
+
130
+ export const defaultFieldTypes = {
131
+ text: InputField,
132
+ email: InputField,
133
+ url: InputField,
134
+ phone: InputField,
135
+ number: NumberInputField,
136
+ password: PasswordInputField,
137
+ textarea: TextareaField,
138
+ switch: SwitchField,
139
+ select: SelectField,
140
+ checkbox: CheckboxField,
141
+ radio: RadioField,
142
+ pin: PinField,
143
+ 'native-select': NativeSelectField,
144
+ }
145
+
146
+ export type DefaultFields = typeof defaultFieldTypes
@@ -1,5 +1,4 @@
1
1
  import * as React from 'react'
2
- import { __DEV__ } from '@chakra-ui/utils'
3
2
  import { useFormContext } from 'react-hook-form'
4
3
 
5
4
  import {
@@ -9,12 +8,16 @@ import {
9
8
  FormLabel,
10
9
  } from '@chakra-ui/react'
11
10
 
12
- import { FieldProps } from './field'
11
+ import { FieldProps } from './types'
13
12
 
14
13
  export interface DisplayFieldProps
15
14
  extends FormControlProps,
16
15
  Omit<FieldProps, 'type' | 'label'> {}
17
-
16
+ /**
17
+ *
18
+ *
19
+ * @see Docs https://saas-ui.dev/
20
+ */
18
21
  export const DisplayField: React.FC<DisplayFieldProps> = ({
19
22
  name,
20
23
  label,
@@ -31,15 +34,11 @@ export const DisplayField: React.FC<DisplayFieldProps> = ({
31
34
  )
32
35
  }
33
36
 
34
- if (__DEV__) {
35
- DisplayField.displayName = 'DisplayField'
36
- }
37
+ DisplayField.displayName = 'DisplayField'
37
38
 
38
39
  export const FormValue: React.FC<{ name: string }> = ({ name }) => {
39
40
  const { getValues } = useFormContext()
40
41
  return getValues(name) || null
41
42
  }
42
43
 
43
- if (__DEV__) {
44
- FormValue.displayName = 'FormValue'
45
- }
44
+ FormValue.displayName = 'FormValue'
@@ -1,5 +1,4 @@
1
1
  import * as React from 'react'
2
- import { __DEV__ } from '@chakra-ui/utils'
3
2
  import {
4
3
  useFormContext,
5
4
  useWatch,
@@ -17,7 +16,11 @@ export interface DisplayIfProps<
17
16
  isExact?: boolean
18
17
  condition?: (value: unknown, context: UseFormReturn<TFieldValues>) => boolean
19
18
  }
20
-
19
+ /**
20
+ * Conditionally render parts of a form.
21
+ *
22
+ * @see Docs https://saas-ui.dev/docs/components/forms/form
23
+ */
21
24
  export const DisplayIf = <TFieldValues extends FieldValues = FieldValues>({
22
25
  children,
23
26
  name,
@@ -36,6 +39,4 @@ export const DisplayIf = <TFieldValues extends FieldValues = FieldValues>({
36
39
  return condition(value, context) ? children : null
37
40
  }
38
41
 
39
- if (__DEV__) {
40
- DisplayIf.displayName = 'DisplayIf'
41
- }
42
+ DisplayIf.displayName = 'DisplayIf'
@@ -1,4 +1,4 @@
1
- import { FieldProps } from './field'
1
+ import { BaseFieldProps as FieldProps } from './types'
2
2
 
3
3
  import { get } from '@chakra-ui/utils'
4
4