src/components/atoms/Form/FormBuilder.tsx
import React, { useCallback, useContext, useMemo } from 'react';
import Form, { useForm, FormInstance } from 'rc-field-form';
import { Rule } from 'rc-field-form/lib/interface';
import { FormConfig } from './FormBuilderConfig';
import Row from '../Row';
import Col from '../Col';
import FormField from './FormField';
import { ConfigContext } from '../../../theme/FowThemeProvider';
import { tr } from './locales/tr';
import { en } from './locales/en';
import { uuidv4 } from '../../../utils/uuid';
const localization = { tr, en };
type FieldTypes =
| 'text'
| 'textarea'
| 'rich-textarea'
| 'number'
| 'url'
| 'checkbox'
| 'checkbox-group'
| 'radio-group'
| 'single-select'
| 'multiple-select'
| 'time'
| 'time-range'
| 'date'
| 'date-range'
| 'date-time'
| 'date-time-range'
| string;
type Option = {
value: string | number;
label: string;
};
type Field = {
key: string;
name: string;
label?: string;
required?: boolean;
disabled?: boolean;
type: FieldTypes;
hint?: string;
options?: Option[];
component?: React.ReactNode;
rules?: Rule[];
requiredMessage?: string;
valueProp?: 'value' | 'checked' | 'select' | string;
initialVisibleField?: boolean;
props?: any;
hidden?: boolean;
dependencies?: string[];
value: any;
};
type Config = {
fields: Field[];
name?: string;
id?: string;
currencyList?: Currency[];
baseCurrency?: string;
disabledAutoFocused?: boolean;
disabledFluid?: false;
};
type Currency = {
/**
* value of currency
*/
value: string;
/**
* name of currency
*/
name: string;
};
export interface FormBuilderProps {
initialValues?: any;
formInstance?: FormInstance;
onSubmit: (values: any) => void;
config: Config;
showOnlyMandatory?: boolean;
onValuesChange?: (value: any, values: any) => void;
onFieldsChange?: (changedFields: any, allFields: any) => void;
wrapWithForm?: boolean;
disableCurrency?: boolean;
}
const FormBuilder = ({
initialValues,
disableCurrency = false,
formInstance,
onSubmit,
showOnlyMandatory = false,
onValuesChange,
onFieldsChange,
config = {
fields: [],
name: undefined,
id: undefined,
currencyList: [],
baseCurrency: undefined,
disabledAutoFocused: false,
disabledFluid: false,
},
wrapWithForm = true,
}: FormBuilderProps) => {
const [formRef] = useForm(formInstance);
const { language } = useContext(ConfigContext);
const formId = useMemo(() => uuidv4(), []);
const getPlaceholderProp = useCallback(
(field) => {
if (field.placeholder) return field.placeholder;
if (
field.type === 'single-select' ||
field.type === 'multiple-select'
)
return [localization[language].select];
if (field.type === 'date-time' || field.type === 'date')
return [localization[language].pickDate];
if (field.type === 'date-time-range' || field.type === 'date-range')
return [
localization[language].startDate,
localization[language].endDate,
];
return field.label || '';
},
[language],
);
const getValueProp = useCallback((field: Field) => {
if (field.valueProp || field?.props?.value)
return field.valueProp || field?.props?.value;
if (field.type === 'checkbox' || field.type === 'checkbox-group')
return 'checked';
return 'value';
}, []);
const getinitialVisibleFieldProp = useCallback((field: Field) => {
if (typeof field.initialVisibleField === 'boolean')
return field.initialVisibleField;
return true;
}, []);
// const getLabelProp = (field) =>
// if (field.type === 'checkbox') return null;
// field.label;
const calculatedProps = useCallback(
(field: Field) => {
switch (field.type) {
case 'price':
return {
...field.props,
setFormFieldValue: (value: string) => {
formRef.setFieldsValue({
currencyId: value,
});
},
initialValue: {
number: initialValues?.[field.name],
currency: initialValues?.currencyId,
},
currencies: config.currencyList,
baseCurrency: config.baseCurrency,
disableCurrency,
};
case 'checkbox':
return {
children: field.label,
};
case 'checkbox-group':
case 'radio-group':
return {
options: field?.options?.map((item) => ({
label: item.label,
value: item.value,
})),
...field.props,
};
case 'single-select':
return {
mode: 'single',
allowClear: true,
allowSearch: true,
options: field?.options?.map((item) => ({
label: item.label,
value: item.value,
})),
...field.props,
};
case 'multiple-select':
return {
mode: 'multiple',
allowClear: true,
allowSearch: true,
options: field?.options?.map((item) => ({
label: item.label,
value: item.value,
})),
...field.props,
};
case 'date':
case 'date-time':
case 'date-range':
return {
showTime: false,
...(field.required === false
? {
defaultValue: false,
}
: {}),
...field.props,
};
default:
return { ...field.props };
}
},
[config.currencyList, initialValues],
);
let focused = false;
const renderField = useCallback(
(field: Field) => {
const fieldComponent = field.component
? field.component
: FormConfig.fields.getFields()[field.type]?.component;
if (!fieldComponent) {
throw new Error(
`FormBuilderError: "${field.type}" has not been added to config's field types.`,
);
}
const predefineFieldRules = field.rules
? field.rules
: FormConfig.fields.getFields()[field.type].rules || [];
const FieldComponent = fieldComponent;
return (
<Col
xs={
// eslint-disable-next-line no-nested-ternary
!config?.disabledFluid
? field.props?.fluid
? 12
: 6
: field?.props?.columnSize || 12
}
style={{
display: field.hidden ? 'none' : 'block',
}}>
<FormField
key={field.key}
valuePropName={getValueProp(field)}
type={field.type}
name={field.name}
label={field.label}
hidden={field.hidden}
rules={[
{
required: field.required,
message:
field.requiredMessage ||
'This field is required',
},
...predefineFieldRules,
...(field.rules ? field.rules : []),
]}
hint={field.hint}
initialVisibleField={getinitialVisibleFieldProp(field)}
dependencies={field.dependencies}>
<FieldComponent
key={field.key}
placeholder={getPlaceholderProp(field)}
inputProps={{
placeholder: getPlaceholderProp(field),
}}
type={field?.type}
ref={(ref: any) => {
if (
ref &&
!ref.value &&
!focused &&
!config?.disabledAutoFocused
) {
focused = true;
setTimeout(() => {
if (typeof ref.focus === 'function') {
ref.focus();
}
if (typeof ref.onFocus === 'function') {
ref.onFocus();
}
}, 300);
}
}}
{...calculatedProps(field)}
/>
</FormField>
</Col>
);
},
[config, initialValues],
);
const fields = useMemo(() => {
if (showOnlyMandatory) {
return config.fields
.filter((field) => field.required)
.map((field) => renderField(field));
}
return config.fields.map((field) => renderField(field));
}, [config.fields, renderField, showOnlyMandatory]);
return (
<div>
{wrapWithForm ? (
<Form
id={config.id || formId}
name={config.name}
form={formRef}
onValuesChange={onValuesChange}
onFieldsChange={(changedFields, allFields) => {
if (onFieldsChange) {
onFieldsChange(changedFields, allFields);
}
}}
onFinishFailed={({ errorFields }) => {
const name = errorFields[0].name[0];
const input = document.querySelector(
`*[name=${name}]`,
) as HTMLElement;
input?.scrollIntoView({
behavior: 'smooth',
block: 'start',
inline: 'start',
});
setTimeout(() => {
input?.focus();
}, 300);
}}
onFinish={onSubmit}
initialValues={initialValues}>
<Row>{fields}</Row>
</Form>
) : (
<Row>{fields}</Row>
)}
</div>
);
};
export default FormBuilder;