@sio-group/form-react 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -82
- package/README.md +2 -2
- package/dist/index.cjs +268 -18
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +258 -17
- package/package.json +6 -5
- package/src/assets/scss/components/button.scss +164 -0
- package/src/assets/scss/components/checkbox.scss +90 -0
- package/src/assets/scss/components/color.scss +29 -0
- package/src/assets/scss/components/form-field.scss +34 -0
- package/src/assets/scss/components/form-states.scss +80 -0
- package/src/assets/scss/components/grid.scss +134 -0
- package/src/assets/scss/components/input.scss +112 -0
- package/src/assets/scss/components/link.scss +66 -0
- package/src/assets/scss/components/radio.scss +104 -0
- package/src/assets/scss/components/range.scss +52 -0
- package/src/assets/scss/components/select.scss +35 -0
- package/src/assets/scss/components/upload.scss +52 -0
- package/src/assets/scss/index.scss +19 -0
- package/src/assets/scss/tokens/_colors.scss +49 -0
- package/src/assets/scss/tokens/_form.scss +6 -0
- package/src/assets/scss/utilities/_mixins.scss +6 -0
- package/src/components/Button/index.tsx +106 -0
- package/src/components/Fields/Checkbox/index.tsx +59 -0
- package/src/components/Fields/Input/DateInput/index.tsx +95 -0
- package/src/components/Fields/Input/FileInput/index.tsx +169 -0
- package/src/components/Fields/Input/Input.tsx +45 -0
- package/src/components/Fields/Input/NumberInput/index.tsx +169 -0
- package/src/components/Fields/Input/RangeInput/index.tsx +77 -0
- package/src/components/Fields/Input/TextInput/index.tsx +65 -0
- package/src/components/Fields/InputWrapper/index.tsx +78 -0
- package/src/components/Fields/Radio/index.tsx +82 -0
- package/src/components/Fields/Select/index.tsx +103 -0
- package/src/components/Fields/Textarea/index.tsx +70 -0
- package/src/components/Fields/index.tsx +11 -0
- package/src/components/Form.tsx +163 -0
- package/src/components/Icon/index.tsx +16 -0
- package/src/components/Link/index.tsx +106 -0
- package/src/hooks/useConnectionStatus.ts +20 -0
- package/src/hooks/useForm.ts +230 -0
- package/src/index.ts +15 -0
- package/src/types/field-props.d.ts +94 -0
- package/src/types/field-setters.d.ts +6 -0
- package/src/types/field-state.d.ts +21 -0
- package/src/types/form-config.d.ts +30 -0
- package/src/types/form-layout.d.ts +6 -0
- package/src/types/index.ts +18 -0
- package/src/types/ui-props.d.ts +33 -0
- package/src/types/use-form-options.d.ts +3 -0
- package/src/utils/create-field-props.ts +115 -0
- package/src/utils/create-field-state.ts +99 -0
- package/src/utils/custom-icons.tsx +145 -0
- package/src/utils/file-type-icon.ts +63 -0
- package/src/utils/get-accept-string.ts +24 -0
- package/src/utils/get-column-classes.ts +21 -0
- package/src/utils/get-file-size.ts +9 -0
- package/src/utils/parse-date.ts +36 -0
- package/src/utils/slugify.ts +9 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { LinkProps } from "../../types";
|
|
2
|
+
import React from "react";
|
|
3
|
+
|
|
4
|
+
const LinkComponent: React.FC<LinkProps> = ({
|
|
5
|
+
label,
|
|
6
|
+
to = '#',
|
|
7
|
+
onClick,
|
|
8
|
+
color = 'default',
|
|
9
|
+
size = 'md',
|
|
10
|
+
block = false,
|
|
11
|
+
loading = false,
|
|
12
|
+
disabled = false,
|
|
13
|
+
className = '',
|
|
14
|
+
ariaLabel = '',
|
|
15
|
+
navigate,
|
|
16
|
+
external = false,
|
|
17
|
+
style = {},
|
|
18
|
+
children,
|
|
19
|
+
}: LinkProps) => {
|
|
20
|
+
const isDisabled: boolean = disabled || loading;
|
|
21
|
+
const isExternal: boolean = external || /^(https?:|mailto:|tel:|ftp:)/.test(to);
|
|
22
|
+
|
|
23
|
+
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
|
|
24
|
+
if (isDisabled) {
|
|
25
|
+
e.preventDefault();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
onClick?.(e);
|
|
30
|
+
|
|
31
|
+
if (!isExternal && navigate) {
|
|
32
|
+
e.preventDefault();
|
|
33
|
+
navigate();
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const linkClasses = [
|
|
38
|
+
'link',
|
|
39
|
+
`link--${color}`,
|
|
40
|
+
`btn--${size}`,
|
|
41
|
+
block && 'link--block',
|
|
42
|
+
loading && 'link--loading',
|
|
43
|
+
isDisabled && 'link--disabled',
|
|
44
|
+
className,
|
|
45
|
+
]
|
|
46
|
+
.filter(Boolean)
|
|
47
|
+
.join(' ');
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<a
|
|
51
|
+
href={isDisabled ? undefined : to}
|
|
52
|
+
onClick={handleClick}
|
|
53
|
+
className={linkClasses}
|
|
54
|
+
style={style}
|
|
55
|
+
aria-label={ariaLabel || (label as string)}
|
|
56
|
+
aria-busy={loading}
|
|
57
|
+
aria-disabled={isDisabled}
|
|
58
|
+
target={isExternal ? '_blank' : undefined}
|
|
59
|
+
rel={isExternal ? 'noopener noreferrer' : undefined}>
|
|
60
|
+
{loading ? (
|
|
61
|
+
<>
|
|
62
|
+
<span className='btn__spinner' aria-hidden='true'>
|
|
63
|
+
<svg viewBox='0 0 20 20'>
|
|
64
|
+
<circle cx='10' cy='10' r='8' />
|
|
65
|
+
</svg>
|
|
66
|
+
</span>
|
|
67
|
+
<span className='btn__loading-text'>Processing...</span>
|
|
68
|
+
</>
|
|
69
|
+
) : (
|
|
70
|
+
<>
|
|
71
|
+
{children}
|
|
72
|
+
{label && <span className="btn__label">{label}</span>}
|
|
73
|
+
</>
|
|
74
|
+
)}
|
|
75
|
+
</a>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Custom Link component for internal or external navigation
|
|
81
|
+
*
|
|
82
|
+
* @component
|
|
83
|
+
* @example
|
|
84
|
+
* // Internal link
|
|
85
|
+
* <Link to="/dashboard" label="Dashboard" />
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* // External link
|
|
89
|
+
* // external property is optional
|
|
90
|
+
* // http(s), ftp, email and tel with automatically render as external
|
|
91
|
+
* <Link to="https://example.com" label="Visit website" external />
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* // Link with loading state
|
|
95
|
+
* <Link to="/profile" label="Profile" loading />
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* // Link with custom click handler en navigation
|
|
99
|
+
* <Link
|
|
100
|
+
* to="/settings"
|
|
101
|
+
* label="Settings"
|
|
102
|
+
* onClick={() => console.log('clicked')}
|
|
103
|
+
* navigate={customNavigate}
|
|
104
|
+
* />
|
|
105
|
+
*/
|
|
106
|
+
export const Link: React.FC<LinkProps> = React.memo(LinkComponent);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
export const useConnectionStatus = (): boolean => {
|
|
4
|
+
const [isOnline, setIsOnline] = useState<boolean>(navigator.onLine);
|
|
5
|
+
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
const handleOnline = () => setIsOnline(true);
|
|
8
|
+
const handleOffline = () => setIsOnline(false);
|
|
9
|
+
|
|
10
|
+
window.addEventListener('online', handleOnline);
|
|
11
|
+
window.addEventListener('offline', handleOffline);
|
|
12
|
+
|
|
13
|
+
return () => {
|
|
14
|
+
window.removeEventListener('online', handleOnline);
|
|
15
|
+
window.removeEventListener('offline', handleOffline);
|
|
16
|
+
}
|
|
17
|
+
}, []);
|
|
18
|
+
|
|
19
|
+
return isOnline;
|
|
20
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { useCallback, useState } from "react";
|
|
2
|
+
import { useConnectionStatus } from "./useConnectionStatus";
|
|
3
|
+
import { FormField, ValidationRule } from "@sio-group/form-types";
|
|
4
|
+
import { slugify } from "../utils/slugify";
|
|
5
|
+
import { createFieldState } from "../utils/create-field-state";
|
|
6
|
+
import { createFieldProps } from "../utils/create-field-props";
|
|
7
|
+
import { FieldProps, UseFormOptions, FieldState, FieldSetters } from "../types";
|
|
8
|
+
|
|
9
|
+
export const useForm = ({ disableWhenOffline }: UseFormOptions = { disableWhenOffline: true }) => {
|
|
10
|
+
const isOnline: boolean = useConnectionStatus();
|
|
11
|
+
|
|
12
|
+
const [fields, setFields] = useState<Record<string, FieldState>>({});
|
|
13
|
+
const [isDirty, setIsDirty] = useState<boolean>(false);
|
|
14
|
+
const [isBusy, setIsBusy] = useState<boolean>(false);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Runs all validations for a field.
|
|
18
|
+
* @param value
|
|
19
|
+
* @param validations
|
|
20
|
+
* @param label
|
|
21
|
+
*/
|
|
22
|
+
const validateField = (value: unknown, validations: ValidationRule<any>[], label?: string): string[] => {
|
|
23
|
+
return validations
|
|
24
|
+
.map((validation: ValidationRule<any>): string | null => validation(value, label))
|
|
25
|
+
.filter((error: string | null): error is string => !!error);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Validates the entire form.
|
|
30
|
+
*/
|
|
31
|
+
const validateForm = (): boolean => {
|
|
32
|
+
return Object.values(fields).every((field: FieldState): boolean => field.errors?.length === 0);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Register a field to the form.
|
|
37
|
+
*/
|
|
38
|
+
const register = useCallback((name: string, config: FormField, renderLayout?: boolean): FieldProps => {
|
|
39
|
+
if (!name) throw new Error('Field name is required');
|
|
40
|
+
|
|
41
|
+
const id: string = slugify(name);
|
|
42
|
+
|
|
43
|
+
const existing: FieldState = fields[name];
|
|
44
|
+
const shouldCreate: boolean = !existing || existing.type !== config.type;
|
|
45
|
+
|
|
46
|
+
let field: FieldState;
|
|
47
|
+
if (shouldCreate) {
|
|
48
|
+
field = createFieldState(name, id, config);
|
|
49
|
+
setFields((prevState) => ({
|
|
50
|
+
...prevState,
|
|
51
|
+
[name]: {
|
|
52
|
+
...field,
|
|
53
|
+
errors: validateField(field.value, field.validations, field.label)
|
|
54
|
+
}
|
|
55
|
+
}));
|
|
56
|
+
} else {
|
|
57
|
+
field = existing;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const setters: FieldSetters = {
|
|
61
|
+
handleChange: (value: unknown) => {
|
|
62
|
+
setIsDirty(true);
|
|
63
|
+
setFields((prevState) => {
|
|
64
|
+
const current = prevState[name];
|
|
65
|
+
if (!current) return prevState;
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
...prevState,
|
|
69
|
+
[name]: {
|
|
70
|
+
...current,
|
|
71
|
+
value,
|
|
72
|
+
errors: validateField(value, current.validations, current.label)
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
},
|
|
77
|
+
setFocused: (focused: boolean) => {
|
|
78
|
+
setFields((prevState) => {
|
|
79
|
+
const current: FieldState = prevState[name];
|
|
80
|
+
if (!current) return prevState;
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
...prevState,
|
|
84
|
+
[name]: {
|
|
85
|
+
...current,
|
|
86
|
+
focused,
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
setTouched: (touched: boolean) => {
|
|
92
|
+
setFields((prevState) => {
|
|
93
|
+
const current: FieldState = prevState[name];
|
|
94
|
+
if (!current) return prevState;
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
...prevState,
|
|
98
|
+
[name]: {
|
|
99
|
+
...current,
|
|
100
|
+
touched,
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
},
|
|
105
|
+
setErrors: (errors: string[]) => {
|
|
106
|
+
setFields((prevState) => {
|
|
107
|
+
const current: FieldState = prevState[name];
|
|
108
|
+
if (!current) return prevState;
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
...prevState,
|
|
112
|
+
[name]: {
|
|
113
|
+
...current,
|
|
114
|
+
errors,
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
return createFieldProps(
|
|
122
|
+
field,
|
|
123
|
+
setters,
|
|
124
|
+
config.config?.disabled || (!isOnline && disableWhenOffline),
|
|
125
|
+
renderLayout
|
|
126
|
+
);
|
|
127
|
+
}, [disableWhenOffline, isOnline, fields]);
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Unregister a field from the form.
|
|
131
|
+
* @param name
|
|
132
|
+
*/
|
|
133
|
+
const unregister = (name: string) => {
|
|
134
|
+
setFields((prevState) => {
|
|
135
|
+
const { [name]: _, ...rest } = prevState;
|
|
136
|
+
return rest;
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Set the value of a field.
|
|
142
|
+
* @param name
|
|
143
|
+
* @param value
|
|
144
|
+
*/
|
|
145
|
+
const setValue = (name: string, value: unknown) => {
|
|
146
|
+
if (fields[name]) {
|
|
147
|
+
setFields((prevState) => {
|
|
148
|
+
const current: FieldState = prevState[name];
|
|
149
|
+
if (!current) return prevState;
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
...prevState,
|
|
153
|
+
[name]: {
|
|
154
|
+
...current,
|
|
155
|
+
value,
|
|
156
|
+
errors: validateField(
|
|
157
|
+
value,
|
|
158
|
+
prevState[name].validations,
|
|
159
|
+
prevState[name].label,
|
|
160
|
+
),
|
|
161
|
+
},
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Get all form values
|
|
169
|
+
*/
|
|
170
|
+
const getValues = (): Record<string, any> => {
|
|
171
|
+
return Object.keys(fields).reduce((acc: Record<string, any>, key: string) => {
|
|
172
|
+
acc[key] = fields[key].value || null;
|
|
173
|
+
return acc;
|
|
174
|
+
}, {});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Fet the value of a specific field by name.
|
|
179
|
+
* @param name
|
|
180
|
+
*/
|
|
181
|
+
const getValue = (name: string) => {
|
|
182
|
+
return fields[name]?.value ?? null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Reset and rebuild the form.
|
|
187
|
+
*/
|
|
188
|
+
const reset = () => {
|
|
189
|
+
setFields({});
|
|
190
|
+
setIsDirty(false);
|
|
191
|
+
setIsBusy(false);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Handle submit and submit state
|
|
196
|
+
* @param onSubmit
|
|
197
|
+
*/
|
|
198
|
+
const handleSubmit = async (onSubmit: (values: Record<string, any>) => void | Promise<void>): Promise<void> => {
|
|
199
|
+
if (isBusy) return;
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
setIsBusy(true);
|
|
203
|
+
await onSubmit(getValues());
|
|
204
|
+
} finally {
|
|
205
|
+
setIsBusy(false);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Get e specific field
|
|
211
|
+
* @param name
|
|
212
|
+
*/
|
|
213
|
+
const getField = (name: string): FieldState | undefined => {
|
|
214
|
+
return fields[name] || undefined;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
register: register,
|
|
219
|
+
unregister: unregister,
|
|
220
|
+
setValue: setValue,
|
|
221
|
+
getValues: getValues,
|
|
222
|
+
getValue: getValue,
|
|
223
|
+
reset: reset,
|
|
224
|
+
isValid: validateForm,
|
|
225
|
+
isDirty: () => isDirty,
|
|
226
|
+
isBusy: () => isBusy,
|
|
227
|
+
submit: handleSubmit,
|
|
228
|
+
getField: getField,
|
|
229
|
+
};
|
|
230
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { Form } from './components/Form';
|
|
2
|
+
|
|
3
|
+
export { useForm } from './hooks/useForm';
|
|
4
|
+
|
|
5
|
+
export { Button } from './components/Button';
|
|
6
|
+
export { Link } from './components/Link';
|
|
7
|
+
export * from './components/Fields';
|
|
8
|
+
|
|
9
|
+
export type {
|
|
10
|
+
FieldProps,
|
|
11
|
+
FormConfig,
|
|
12
|
+
FormLayout,
|
|
13
|
+
ButtonProps,
|
|
14
|
+
LinkProps,
|
|
15
|
+
} from './types'
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { AcceptType, CaptureType, IconType, Option, SelectOption, SpinnerVariant } from "@sio-group/form-types";
|
|
2
|
+
import { Properties } from 'csstype';
|
|
3
|
+
|
|
4
|
+
export type BaseFieldProps = {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
label?: string;
|
|
8
|
+
placeholder?: string;
|
|
9
|
+
value: unknown;
|
|
10
|
+
errors: string[];
|
|
11
|
+
required?: boolean;
|
|
12
|
+
autocomplete?: string;
|
|
13
|
+
touched: boolean;
|
|
14
|
+
focused: boolean;
|
|
15
|
+
readOnly?: boolean;
|
|
16
|
+
disabled: boolean;
|
|
17
|
+
icon?: IconType;
|
|
18
|
+
description?: string;
|
|
19
|
+
onChange: (value: unknown) => void;
|
|
20
|
+
setFocused: (focused: boolean) => void;
|
|
21
|
+
setTouched: (touched: boolean) => void;
|
|
22
|
+
className?: string;
|
|
23
|
+
style?: Properties;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type TextareaFieldProps = BaseFieldProps & {
|
|
27
|
+
type: "textarea";
|
|
28
|
+
rows?: number;
|
|
29
|
+
cols?: number;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type FileFieldProps = BaseFieldProps & {
|
|
33
|
+
type: "file";
|
|
34
|
+
accept?: AcceptType;
|
|
35
|
+
multiple: boolean;
|
|
36
|
+
capture: CaptureType;
|
|
37
|
+
onError: (errors: string[]) => void;
|
|
38
|
+
filesize: number;
|
|
39
|
+
onFileRemove?: (file: File, index: number, files: File[]) => void;
|
|
40
|
+
onRemoveAll?: (files: File[]) => void;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type NumberFieldProps = BaseFieldProps & {
|
|
44
|
+
type: "number";
|
|
45
|
+
min: number;
|
|
46
|
+
max: number;
|
|
47
|
+
step: number;
|
|
48
|
+
spinner: boolean | SpinnerVariant;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type RangeFieldProps = BaseFieldProps & {
|
|
52
|
+
type: "range";
|
|
53
|
+
min: number;
|
|
54
|
+
max: number;
|
|
55
|
+
step: number;
|
|
56
|
+
showValue: boolean;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type DateFieldProps = BaseFieldProps & {
|
|
60
|
+
type: "date" | "time" | "datetime-local";
|
|
61
|
+
min: string;
|
|
62
|
+
max: string;
|
|
63
|
+
step: number;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export type UrlFieldProps = BaseFieldProps & {
|
|
67
|
+
type: "url";
|
|
68
|
+
allowLocalhost: boolean;
|
|
69
|
+
allowFtp: boolean;
|
|
70
|
+
secureOnly: boolean;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export type SelectFieldProps = BaseFieldProps & {
|
|
74
|
+
type: "select" | "creatable";
|
|
75
|
+
options: SelectOption[];
|
|
76
|
+
multiple: boolean;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export type RadioFieldProps = BaseFieldProps & {
|
|
80
|
+
type: "radio";
|
|
81
|
+
options: string[] | Option[];
|
|
82
|
+
inline: boolean;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export type FieldProps =
|
|
86
|
+
| FileFieldProps
|
|
87
|
+
| TextareaFieldProps
|
|
88
|
+
| NumberFieldProps
|
|
89
|
+
| RangeFieldProps
|
|
90
|
+
| DateFieldProps
|
|
91
|
+
| UrlFieldProps
|
|
92
|
+
| SelectFieldProps
|
|
93
|
+
| RadioFieldProps
|
|
94
|
+
| (BaseFieldProps & { type: Exclude<FieldType, "file" | "textarea" | "range" | "date" | "time" | "datetime-local" | "url" | "select" | "creatable" | "radio"> });
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { FieldConfigMap } from "@sio-group/form-types";
|
|
2
|
+
import { ValidationRule } from "@sio-group/form-types/src/core/valudation-rule";
|
|
3
|
+
|
|
4
|
+
export type FieldType = keyof FieldConfigMap;
|
|
5
|
+
|
|
6
|
+
export type FieldState = {
|
|
7
|
+
[K in FieldType]: {
|
|
8
|
+
type: K;
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
value: FieldConfigMap[T]['defaultValue'];
|
|
12
|
+
|
|
13
|
+
validations: ValidationRule<T>[];
|
|
14
|
+
errors: string[];
|
|
15
|
+
|
|
16
|
+
touched: boolean;
|
|
17
|
+
focused: boolean;
|
|
18
|
+
} & FieldConfigMap[K]
|
|
19
|
+
}[FieldType];
|
|
20
|
+
|
|
21
|
+
export type FieldValue<T extends FieldType> = FieldState<T>['value'];
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ComponentType, CSSProperties, ComponentPropsWithoutRef, HTMLAttributes } from "react";
|
|
2
|
+
import { FormField } from "@sio-group/form-types";
|
|
3
|
+
import { FormLayout } from "./form-layout";
|
|
4
|
+
import { ButtonProps, LinkProps } from "./ui-props";
|
|
5
|
+
|
|
6
|
+
type FormContainerProps = ComponentPropsWithoutRef<'form'>;
|
|
7
|
+
type ButtonContainerProps = HTMLAttributes<HTMLDivElement>;
|
|
8
|
+
|
|
9
|
+
export interface FormConfig {
|
|
10
|
+
fields: FormField[];
|
|
11
|
+
layout?: FormLayout[];
|
|
12
|
+
|
|
13
|
+
submitShow?: boolean;
|
|
14
|
+
submitAction: (values: Record<string, any>) => void | Promise<void>;
|
|
15
|
+
submitLabel?: string;
|
|
16
|
+
cancelShow?: boolean;
|
|
17
|
+
cancelAction?: () => void;
|
|
18
|
+
cancelLabel?: string;
|
|
19
|
+
|
|
20
|
+
buttons?: (ButtonProps | LinkProps)[];
|
|
21
|
+
extraValidation?: (values: Record<string, any>) => boolean | Promise<boolean>
|
|
22
|
+
|
|
23
|
+
className?: string;
|
|
24
|
+
style?: CSSProperties;
|
|
25
|
+
|
|
26
|
+
disableWhenOffline?: boolean;
|
|
27
|
+
|
|
28
|
+
container?: ComponentType<FormContainerProps>;
|
|
29
|
+
buttonContainer?: ComponentType<ButtonContainerProps>;
|
|
30
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
FieldProps,
|
|
3
|
+
BaseFieldProps,
|
|
4
|
+
TextareaFieldProps,
|
|
5
|
+
FileFieldProps,
|
|
6
|
+
NumberFieldProps,
|
|
7
|
+
RangeFieldProps,
|
|
8
|
+
DateFieldProps,
|
|
9
|
+
UrlFieldProps,
|
|
10
|
+
SelectFieldProps,
|
|
11
|
+
RadioFieldProps,
|
|
12
|
+
} from './field-props';
|
|
13
|
+
export type { FieldSetters } from './field-setters';
|
|
14
|
+
export type { FieldType, FieldState, FieldValue } from './field-state';
|
|
15
|
+
export type { FormConfig } from './form-config';
|
|
16
|
+
export type { FormLayout } from './form-layout';
|
|
17
|
+
export type { ButtonProps, LinkProps } from './ui-props';
|
|
18
|
+
export type { UseFormOptions } from './use-form-options';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
export type ButtonType = 'button' | 'submit' | 'reset';
|
|
4
|
+
export type Variant = 'primary' | 'secondary' | 'link';
|
|
5
|
+
export type Color = 'default' | 'error' | 'success' | 'warning' | 'info';
|
|
6
|
+
export type Size = 'sm' | 'md' | 'lg';
|
|
7
|
+
|
|
8
|
+
type BaseUiProps = {
|
|
9
|
+
variant?: Variant;
|
|
10
|
+
label?: string | React.ReactNode;
|
|
11
|
+
color?: Color;
|
|
12
|
+
size?: Size;
|
|
13
|
+
block?: boolean;
|
|
14
|
+
loading?: boolean;
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
className?: string;
|
|
17
|
+
ariaLabel?: string;
|
|
18
|
+
style?: React.CSSProperties;
|
|
19
|
+
children?: React.ReactNode;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type ButtonProps = BaseUiProps & {
|
|
23
|
+
type?: ButtonType;
|
|
24
|
+
onClick: (e: React.MouseEvent) => void;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type LinkProps = Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'onClick' | 'color'> &
|
|
28
|
+
BaseUiProps & {
|
|
29
|
+
to: string;
|
|
30
|
+
navigate?: () => void;
|
|
31
|
+
external?: boolean;
|
|
32
|
+
onClick?: (e: React.MouseEvent) => void;
|
|
33
|
+
};
|