@sio-group/form-react 0.1.0 → 0.1.3

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.
Files changed (59) hide show
  1. package/CHANGELOG.md +35 -4
  2. package/dist/index.cjs +268 -18
  3. package/dist/index.d.cts +2 -2
  4. package/dist/index.d.ts +2 -2
  5. package/dist/index.js +258 -17
  6. package/package.json +4 -3
  7. package/src/assets/scss/components/button.scss +164 -0
  8. package/src/assets/scss/components/checkbox.scss +90 -0
  9. package/src/assets/scss/components/color.scss +29 -0
  10. package/src/assets/scss/components/form-field.scss +34 -0
  11. package/src/assets/scss/components/form-states.scss +80 -0
  12. package/src/assets/scss/components/grid.scss +134 -0
  13. package/src/assets/scss/components/input.scss +112 -0
  14. package/src/assets/scss/components/link.scss +66 -0
  15. package/src/assets/scss/components/radio.scss +104 -0
  16. package/src/assets/scss/components/range.scss +52 -0
  17. package/src/assets/scss/components/select.scss +35 -0
  18. package/src/assets/scss/components/upload.scss +52 -0
  19. package/src/assets/scss/index.scss +19 -0
  20. package/src/assets/scss/tokens/_colors.scss +49 -0
  21. package/src/assets/scss/tokens/_form.scss +6 -0
  22. package/src/assets/scss/utilities/_mixins.scss +6 -0
  23. package/src/components/Button/index.tsx +106 -0
  24. package/src/components/Fields/Checkbox/index.tsx +59 -0
  25. package/src/components/Fields/Input/DateInput/index.tsx +95 -0
  26. package/src/components/Fields/Input/FileInput/index.tsx +169 -0
  27. package/src/components/Fields/Input/Input.tsx +45 -0
  28. package/src/components/Fields/Input/NumberInput/index.tsx +169 -0
  29. package/src/components/Fields/Input/RangeInput/index.tsx +77 -0
  30. package/src/components/Fields/Input/TextInput/index.tsx +65 -0
  31. package/src/components/Fields/InputWrapper/index.tsx +78 -0
  32. package/src/components/Fields/Radio/index.tsx +82 -0
  33. package/src/components/Fields/Select/index.tsx +103 -0
  34. package/src/components/Fields/Textarea/index.tsx +70 -0
  35. package/src/components/Fields/index.tsx +11 -0
  36. package/src/components/Form.tsx +163 -0
  37. package/src/components/Icon/index.tsx +16 -0
  38. package/src/components/Link/index.tsx +106 -0
  39. package/src/hooks/useConnectionStatus.ts +20 -0
  40. package/src/hooks/useForm.ts +230 -0
  41. package/src/index.ts +15 -0
  42. package/src/types/field-props.d.ts +94 -0
  43. package/src/types/field-setters.d.ts +6 -0
  44. package/src/types/field-state.d.ts +21 -0
  45. package/src/types/form-config.d.ts +30 -0
  46. package/src/types/form-layout.d.ts +6 -0
  47. package/src/types/index.ts +18 -0
  48. package/src/types/ui-props.d.ts +33 -0
  49. package/src/types/use-form-options.d.ts +3 -0
  50. package/src/utils/create-field-props.ts +115 -0
  51. package/src/utils/create-field-state.ts +99 -0
  52. package/src/utils/custom-icons.tsx +145 -0
  53. package/src/utils/file-type-icon.ts +63 -0
  54. package/src/utils/get-accept-string.ts +24 -0
  55. package/src/utils/get-column-classes.ts +21 -0
  56. package/src/utils/get-file-size.ts +9 -0
  57. package/src/utils/parse-date.ts +36 -0
  58. package/src/utils/slugify.ts +9 -0
  59. package/tsconfig.json +15 -0
@@ -0,0 +1,77 @@
1
+ import { RangeFieldProps } from "../../../../types/field-props";
2
+ import InputWrapper from "../../InputWrapper";
3
+ import { Icon } from "../../../Icon";
4
+
5
+ export const RangeInput = ({
6
+ value,
7
+ min,
8
+ max,
9
+ step = 1,
10
+ showValue = true,
11
+ onChange,
12
+
13
+ name,
14
+ id,
15
+ placeholder,
16
+ label,
17
+ required,
18
+ autocomplete,
19
+ setTouched,
20
+ setFocused,
21
+ readOnly,
22
+ disabled,
23
+ icon,
24
+
25
+ description,
26
+ focused,
27
+ errors,
28
+ touched,
29
+ type,
30
+ className,
31
+ style,
32
+ }: RangeFieldProps) => {
33
+ return (
34
+ <InputWrapper
35
+ type={type}
36
+ id={id}
37
+ label={label}
38
+ description={description}
39
+ required={required}
40
+ focused={focused}
41
+ disabled={disabled || readOnly}
42
+ hasValue={!!value}
43
+ hasError={errors.length > 0 && touched}
44
+ errors={errors}
45
+ className={className}
46
+ style={style}>
47
+ <Icon icon={icon} />
48
+ {showValue && (
49
+ <span className='form-field__range-value'>{Number(value)}</span>
50
+ )}
51
+ <input
52
+ type={type}
53
+ name={name}
54
+ id={id}
55
+ value={Number(value)}
56
+ autoComplete={autocomplete ? autocomplete : 'off'}
57
+ onChange={e => onChange(Number(e.target.value))}
58
+ onBlur={() => {
59
+ if (setTouched) setTouched(true);
60
+ if (setFocused) setFocused(false);
61
+ }}
62
+ onFocus={() => {
63
+ if (setFocused) setFocused(true);
64
+ }}
65
+ min={min}
66
+ max={max}
67
+ step={step}
68
+ aria-valuemin={min}
69
+ aria-valuemax={max}
70
+ aria-valuenow={Number(value)}
71
+ readOnly={readOnly}
72
+ disabled={disabled}
73
+ aria-label={label || placeholder}
74
+ />
75
+ </InputWrapper>
76
+ );
77
+ };
@@ -0,0 +1,65 @@
1
+ import InputWrapper from "../../InputWrapper";
2
+ import { FieldProps } from "../../../../types";
3
+ import { Icon } from "../../../Icon";
4
+
5
+ export const TextInput = ({
6
+ value,
7
+ onChange,
8
+
9
+ name,
10
+ id,
11
+ placeholder,
12
+ label,
13
+ required,
14
+ autocomplete,
15
+ setTouched,
16
+ setFocused,
17
+ readOnly,
18
+ disabled,
19
+ icon,
20
+
21
+ description,
22
+ focused,
23
+ errors,
24
+ touched,
25
+ type,
26
+ className,
27
+ style,
28
+ }: FieldProps) => {
29
+ return (
30
+ <InputWrapper
31
+ type={type}
32
+ id={id}
33
+ label={label}
34
+ description={description}
35
+ required={required}
36
+ focused={focused}
37
+ disabled={disabled || readOnly}
38
+ hasValue={!!value}
39
+ hasError={errors.length > 0 && touched}
40
+ errors={errors}
41
+ className={className}
42
+ style={style}>
43
+ <Icon icon={icon} />
44
+ <input
45
+ type={type}
46
+ name={name}
47
+ id={id}
48
+ value={value as string}
49
+ placeholder={`${placeholder ?? ''}${!label && required ? ' *' : ''}`}
50
+ autoComplete={autocomplete ? autocomplete : 'off'}
51
+ onChange={e => onChange(e.target.value)}
52
+ onBlur={() => {
53
+ if (setTouched) setTouched(true);
54
+ if (setFocused) setFocused(false);
55
+ }}
56
+ onFocus={() => {
57
+ if (setFocused) setFocused(true);
58
+ }}
59
+ readOnly={readOnly}
60
+ disabled={disabled}
61
+ aria-label={label || placeholder}
62
+ />
63
+ </InputWrapper>
64
+ );
65
+ };
@@ -0,0 +1,78 @@
1
+ import React, { memo } from 'react';
2
+
3
+ interface InputWrapperProps {
4
+ id: string;
5
+ label?: string;
6
+ description?: string | React.ReactNode;
7
+ required?: boolean;
8
+ focused?: boolean;
9
+ disabled?: boolean;
10
+ hasValue?: boolean;
11
+ hasError?: boolean;
12
+ errors?: string[];
13
+ hideLayout?: boolean;
14
+ className?: string;
15
+ style?: React.CSSProperties;
16
+ children: React.ReactNode;
17
+ type?: string;
18
+ }
19
+
20
+ const InputWrapper: React.FC<InputWrapperProps> = ({
21
+ type = 'text',
22
+ id,
23
+ label,
24
+ description,
25
+ required = false,
26
+ focused = false,
27
+ disabled = false,
28
+ hasValue = false,
29
+ hasError = false,
30
+ errors = [],
31
+ hideLayout = false,
32
+ className = '',
33
+ style = {},
34
+ children,
35
+ }) => {
36
+ const classes: string = [
37
+ 'form-field',
38
+ `form-field__${type}`,
39
+ focused && 'form-field--focused',
40
+ hasValue && 'form-field--has-value',
41
+ hasError && 'form-field--has-errors',
42
+ disabled && 'form-field--disabled',
43
+ hideLayout && 'form-field--hidden-layout',
44
+ className,
45
+ ]
46
+ .filter(Boolean)
47
+ .join(' ');
48
+
49
+ return (
50
+ <div
51
+ className={classes}
52
+ style={style}
53
+ aria-required={required}
54
+ aria-invalid={hasError}>
55
+ {label && (
56
+ <label htmlFor={id} id={`${id}-label`}>
57
+ {label} {required && <span aria-hidden={true}>*</span>}
58
+ </label>
59
+ )}
60
+
61
+ <div className={`form-field__control`}>{children}</div>
62
+ {description && (
63
+ <div className='form-field__description'>{description}</div>
64
+ )}
65
+ {hasError && errors.length > 0 && (
66
+ <ul className='form-field__errors' role='alert'>
67
+ {[...new Set(errors)].map((x: string, i: number) => (
68
+ <li className={'form-field__errors-item'} key={i}>
69
+ {x}
70
+ </li>
71
+ ))}
72
+ </ul>
73
+ )}
74
+ </div>
75
+ );
76
+ };
77
+
78
+ export default memo(InputWrapper);
@@ -0,0 +1,82 @@
1
+ import React from "react";
2
+ import { Option } from "@sio-group/form-types";
3
+ import { RadioFieldProps } from "../../../types/field-props";
4
+ import InputWrapper from "../InputWrapper";
5
+ import { Icon } from "../../Icon";
6
+
7
+ export const Radio = ({
8
+ value,
9
+ onChange,
10
+
11
+ options,
12
+ inline,
13
+
14
+ name,
15
+ id,
16
+ label,
17
+ required,
18
+ setTouched,
19
+ readOnly,
20
+ disabled,
21
+ icon,
22
+
23
+ description,
24
+ focused,
25
+ errors,
26
+ touched,
27
+ type,
28
+ className,
29
+ style,
30
+ }: RadioFieldProps) => {
31
+ return (
32
+ <InputWrapper
33
+ type={type}
34
+ id={id}
35
+ label={label}
36
+ description={description}
37
+ required={required}
38
+ focused={focused}
39
+ disabled={disabled || readOnly}
40
+ hasError={errors.length > 0 && touched}
41
+ errors={errors}
42
+ className={`${className ?? ''}${inline ? ' form-field__radio-inline' : ''}`}
43
+ style={style}
44
+ hideLayout>
45
+ <Icon icon={icon} />
46
+ {options
47
+ .filter((option: string | Option) => typeof option === 'string' || !option.hide)
48
+ .map((option: string | Option) => {
49
+ const opt: Option =
50
+ typeof option === 'string'
51
+ ? { value: option, label: option }
52
+ : option;
53
+
54
+ return (
55
+ <label
56
+ htmlFor={`${id}-${opt.value}`}
57
+ key={opt.value}
58
+ className={
59
+ value === opt.value
60
+ ? 'form-field--has-value'
61
+ : ''
62
+ }>
63
+ <input
64
+ name={name}
65
+ type={type}
66
+ id={`${id}-${opt.value}`}
67
+ value={opt.value}
68
+ checked={value === opt.value}
69
+ onChange={() => {
70
+ if (onChange) onChange(opt.value);
71
+ if (setTouched) setTouched(true);
72
+ }}
73
+ readOnly={readOnly}
74
+ disabled={opt.disabled || disabled}
75
+ />
76
+ <div>{opt.label}</div>
77
+ </label>
78
+ );
79
+ })}
80
+ </InputWrapper>
81
+ )
82
+ }
@@ -0,0 +1,103 @@
1
+ import { SelectOption } from "@sio-group/form-types";
2
+ import { SelectFieldProps } from "../../../types/field-props";
3
+ import InputWrapper from "../InputWrapper";
4
+ import { Icon } from "../../Icon";
5
+
6
+ export const Select = ({
7
+ value,
8
+ onChange,
9
+
10
+ options,
11
+ multiple,
12
+
13
+ name,
14
+ id,
15
+ placeholder,
16
+ label,
17
+ required,
18
+ autocomplete,
19
+ setTouched,
20
+ setFocused,
21
+ readOnly,
22
+ disabled,
23
+ icon,
24
+
25
+ description,
26
+ focused,
27
+ errors,
28
+ touched,
29
+ type,
30
+ className,
31
+ style,
32
+ }: SelectFieldProps) => {
33
+ const renderOption = (option: SelectOption) => {
34
+ if (typeof option === 'string') {
35
+ return <option value={option} key={option}>{option}</option>;
36
+ }
37
+
38
+ if ("options" in option) {
39
+ return (
40
+ <optgroup label={option.label} key={option.label}>
41
+ {option.options.map(renderOption)}
42
+ </optgroup>
43
+ );
44
+ }
45
+
46
+ if (option.hide) return null;
47
+
48
+ return (
49
+ <option
50
+ value={option.value}
51
+ disabled={option.disabled}
52
+ key={option.value}
53
+ >
54
+ {option.label}
55
+ </option>
56
+ )
57
+ }
58
+
59
+ return (
60
+ <InputWrapper
61
+ type={type}
62
+ id={id}
63
+ label={label}
64
+ description={description}
65
+ required={required}
66
+ focused={focused}
67
+ disabled={disabled || readOnly}
68
+ hasValue={value !== undefined && value !== null && value !== ''}
69
+ hasError={errors.length > 0 && touched}
70
+ errors={errors}
71
+ className={`${className ?? ''} ${multiple ? 'form-field__select-multiple' : 'form-field__select-single'}`}
72
+ style={style}>
73
+ <Icon icon={icon} />
74
+ <select
75
+ name={name}
76
+ id={id}
77
+ value={value as string | string[]}
78
+ autoComplete={autocomplete ? autocomplete : 'off'}
79
+ onChange={e => {
80
+ if (multiple) {
81
+ const values = Array.from(e.target.selectedOptions).map(o => o.value);
82
+ onChange(values);
83
+ } else {
84
+ onChange(e.target.value);
85
+ }
86
+ }}
87
+ onBlur={() => {
88
+ if (setTouched) setTouched(true);
89
+ if (setFocused) setFocused(false);
90
+ }}
91
+ onFocus={() => {
92
+ if (setFocused) setFocused(true);
93
+ }}
94
+ disabled={disabled}
95
+ multiple={multiple}
96
+ aria-label={label || placeholder}
97
+ >
98
+ {placeholder && <option value="" selected disabled>{placeholder}{!label && required ? ' *' : ''}</option>}
99
+ {options.map(renderOption)}
100
+ </select>
101
+ </InputWrapper>
102
+ )
103
+ }
@@ -0,0 +1,70 @@
1
+ import { TextareaFieldProps } from "../../../types/field-props";
2
+ import InputWrapper from "../InputWrapper";
3
+ import { Icon } from "../../Icon";
4
+
5
+
6
+ export const Textarea = ({
7
+ value,
8
+ onChange,
9
+
10
+ rows,
11
+ cols,
12
+
13
+ name,
14
+ id,
15
+ placeholder,
16
+ label,
17
+ required,
18
+ autocomplete,
19
+ setTouched,
20
+ setFocused,
21
+ readOnly,
22
+ disabled,
23
+ icon,
24
+
25
+ description,
26
+ focused,
27
+ errors,
28
+ touched,
29
+ type,
30
+ className,
31
+ style,
32
+ }: TextareaFieldProps) => {
33
+ return (
34
+ <InputWrapper
35
+ type={type}
36
+ id={id}
37
+ label={label}
38
+ description={description}
39
+ required={required}
40
+ focused={focused}
41
+ disabled={disabled || readOnly}
42
+ hasValue={!!value}
43
+ hasError={errors.length > 0 && touched}
44
+ errors={errors}
45
+ className={className}
46
+ style={style}>
47
+ <Icon icon={icon} />
48
+ <textarea
49
+ name={name}
50
+ id={id}
51
+ value={value as string}
52
+ rows={rows}
53
+ cols={cols}
54
+ placeholder={`${placeholder ?? ''}${!label && required ? ' *' : ''}`}
55
+ autoComplete={autocomplete ? autocomplete : 'off'}
56
+ onChange={e => onChange(e.target.value)}
57
+ onBlur={() => {
58
+ if (setTouched) setTouched(true);
59
+ if (setFocused) setFocused(false);
60
+ }}
61
+ onFocus={() => {
62
+ if (setFocused) setFocused(true);
63
+ }}
64
+ readOnly={readOnly}
65
+ disabled={disabled}
66
+ aria-label={label || placeholder}
67
+ />
68
+ </InputWrapper>
69
+ );
70
+ }
@@ -0,0 +1,11 @@
1
+ export * from './Checkbox';
2
+ export * from './Input/Input';
3
+ export { NumberInput } from './Input/NumberInput';
4
+ export { RangeInput } from './Input/RangeInput';
5
+ export { DateInput } from './Input/DateInput';
6
+ export { FileInput } from './Input/FileInput';
7
+ export { TextInput } from './Input/TextInput';
8
+ export * from './InputWrapper';
9
+ export * from './Radio';
10
+ export * from './Select';
11
+ export * from './Textarea';
@@ -0,0 +1,163 @@
1
+ import { ButtonContainerProps, FormConfig, FormContainerProps } from "../types/form-config";
2
+ import { FormField } from "@sio-group/form-types";
3
+ import React, { useMemo } from "react";
4
+ import { useForm } from "../hooks/useForm";
5
+ import { ButtonProps, LinkProps, FormLayout, RadioFieldProps, SelectFieldProps, TextareaFieldProps } from "../types";
6
+ import { getColumnClasses } from "../utils/get-column-classes";
7
+ import { Checkbox, Input, Radio, Select, Textarea } from "./Fields";
8
+ import { Link } from "./Link";
9
+ import { Button } from "./Button";
10
+
11
+ const DefaultContainer: React.FC<FormContainerProps> = ({ children, ...props }) => (
12
+ <form {...props} noValidate>{children}</form>
13
+ );
14
+
15
+ const DefaultButtonContainer: React.FC<ButtonContainerProps> = ({ children }) => (
16
+ <div className="btn-group">{children}</div>
17
+ );
18
+
19
+ export const Form = ({
20
+ fields,
21
+ layout = [],
22
+ submitShow = true,
23
+ submitAction,
24
+ submitLabel = 'Bewaar',
25
+ cancelShow = false,
26
+ cancelAction,
27
+ cancelLabel = 'Annuleren',
28
+ buttons = [],
29
+ extraValidation = () => true,
30
+ className,
31
+ style,
32
+ disableWhenOffline = false,
33
+ container: Container = DefaultContainer,
34
+ buttonContainer: ButtonContainer = DefaultButtonContainer,
35
+ }: FormConfig) => {
36
+ const { register, getValues, isValid, isBusy, isDirty, reset, submit } = useForm({
37
+ disableWhenOffline,
38
+ });
39
+
40
+ const loadElement = (field: FormField, renderLayout: boolean = false) => {
41
+ switch (field.type) {
42
+ case 'textarea':
43
+ return <Textarea {...register(field.name, field, renderLayout) as TextareaFieldProps} key={field.name} />;
44
+
45
+ case 'checkbox':
46
+ return <Checkbox {...register(field.name, field, renderLayout)} key={field.name} />;
47
+
48
+ case 'radio':
49
+ return <Radio {...register(field.name, field, renderLayout) as RadioFieldProps} key={field.name} />;
50
+
51
+ case 'select':
52
+ return <Select {...register(field.name, field, renderLayout) as SelectFieldProps} key={field.name} />
53
+
54
+ case 'text':
55
+ case 'search':
56
+ case 'email':
57
+ case 'tel':
58
+ case 'password':
59
+ case 'url':
60
+ case 'number':
61
+ case 'range':
62
+ case 'date':
63
+ case 'time':
64
+ case 'datetime-local':
65
+ case 'color':
66
+ case 'file':
67
+ case 'hidden':
68
+ return <Input {...register(field.name, field, renderLayout)} key={field.name} />;
69
+
70
+ default:
71
+ return <div key={field.name}>{field.type} to implement</div>;
72
+ }
73
+ };
74
+
75
+ const renderFields = () => {
76
+ const target: FormLayout[] | FormField[] = layout?.length ? layout : fields;
77
+ const hasLayout: boolean = layout?.length !== 0 || fields.some((f: FormField) => f.config.layout);
78
+
79
+ const content = target.map((element) => {
80
+ if ('fields' in element) {
81
+ const classes: string = getColumnClasses(element.layout, element.layout?.className);
82
+
83
+ return (
84
+ <div
85
+ key={element.fields.join('-')}
86
+ className={classes}
87
+ style={element.layout?.style}
88
+ >
89
+ {element.fields.map((name: string) => {
90
+ const field: FormField | undefined = fieldMap.get(name);
91
+ return field ? loadElement(field) : null;
92
+ })}
93
+ </div>
94
+ );
95
+ }
96
+
97
+ return loadElement(element, hasLayout);
98
+ });
99
+
100
+
101
+ return hasLayout
102
+ ? <div className="sio-row">{content}</div>
103
+ : content
104
+ };
105
+
106
+ const fieldMap = useMemo(() => {
107
+ const map = new Map<string, FormField>();
108
+ fields.forEach((f: FormField) => map.set(f.name, f));
109
+ return map;
110
+ }, [fields]);
111
+
112
+ const handleCancel = () => {
113
+ cancelAction?.();
114
+ reset();
115
+ };
116
+
117
+ const handleSubmit = async () => {
118
+ await submit(() => submitAction(getValues()));
119
+ reset();
120
+ };
121
+
122
+ const renderButton = (props: ButtonProps | LinkProps, i: number) => {
123
+ return props.type === 'link' ? (
124
+ <Link {...(props as LinkProps)} key={i} />
125
+ ) : (
126
+ <Button {...(props as ButtonProps)} key={i} />
127
+ );
128
+ }
129
+
130
+ const renderButtons = () => {
131
+ return submitShow || cancelShow || buttons.length ? (
132
+ <ButtonContainer>
133
+ {submitShow && (
134
+ <Button
135
+ type='submit'
136
+ onClick={handleSubmit}
137
+ variant='primary'
138
+ label={submitLabel}
139
+ loading={isBusy()}
140
+ disabled={!isValid() || !extraValidation(getValues())}
141
+ />
142
+ )}
143
+ {cancelShow && (
144
+ <Button
145
+ type='button'
146
+ onClick={handleCancel}
147
+ variant='secondary'
148
+ label={cancelLabel}
149
+ disabled={!isDirty()}
150
+ />
151
+ )}
152
+ {buttons?.map(renderButton)}
153
+ </ButtonContainer>
154
+ ) : null;
155
+ };
156
+
157
+ return (
158
+ <Container className={className} style={style} noValidate>
159
+ {renderFields()}
160
+ {renderButtons()}
161
+ </Container>
162
+ );
163
+ }
@@ -0,0 +1,16 @@
1
+ import { IconType } from "@sio-group/form-types";
2
+ import { ReactNode } from "react";
3
+
4
+ export const Icon = ({ icon }: { icon?:IconType }): ReactNode | null => {
5
+ if (!icon) return null;
6
+
7
+ if (typeof icon === 'string') {
8
+ return <span className='form-field__icon'><i className={icon} /></span>;
9
+ }
10
+
11
+ switch (icon.type) {
12
+ case 'class': return <span className='form-field__icon'><i className={icon.value} /></span>;
13
+ case 'emoji': return <span className='form-field__icon'><span>{icon.value}</span></span>;
14
+ case 'html': return <span dangerouslySetInnerHTML={{ __html: icon.value }} className='form-field__icon'></span>;
15
+ }
16
+ }