@matthew.ngo/react-dynamic-form
is a powerful and flexible React library for creating dynamic forms based on a configuration object. It leverages react-hook-form
for form handling and yup
for validation, offering a streamlined and efficient way to build complex forms with minimal code. It also supports conditional fields, custom inputs, and theming with styled-components
.
View the live demo here: https://maemreyo.github.io/react-dynamic-form/
- Dynamic Form Generation: Create forms from a simple JSON configuration.
-
react-hook-form
Integration: Utilizesreact-hook-form
for efficient form state management. -
yup
Validation: Supports schema-based validation usingyup
. - Conditional Fields: Show or hide fields based on other field values.
- Custom Inputs: Easily integrate your own custom input components.
-
Theming: Customize the look and feel with
styled-components
. -
Layout Options: Supports
flex
andgrid
layouts. - Auto Save: Option to automatically save form data at intervals.
- Local Storage: Persist form data in local storage.
-
Debounced
onChange
: Provides a debouncedonChange
callback. -
Built-in Input Types: Supports a wide range of input types:
text
number
checkbox
select
textarea
email
password
tel
url
radio
date
switch
time
datetime-local
combobox
- Highly Customizable
- Production Ready
- Basic test coverage
yarn add @matthew.ngo/react-dynamic-form react-hook-form yup @hookform/resolvers styled-components
or
npm install @matthew.ngo/react-dynamic-form react-hook-form yup @hookform/resolvers styled-components
import React from 'react';
import { DynamicForm, DynamicFormProps } from '@matthew.ngo/react-dynamic-form';
const basicFormConfig: DynamicFormProps['config'] = {
firstName: {
type: 'text',
label: 'First Name',
defaultValue: 'John',
validation: {
required: { value: true, message: 'This field is required' },
},
},
lastName: {
type: 'text',
label: 'Last Name',
defaultValue: 'Doe',
},
email: {
type: 'email',
label: 'Email',
validation: {
required: { value: true, message: 'This field is required' },
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i,
message: 'Invalid email address',
},
},
},
};
const App: React.FC = () => {
const handleSubmit = (data: any) => {
console.log(data);
};
return (
<div>
<DynamicForm config={basicFormConfig} onSubmit={handleSubmit} />
</div>
);
};
export default App;
import React from 'react';
import { DynamicForm, DynamicFormProps } from '@matthew.ngo/react-dynamic-form';
import { FlexLayout } from '@matthew.ngo/react-dynamic-form';
const advancedFormConfig: DynamicFormProps['config'] = {
firstName: {
label: 'First Name',
type: 'text',
defaultValue: 'John',
validation: {
required: { value: true, message: 'This field is required' },
},
classNameConfig: {
input: 'border border-gray-400 p-2 rounded w-full',
label: 'block text-gray-700 text-sm font-bold mb-2',
},
},
subscribe: {
label: 'Subscribe to newsletter?',
type: 'checkbox',
defaultValue: true,
classNameConfig: {
checkboxInput: 'mr-2 leading-tight',
label: 'block text-gray-700 text-sm font-bold mb-2',
},
},
country: {
label: 'Country',
type: 'select',
defaultValue: 'US',
options: [
{ value: 'US', label: 'United States' },
{ value: 'CA', label: 'Canada' },
{ value: 'UK', label: 'United Kingdom' },
],
classNameConfig: {
select: 'border border-gray-400 p-2 rounded w-full',
label: 'block text-gray-700 text-sm font-bold mb-2',
},
},
gender: {
label: 'Gender',
type: 'radio',
defaultValue: 'male',
options: [
{ value: 'male', label: 'Male' },
{ value: 'female', label: 'Female' },
{ value: 'other', label: 'Other' },
],
classNameConfig: {
radioGroup: 'flex items-center',
radioLabel: 'mr-4',
radioButton: 'mr-1',
label: 'block text-gray-700 text-sm font-bold mb-2',
},
},
dynamicField: {
label: 'Dynamic Field',
type: 'text',
defaultValue: '',
conditional: {
when: 'firstName',
operator: 'is',
value: 'ShowDynamic',
fields: ['dynamicField'],
},
classNameConfig: {
input: 'border border-gray-400 p-2 rounded w-full',
label: 'block text-gray-700 text-sm font-bold mb-2',
},
},
asyncEmail: {
label: 'Async Email Validation',
type: 'email',
validation: {
required: { value: true, message: 'This field is required' },
validate: async (value: string): Promise<any> => {
// Simulate an async API call
const isValid = await new Promise<boolean>((resolve) => {
setTimeout(() => {
resolve(value !== 'test@example.com');
}, 1000);
});
return isValid || 'Email already exists (async check)';
},
},
classNameConfig: {
input: 'border border-gray-400 p-2 rounded w-full',
label: 'block text-gray-700 text-sm font-bold mb-2',
errorMessage: 'text-red-500 text-xs italic',
},
},
};
const App: React.FC = () => {
const handleSubmit = (data: any) => {
console.log(data);
alert(JSON.stringify(data));
};
return (
<DynamicForm
config={advancedFormConfig}
renderLayout={({ children, ...rest }) => (
<FlexLayout {...rest}>{children}</FlexLayout>
)}
formClassNameConfig={{
formContainer: 'p-6 border border-gray-300 rounded-md',
inputWrapper: 'mb-4',
errorMessage: 'text-red-600',
button:
'bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded w-full',
}}
autoSave={{
interval: 3000,
save: (data) => console.log('Auto-saving:', data),
}}
enableLocalStorage={true}
resetOnSubmit={true}
focusFirstError={true}
debounceOnChange={300}
onSubmit={handleSubmit}
onChange={(data) => console.log('Debounced change:', data)}
onFormReady={(form) => {
console.log('Form is ready:', form);
}}
showSubmitButton={true}
showInlineError={true}
showErrorSummary={true}
/>
);
};
export default App;
The DynamicForm
component accepts the following props:
Prop | Type | Default | Description |
---|---|---|---|
config |
FormConfig |
{} |
The configuration object for the form. |
onChange |
(formData: FormValues) => void |
undefined |
Callback function called when the form data changes (debounced if debounceOnChange is set). |
onSubmit |
SubmitHandler<FieldValues> |
undefined |
Callback function called when the form is submitted. |
formOptions |
UseFormProps |
{} |
Options for react-hook-form 's useForm hook. |
header |
React.ReactNode |
undefined |
Header element for the form. |
footer |
React.ReactNode |
undefined |
Footer element for the form. |
readOnly |
boolean |
false |
Whether the form is read-only. |
disableForm |
boolean |
false |
Whether the form is disabled. |
showSubmitButton |
boolean |
true |
Whether to show the submit button. |
autoSave |
{ interval: number; save: (data: Record<string, any>) => void } |
undefined |
Auto-save configuration. |
resetOnSubmit |
boolean |
false |
Whether to reset the form on submit. |
focusFirstError |
boolean |
false |
Whether to focus on the first error field on submit. |
layout |
'flex' | 'grid' |
'grid' |
The layout type for the form. |
layoutConfig |
any |
{} |
Layout configuration. For grid , it can be { minWidth: '300px' } . For flex , it can be { gap: '10px' } . |
renderLayout |
RenderLayoutProps |
undefined |
Custom layout renderer. |
horizontalLabel |
boolean |
false |
Whether to use horizontal labels. |
labelWidth |
string | number |
undefined |
Label width (for horizontal labels). |
enableLocalStorage |
boolean |
false |
Whether to enable local storage for the form data. |
debounceOnChange |
number |
0 |
Debounce time (in ms) for the onChange callback. |
disableAutocomplete |
boolean |
false |
Whether to disable autocomplete for the form. |
showInlineError |
boolean |
true |
Whether to show inline error messages. |
showErrorSummary |
boolean |
false |
Whether to show an error summary. |
validateOnBlur |
boolean |
false |
Whether to validate on blur. |
validateOnChange |
boolean |
true |
Whether to validate on change. |
validateOnSubmit |
boolean |
true |
Whether to validate on submit. |
className |
string |
undefined |
CSS class name for the form container. |
formClassNameConfig |
FormClassNameConfig |
{} |
CSS class names for form elements. |
style |
React.CSSProperties |
undefined |
Inline styles for the form container. |
theme |
any |
undefined |
Theme object. You can provide custom theme. Please refer to ThemeProvider component for more information. |
onFormReady |
(form: UseFormReturn<any>) => void |
undefined |
Callback function called when the form is ready. |
renderSubmitButton |
(handleSubmit: (e?: React.BaseSyntheticEvent) => Promise<void>, isSubmitting: boolean) => React.ReactNode |
undefined |
Custom submit button renderer. |
renderFormContent |
RenderFormContentProps |
undefined |
Custom form content renderer. |
renderFormFooter |
RenderFormFooterProps |
undefined |
Custom form footer renderer. |
customValidators |
{ [key: string]: (value: any, context: any) => string | undefined } |
undefined |
Custom validators. |
customInputs |
{ [key: string]: React.ComponentType<CustomInputProps> } |
undefined |
Custom input components. |
onError |
(errors: FieldErrors) => void |
undefined |
Error handler function. |
renderErrorSummary |
(errors: FieldErrors, formClassNameConfig: FormClassNameConfig | undefined) => React.ReactNode |
undefined |
Custom error summary renderer. |
validationMessages |
ValidationMessages |
undefined |
Custom validation messages. Use to override default validation messages. More detail at Validation section |
The FormConfig
object defines the structure and behavior of the form. Each key in the object represents a field in the form, and the value is a FieldConfig
object that defines the field's properties.
Prop | Type | Default | Description |
---|---|---|---|
type |
InputType |
'text' |
The input type of the field. |
label |
string |
undefined |
The label text for the field. |
placeholder |
string |
undefined |
The placeholder text for the field. |
validation |
ValidationConfig |
undefined |
The validation configuration for the field. |
component |
React.ComponentType<any> |
undefined |
A custom component to use for rendering the field. |
style |
React.CSSProperties |
undefined |
Inline styles for the input element. |
readOnly |
boolean |
false |
Whether the field is read-only. |
clearable |
boolean |
false |
Whether the field can be cleared. |
showCounter |
boolean |
false |
Whether to show a character counter for the field (for text, textarea). |
copyToClipboard |
boolean |
false |
Whether to enable copy-to-clipboard functionality for the field (for text, textarea). |
tooltip |
string |
undefined |
Tooltip text for the field. |
classNameConfig |
FieldClassNameConfig |
undefined |
CSS class names for the field's elements. |
options |
{ value: string; label: string }[] |
undefined |
Options for select, radio, or combobox inputs. |
conditional |
Condition |
undefined |
Conditional logic for the field. |
fields |
FormConfig |
undefined |
Nested fields (for complex inputs). |
validationMessages |
{ [key: string]: string | ((values: { label?: string; value: any; error: any; config: FieldConfig; }) => string) } |
undefined |
Custom validation messages for the field. Use to override default or global validation messages. Support function to provide dynamic message based on values. |
defaultValue |
any |
undefined |
The default value for the field. |
Supported input types:
text
number
checkbox
select
textarea
email
password
tel
url
radio
date
switch
time
datetime-local
combobox
custom
Prop | Type | Default | Description |
---|---|---|---|
required |
boolean | { value: boolean; message: string } |
false |
Whether the field is required. |
minLength |
number | { value: number; message: string } |
undefined |
The minimum length of the field's value. |
maxLength |
number | { value: number; message: string } |
undefined |
The maximum length of the field's value. |
min |
number | string | { value: number | string; message: string } |
undefined |
The minimum value of the field (for number, date). |
max |
number | string | { value: number | string; message: string } |
undefined |
The maximum value of the field (for number, date). |
pattern |
RegExp | { value: RegExp; message: string } |
undefined |
A regular expression that the field's value must match. |
validate |
(value: any, formValues: FormValues) => string | undefined | Promise<string | undefined> |
undefined |
A custom validation function. Returns an error message if validation fails, undefined otherwise. |
requiredMessage |
string |
undefined |
Custom message for required validation. This prop is deprecated, use validationMessages instead |
Prop | Type | Default | Description |
---|---|---|---|
when |
string |
undefined |
The field to watch for changes. |
operator |
ComparisonOperator |
'is' |
The comparison operator to use. |
value |
any |
undefined |
The value to compare against. |
comparator |
(value: any) => boolean |
undefined |
A custom comparator function (only used when operator is 'custom' ). |
fields |
string[] |
[] |
The fields to show or hide based on the condition. |
Supported comparison operators:
is
isNot
greaterThan
lessThan
greaterThanOrEqual
lessThanOrEqual
contains
startsWith
endsWith
custom
Prop | Type | Default | Description |
---|---|---|---|
formContainer |
string |
undefined |
CSS class name for the form container. |
inputWrapper |
string |
undefined |
CSS class name for the input wrapper. |
label |
string |
undefined |
CSS class name for the label. |
input |
string |
undefined |
CSS class name for the input element. |
errorMessage |
string |
undefined |
CSS class name for the error message. |
button |
string |
undefined |
CSS class name for the button. |
select |
string |
undefined |
CSS class name for the select element. |
textarea |
string |
undefined |
CSS class name for the textarea element. |
checkbox |
string |
undefined |
CSS class name for the checkbox element. |
radio |
string |
undefined |
CSS class name for the radio element. |
date |
string |
undefined |
CSS class name for the date input element. |
number |
string |
undefined |
CSS class name for the number input element. |
switch |
string |
undefined |
CSS class name for the switch element. |
time |
string |
undefined |
CSS class name for the time input element. |
dateTime |
string |
undefined |
CSS class name for the datetime input element. |
comboBox |
string |
undefined |
CSS class name for the combobox input element. |
radioGroup |
string |
undefined |
CSS class name for the radio group. |
radioButton |
string |
undefined |
CSS class name for the radio button. |
radioLabel |
string |
undefined |
CSS class name for the radio label. |
checkboxInput |
string |
undefined |
CSS class name for the checkbox input. |
switchContainer |
string |
undefined |
CSS class name for the switch container. |
switchSlider |
string |
undefined |
CSS class name for the switch slider. |
numberInputContainer |
string |
undefined |
CSS class name for the number input container. |
numberInputButton |
string |
undefined |
CSS class name for the number input button. |
comboBoxContainer |
string |
undefined |
CSS class name for the combobox container. |
comboBoxDropdownList |
string |
undefined |
CSS class name for the combobox dropdown list. |
comboBoxDropdownItem |
string |
undefined |
CSS class name for the combobox dropdown item. |
Same as FormClassNameConfig
, but applies to individual fields. FieldClassNameConfig
will override FormClassNameConfig
for that specific field.
You can define validation rules for each field in the validation
property of the FieldConfig
object. The validation
property can contain the following validation rules:
-
required
: Whether the field is required. -
minLength
: The minimum length of the field's value. -
maxLength
: The maximum length of the field's value. -
min
: The minimum value of the field (for number, date). -
max
: The maximum value of the field (for number, date). -
pattern
: A regular expression that the field's value must match. -
validate
: A custom validation function.
You can provide custom validation messages for each field by using the validationMessages
property in the FieldConfig
object.
You can also provide global custom validation messages for the whole form by using the validationMessages
prop in the DynamicForm
component.
FieldConfig.validationMessages
will override DynamicForm.validationMessages
for that specific field.
// Example usage
const formConfig: DynamicFormProps['config'] = {
email: {
type: 'email',
label: 'Email',
validation: {
required: { value: true, message: 'This field is required' },
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i,
message: 'Invalid email address',
},
},
validationMessages: {
required: 'You must enter an email address.', // Override required message for this field
pattern: ({ value }) => `${value} is not a valid email address.`, // Dynamic message based on input value
},
},
};
// Global validation messages
const validationMessages: ValidationMessages = {
required: 'This is a globally defined required message',
};
interface ValidationMessages {
[key: string]:
| string // Static message
| ((values: {
label?: string;
value: any;
error: any;
config: FieldConfig;
}) => string); // Dynamic message function
}
The validate
function receives two arguments:
-
value
: The current value of the field. -
formValues
: The current values of all fields in the form.
The function should return a string if validation fails (the error message), or undefined
if validation passes. You can also return a Promise
that resolves to a string or undefined
for asynchronous validation.
// Example custom validation function that checks if an email is already taken
const validateEmailNotTaken = async (value: string) => {
const isTaken = await checkIfEmailExists(value); // Assume this function makes an API call
if (isTaken) {
return 'Email already taken';
}
return undefined;
};
// Example usage in a field configuration
const formConfig: DynamicFormProps['config'] = {
email: {
type: 'email',
label: 'Email',
validation: {
validate: validateEmailNotTaken,
},
},
};
You can create your own custom input components and use them in the form. To do this, pass a customInputs
prop to the DynamicForm
component. The customInputs
prop is an object where the keys are the input types and the values are the custom components.
import React from 'react';
import {
DynamicForm,
DynamicFormProps,
CustomInputProps,
} from '@matthew.ngo/react-dynamic-form';
// Example custom input component
const MyCustomInput: React.FC<CustomInputProps> = ({
fieldConfig,
formClassNameConfig,
field,
onChange,
value,
}) => {
return (
<div>
<label htmlFor={fieldConfig.id}>{fieldConfig.label}</label>
<input
id={fieldConfig.id}
type="text"
value={value || ''}
onChange={(e) => onChange(e.target.value)}
className={formClassNameConfig?.input}
/>
{/* You can add your own error handling here */}
</div>
);
};
// Example usage of custom input
const myFormConfig: DynamicFormProps['config'] = {
customField: {
type: 'custom',
label: 'My Custom Field',
component: MyCustomInput, // Specify the custom component here
},
};
const App: React.FC = () => {
const handleSubmit = (data: any) => {
console.log(data);
};
return (
<DynamicForm
config={myFormConfig}
onSubmit={handleSubmit}
customInputs={{
custom: MyCustomInput, // Register the custom input type
}}
/>
);
};
export default App;
You can customize the look and feel of the form by providing a custom theme object to the DynamicForm
component via the theme
prop. The theme
object should follow the styled-components
DefaultTheme
interface.
import React from 'react';
import {
DynamicForm,
DynamicFormProps,
defaultTheme,
} from '@matthew.ngo/react-dynamic-form';
import { ThemeProvider } from 'styled-components';
const myTheme = {
...defaultTheme,
colors: {
...defaultTheme.colors,
primary: 'blue',
},
};
const myFormConfig: DynamicFormProps['config'] = {
firstName: {
type: 'text',
label: 'First Name',
},
};
const App: React.FC = () => {
return (
<ThemeProvider theme={myTheme}>
<DynamicForm config={myFormConfig} />
</ThemeProvider>
);
};
export default App;
Contributions are welcome! Please read the contributing guidelines (TODO: create this file later) for details on how to contribute.
This project is licensed under the MIT License - see the LICENSE file for details.