>;
47 |
48 | export type CustomComponents<
49 | P extends IFieldComponentProps = IFieldComponentProps
50 | > = {
51 | [type: string]: {
52 | component: CustomComponent;
53 | defaultValue?: any;
54 | validation?: any[][];
55 | };
56 | };
57 |
58 | export interface IFieldConfig<
59 | P extends IFieldComponentProps = IFieldComponentProps
60 | > {
61 | type: string;
62 | name: string;
63 | group: 'input' | 'content';
64 | icon: React.ReactNode;
65 | dataType: string;
66 | defaultValue: any;
67 | component: CustomComponent
;
68 | settings: Fields;
69 | validation: (config: Record) => any[][];
70 | }
71 |
--------------------------------------------------------------------------------
/src/Fields/MultiSelect/MultiSelectComponent.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { IFieldComponentProps } from '../../types';
3 | import MultiSelect, { MultiSelectProps } from '@rowy/multiselect';
4 |
5 | import FieldAssistiveText from '../../FieldAssistiveText';
6 |
7 | export interface IMultiSelectComponentProps
8 | extends IFieldComponentProps,
9 | Omit, 'value' | 'onChange' | 'options' | 'label'> {
10 | options: (string | { value: string; label: React.ReactNode })[];
11 | }
12 |
13 | export default function MultiSelectComponent({
14 | field: { onChange, onBlur, value, ref },
15 | fieldState,
16 | formState,
17 |
18 | name,
19 | useFormMethods,
20 |
21 | errorMessage,
22 | assistiveText,
23 |
24 | options = [],
25 | ...props
26 | }: IMultiSelectComponentProps) {
27 | return (
28 |
48 | {errorMessage}
49 |
50 |
54 | {assistiveText}
55 |
56 | >
57 | ),
58 | onBlur,
59 | 'data-type': 'multi-select',
60 | 'data-label': props.label ?? '',
61 | inputRef: ref,
62 | }}
63 | />
64 | );
65 | }
66 |
--------------------------------------------------------------------------------
/src/Fields/ContentParagraph/ContentParagraphComponent.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { IFieldComponentProps } from '../../types';
3 | import DOMPurify from 'dompurify';
4 |
5 | import { Typography, TypographyProps } from '@mui/material';
6 |
7 | const rootStyles = { mb: -1.5, whiteSpace: 'pre-line', cursor: 'default' };
8 |
9 | export interface IContentParagraphComponentProps
10 | extends IFieldComponentProps,
11 | Partial> {}
12 |
13 | export default function ContentParagraphComponent({
14 | field,
15 | fieldState,
16 | formState,
17 |
18 | index,
19 | label,
20 | children,
21 | className,
22 |
23 | disabled,
24 | errorMessage,
25 | name,
26 | useFormMethods,
27 | ...props
28 | }: IContentParagraphComponentProps) {
29 | if (children)
30 | return (
31 |
40 | {children}
41 |
42 | );
43 |
44 | const renderedLabel =
45 | typeof label === 'string' ? DOMPurify.sanitize(label) : null;
46 |
47 | if (renderedLabel)
48 | return (
49 |
59 | );
60 |
61 | return (
62 |
71 | {label}
72 |
73 | );
74 | }
75 |
--------------------------------------------------------------------------------
/src/ScrollableDialogContent.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react';
2 | import useScrollInfo from 'react-element-scroll-hook';
3 |
4 | import {
5 | Divider,
6 | DividerProps,
7 | DialogContent,
8 | DialogContentProps,
9 | } from '@mui/material';
10 |
11 | const MemoizedDialogContent = memo(function MemoizedDialogContent_({
12 | setRef,
13 | ...props
14 | }: DialogContentProps & { setRef: any }) {
15 | return ;
16 | });
17 |
18 | export interface IScrollableDialogContentProps extends DialogContentProps {
19 | disableTopDivider?: boolean;
20 | disableBottomDivider?: boolean;
21 | dividerSx?: DividerProps['sx'];
22 | topDividerSx?: DividerProps['sx'];
23 | bottomDividerSx?: DividerProps['sx'];
24 | }
25 |
26 | export default function ScrollableDialogContent({
27 | disableTopDivider = false,
28 | disableBottomDivider = false,
29 | dividerSx = [],
30 | topDividerSx = [],
31 | bottomDividerSx = [],
32 | ...props
33 | }: IScrollableDialogContentProps) {
34 | const [scrollInfo, setRef] = useScrollInfo();
35 |
36 | return (
37 | <>
38 | {!disableTopDivider && scrollInfo.y.percentage !== null && (
39 | 0 ? 'visible' : 'hidden',
42 | }}
43 | sx={[
44 | ...(Array.isArray(dividerSx) ? dividerSx : [dividerSx]),
45 | ...(Array.isArray(topDividerSx) ? topDividerSx : [topDividerSx]),
46 | ]}
47 | />
48 | )}
49 |
50 |
51 |
52 | {!disableBottomDivider && scrollInfo.y.percentage !== null && (
53 |
64 | )}
65 | >
66 | );
67 | }
68 |
--------------------------------------------------------------------------------
/src/Fields/Checkbox/CheckboxComponent.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { IFieldComponentProps } from '../../types';
3 |
4 | import { FormControlLabel, Checkbox, CheckboxProps } from '@mui/material';
5 |
6 | import FieldErrorMessage from '../../FieldErrorMessage';
7 | import FieldAssistiveText from '../../FieldAssistiveText';
8 |
9 | export interface ICheckboxComponentProps
10 | extends IFieldComponentProps,
11 | Omit<
12 | CheckboxProps,
13 | 'name' | 'onChange' | 'checked' | 'ref' | 'value' | 'onBlur'
14 | > {}
15 |
16 | export default function CheckboxComponent({
17 | field: { onChange, onBlur, value, ref },
18 | fieldState,
19 | formState,
20 |
21 | name,
22 | useFormMethods,
23 |
24 | label,
25 | errorMessage,
26 | assistiveText,
27 |
28 | required,
29 |
30 | ...props
31 | }: ICheckboxComponentProps) {
32 | return (
33 | {
39 | onChange(e.target.checked);
40 | onBlur();
41 | }}
42 | inputProps={
43 | {
44 | 'data-type': 'checkbox',
45 | 'data-label': label ?? '',
46 | } as any
47 | }
48 | sx={[
49 | {
50 | '.MuiFormControlLabel-root:not(.Mui-disabled):hover &': {
51 | bgcolor: 'action.hover',
52 | },
53 | },
54 | ...(Array.isArray(props.sx)
55 | ? props.sx
56 | : props.sx
57 | ? [props.sx]
58 | : []),
59 | ]}
60 | inputRef={ref}
61 | />
62 | }
63 | onBlur={onBlur}
64 | label={
65 | <>
66 | {label}
67 | {required && <> *>}
68 |
69 | {errorMessage}
70 |
71 | {assistiveText}
72 |
73 | >
74 | }
75 | sx={{ mr: 0, display: 'flex' }}
76 | />
77 | );
78 | }
79 |
--------------------------------------------------------------------------------
/src/Fields/ShortText/ShortTextSettings.ts:
--------------------------------------------------------------------------------
1 | import { IFieldConfig } from '../../types';
2 | import { FieldType } from '../../constants/fields';
3 |
4 | export const ShortTextSettings: IFieldConfig['settings'] = [
5 | {
6 | type: FieldType.shortText,
7 | name: 'placeholder',
8 | label: 'Placeholder',
9 | defaultValue: '',
10 | },
11 | {
12 | type: FieldType.shortText,
13 | name: 'maxCharacters',
14 | label: 'Max characters',
15 | conditional: 'check',
16 | defaultValue: undefined,
17 | format: 'number',
18 | },
19 | {
20 | type: FieldType.singleSelect,
21 | name: 'format',
22 | label: 'Format',
23 | defaultValue: '',
24 | options: [
25 | { value: '', label: 'None' },
26 | { value: 'email', label: 'Email' },
27 | { value: 'phone', label: 'Phone' },
28 | { value: 'number', label: 'Number' },
29 | { value: 'url', label: 'URL' },
30 | { value: 'twitter', label: 'Twitter handle' },
31 | { value: 'linkedin', label: 'LinkedIn URL' },
32 | ],
33 | },
34 | {
35 | type: FieldType.singleSelect,
36 | name: 'autoComplete',
37 | label: 'Autocomplete Suggestion',
38 | defaultValue: '',
39 | options: [
40 | { value: '', label: 'None' },
41 | { value: 'name', label: 'Full Name' },
42 | { value: 'given-name', label: 'Given Name' },
43 | { value: 'family-name', label: 'Family Name' },
44 | { value: 'additional-name', label: 'Middle Name' },
45 | { value: 'email', label: 'Email' },
46 | { value: 'organization', label: 'Organisation' },
47 | { value: 'organization-title', label: 'Organisation title' },
48 | { value: 'street-address', label: 'Street address' },
49 | { value: 'country-name', label: 'Country name' },
50 | { value: 'bday', label: 'Birthday' },
51 | { value: 'tel', label: 'Phone number' },
52 | { value: 'url', label: 'URL' },
53 | ],
54 | assistiveText:
55 | 'Phones will suggest this value when the user clicks on this field. See all available values',
56 | freeText: true,
57 | },
58 | ];
59 |
60 | export default ShortTextSettings;
61 |
--------------------------------------------------------------------------------
/src/Fields/ShortText/ShortTextValidation.ts:
--------------------------------------------------------------------------------
1 | export const ShortTextValidation = (config: Record) => {
2 | const validation: any[][] = [['string'], ['trim']];
3 |
4 | switch (config.format) {
5 | case 'email':
6 | validation.push([
7 | 'email',
8 | 'Please enter the email in the format: mail@domain.com',
9 | ]);
10 | break;
11 |
12 | case 'emailWithName':
13 | validation.push([
14 | 'matches',
15 | /(?:"?([^"]*)"?\s)?(?:(.+@[^>]+)>?)/, // https://stackoverflow.com/a/14011481
16 | {
17 | message:
18 | 'Please enter the email in the format: Name ',
19 | excludeEmptyString: true,
20 | },
21 | ]);
22 | break;
23 |
24 | case 'phone':
25 | validation.push([
26 | 'matches',
27 | /^(?=(?:\D*\d\D*){8,14}$)[- \d()+]*/, // https://stackoverflow.com/a/28228199
28 | {
29 | message: 'Please enter a valid phone number',
30 | excludeEmptyString: true,
31 | },
32 | ]);
33 | break;
34 |
35 | case 'number':
36 | validation[0] = ['number'];
37 | // https://github.com/jquense/yup/issues/298#issuecomment-559017330
38 | validation.push([
39 | 'transform',
40 | (value: any) => {
41 | if ((typeof value === 'string' && value === '') || isNaN(value))
42 | return null;
43 | return value;
44 | },
45 | ]);
46 | validation.push(['nullable']);
47 | break;
48 |
49 | case 'url':
50 | validation.push([
51 | 'url',
52 | 'Please enter the URL in the format: https://example.com',
53 | ]);
54 | break;
55 |
56 | case 'twitter':
57 | validation.push([
58 | 'matches',
59 | /^@?(\w){1,15}$/, //https://stackoverflow.com/a/8650024
60 | {
61 | message: 'Please enter the Twitter account in the format: @username',
62 | excludeEmptyString: true,
63 | },
64 | ]);
65 | break;
66 |
67 | case 'linkedin':
68 | validation.push([
69 | 'matches',
70 | /^https?:\/\/([a-z]+.)?linkedin\.com\/in\/[a-zA-z\d-]+/,
71 | {
72 | message:
73 | 'Please enter the LinkedIn URL in the format: https://linkedin.com/in/your-name',
74 | excludeEmptyString: true,
75 | },
76 | ]);
77 | break;
78 |
79 | default:
80 | break;
81 | }
82 |
83 | if (typeof config.maxCharacters === 'number')
84 | validation.push([
85 | 'max',
86 | config.maxCharacters,
87 | 'You have reached the character limit',
88 | ]);
89 |
90 | return validation;
91 | };
92 |
93 | export default ShortTextValidation;
94 |
--------------------------------------------------------------------------------
/src/Fields/Date/DateComponent.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { IFieldComponentProps } from '../../types';
3 |
4 | import {
5 | LocalizationProvider,
6 | DatePicker,
7 | DatePickerProps,
8 | } from '@mui/x-date-pickers';
9 | import { TextField, TextFieldProps } from '@mui/material';
10 | import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
11 |
12 | import FieldAssistiveText from '../../FieldAssistiveText';
13 |
14 | export interface IDateComponentProps
15 | extends IFieldComponentProps,
16 | Omit<
17 | DatePickerProps,
18 | 'label' | 'name' | 'onChange' | 'value' | 'ref'
19 | > {
20 | TextFieldProps: TextFieldProps;
21 | }
22 |
23 | export default function DateComponent({
24 | field: { onChange, onBlur, value, ref },
25 | fieldState,
26 | formState,
27 |
28 | name,
29 | useFormMethods,
30 |
31 | errorMessage,
32 | assistiveText,
33 |
34 | TextFieldProps,
35 | ...props
36 | }: IDateComponentProps) {
37 | let transformedValue: any = null;
38 | if (value && 'toDate' in value) transformedValue = value.toDate();
39 | else if (value !== undefined) transformedValue = value;
40 |
41 | return (
42 |
43 | (
54 |
64 | {errorMessage}
65 |
66 |
70 | {assistiveText}
71 |
72 | >
73 | )
74 | }
75 | data-type="date"
76 | data-label={props.label ?? ''}
77 | inputProps={{
78 | ...props.inputProps,
79 | required: false,
80 | }}
81 | sx={{
82 | '& .MuiInputBase-input': { fontVariantNumeric: 'tabular-nums' },
83 | ...TextFieldProps?.sx,
84 | }}
85 | />
86 | )}
87 | />
88 |
89 | );
90 | }
91 |
--------------------------------------------------------------------------------
/src/Form.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useForm, UseFormProps, FieldValues } from 'react-hook-form';
3 | import _isEmpty from 'lodash-es/isEmpty';
4 |
5 | import useFormSettings from './useFormSettings';
6 | import FormFields from './FormFields';
7 | import AutoSave from './AutoSave';
8 | import SubmitButton, { ISubmitButtonProps } from './SubmitButton';
9 | import SubmitError, { ISubmitErrorProps } from './SubmitError';
10 |
11 | import { Fields, CustomComponents } from './types';
12 |
13 | export interface IFormProps {
14 | fields: Fields;
15 | values?: FieldValues;
16 | onSubmit: (
17 | values: FieldValues,
18 | event?: React.BaseSyntheticEvent