0.0.37 • Published 10 months ago

@matthew.ngo/react-dynamic-form v0.0.37

Weekly downloads
-
License
MIT
Repository
github
Last release
10 months ago

@matthew.ngo/react-dynamic-form

Version Downloads/week License codecov

@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.

Demo

View the live demo here: https://maemreyo.github.io/react-dynamic-form/

Features

  • Dynamic Form Generation: Create forms from a simple JSON configuration.
  • react-hook-form Integration: Utilizes react-hook-form for efficient form state management.
  • yup Validation: Supports schema-based validation using yup.
  • 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 and grid layouts.
  • Auto Save: Option to automatically save form data at intervals.
  • Local Storage: Persist form data in local storage.
  • Debounced onChange: Provides a debounced onChange 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

Installation

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

Usage

Basic Example

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;

Advanced Example

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;

Props

The DynamicForm component accepts the following props:

PropTypeDefaultDescription
configFormConfig{}The configuration object for the form.
onChange(formData: FormValues) => voidundefinedCallback function called when the form data changes (debounced if debounceOnChange is set).
onSubmitSubmitHandler<FieldValues>undefinedCallback function called when the form is submitted.
formOptionsUseFormProps{}Options for react-hook-form's useForm hook.
headerReact.ReactNodeundefinedHeader element for the form.
footerReact.ReactNodeundefinedFooter element for the form.
readOnlybooleanfalseWhether the form is read-only.
disableFormbooleanfalseWhether the form is disabled.
showSubmitButtonbooleantrueWhether to show the submit button.
autoSave{ interval: number; save: (data: Record<string, any>) => void }undefinedAuto-save configuration.
resetOnSubmitbooleanfalseWhether to reset the form on submit.
focusFirstErrorbooleanfalseWhether to focus on the first error field on submit.
layout'flex' \| 'grid''grid'The layout type for the form.
layoutConfigany{}Layout configuration. For grid, it can be { minWidth: '300px' }. For flex, it can be { gap: '10px' }.
renderLayoutRenderLayoutPropsundefinedCustom layout renderer.
horizontalLabelbooleanfalseWhether to use horizontal labels.
labelWidthstring \| numberundefinedLabel width (for horizontal labels).
enableLocalStoragebooleanfalseWhether to enable local storage for the form data.
debounceOnChangenumber0Debounce time (in ms) for the onChange callback.
disableAutocompletebooleanfalseWhether to disable autocomplete for the form.
showInlineErrorbooleantrueWhether to show inline error messages.
showErrorSummarybooleanfalseWhether to show an error summary.
validateOnBlurbooleanfalseWhether to validate on blur.
validateOnChangebooleantrueWhether to validate on change.
validateOnSubmitbooleantrueWhether to validate on submit.
classNamestringundefinedCSS class name for the form container.
formClassNameConfigFormClassNameConfig{}CSS class names for form elements.
styleReact.CSSPropertiesundefinedInline styles for the form container.
themeanyundefinedTheme object. You can provide custom theme. Please refer to ThemeProvider component for more information.
onFormReady(form: UseFormReturn<any>) => voidundefinedCallback function called when the form is ready.
renderSubmitButton(handleSubmit: (e?: React.BaseSyntheticEvent) => Promise<void>, isSubmitting: boolean) => React.ReactNodeundefinedCustom submit button renderer.
renderFormContentRenderFormContentPropsundefinedCustom form content renderer.
renderFormFooterRenderFormFooterPropsundefinedCustom form footer renderer.
customValidators{ [key: string]: (value: any, context: any) => string \| undefined }undefinedCustom validators.
customInputs{ [key: string]: React.ComponentType<CustomInputProps> }undefinedCustom input components.
onError(errors: FieldErrors) => voidundefinedError handler function.
renderErrorSummary(errors: FieldErrors, formClassNameConfig: FormClassNameConfig \| undefined) => React.ReactNodeundefinedCustom error summary renderer.
validationMessagesValidationMessagesundefinedCustom validation messages. Use to override default validation messages. More detail at Validation section

FormConfig

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.

FieldConfig

PropTypeDefaultDescription
typeInputType'text'The input type of the field.
labelstringundefinedThe label text for the field.
placeholderstringundefinedThe placeholder text for the field.
validationValidationConfigundefinedThe validation configuration for the field.
componentReact.ComponentType<any>undefinedA custom component to use for rendering the field.
styleReact.CSSPropertiesundefinedInline styles for the input element.
readOnlybooleanfalseWhether the field is read-only.
clearablebooleanfalseWhether the field can be cleared.
showCounterbooleanfalseWhether to show a character counter for the field (for text, textarea).
copyToClipboardbooleanfalseWhether to enable copy-to-clipboard functionality for the field (for text, textarea).
tooltipstringundefinedTooltip text for the field.
classNameConfigFieldClassNameConfigundefinedCSS class names for the field's elements.
options{ value: string; label: string }[]undefinedOptions for select, radio, or combobox inputs.
conditionalConditionundefinedConditional logic for the field.
fieldsFormConfigundefinedNested fields (for complex inputs).
validationMessages{ [key: string]: string \| ((values: { label?: string; value: any; error: any; config: FieldConfig; }) => string) }undefinedCustom validation messages for the field. Use to override default or global validation messages. Support function to provide dynamic message based on values.
defaultValueanyundefinedThe default value for the field.

InputType

Supported input types:

  • text
  • number
  • checkbox
  • select
  • textarea
  • email
  • password
  • tel
  • url
  • radio
  • date
  • switch
  • time
  • datetime-local
  • combobox
  • custom

ValidationConfig

PropTypeDefaultDescription
requiredboolean \| { value: boolean; message: string }falseWhether the field is required.
minLengthnumber \| { value: number; message: string }undefinedThe minimum length of the field's value.
maxLengthnumber \| { value: number; message: string }undefinedThe maximum length of the field's value.
minnumber \| string \| { value: number \| string; message: string }undefinedThe minimum value of the field (for number, date).
maxnumber \| string \| { value: number \| string; message: string }undefinedThe maximum value of the field (for number, date).
patternRegExp \| { value: RegExp; message: string }undefinedA regular expression that the field's value must match.
validate(value: any, formValues: FormValues) => string \| undefined \| Promise<string \| undefined>undefinedA custom validation function. Returns an error message if validation fails, undefined otherwise.
requiredMessagestringundefinedCustom message for required validation. This prop is deprecated, use validationMessages instead

Condition

PropTypeDefaultDescription
whenstringundefinedThe field to watch for changes.
operatorComparisonOperator'is'The comparison operator to use.
valueanyundefinedThe value to compare against.
comparator(value: any) => booleanundefinedA custom comparator function (only used when operator is 'custom').
fieldsstring[][]The fields to show or hide based on the condition.

ComparisonOperator

Supported comparison operators:

  • is
  • isNot
  • greaterThan
  • lessThan
  • greaterThanOrEqual
  • lessThanOrEqual
  • contains
  • startsWith
  • endsWith
  • custom

FormClassNameConfig

PropTypeDefaultDescription
formContainerstringundefinedCSS class name for the form container.
inputWrapperstringundefinedCSS class name for the input wrapper.
labelstringundefinedCSS class name for the label.
inputstringundefinedCSS class name for the input element.
errorMessagestringundefinedCSS class name for the error message.
buttonstringundefinedCSS class name for the button.
selectstringundefinedCSS class name for the select element.
textareastringundefinedCSS class name for the textarea element.
checkboxstringundefinedCSS class name for the checkbox element.
radiostringundefinedCSS class name for the radio element.
datestringundefinedCSS class name for the date input element.
numberstringundefinedCSS class name for the number input element.
switchstringundefinedCSS class name for the switch element.
timestringundefinedCSS class name for the time input element.
dateTimestringundefinedCSS class name for the datetime input element.
comboBoxstringundefinedCSS class name for the combobox input element.
radioGroupstringundefinedCSS class name for the radio group.
radioButtonstringundefinedCSS class name for the radio button.
radioLabelstringundefinedCSS class name for the radio label.
checkboxInputstringundefinedCSS class name for the checkbox input.
switchContainerstringundefinedCSS class name for the switch container.
switchSliderstringundefinedCSS class name for the switch slider.
numberInputContainerstringundefinedCSS class name for the number input container.
numberInputButtonstringundefinedCSS class name for the number input button.
comboBoxContainerstringundefinedCSS class name for the combobox container.
comboBoxDropdownListstringundefinedCSS class name for the combobox dropdown list.
comboBoxDropdownItemstringundefinedCSS class name for the combobox dropdown item.

FieldClassNameConfig

Same as FormClassNameConfig, but applies to individual fields. FieldClassNameConfig will override FormClassNameConfig for that specific field.

Validation

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.

Custom Validation Messages

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',
};
Validation Messages Interface
interface ValidationMessages {
  [key: string]:
    | string // Static message
    | ((values: {
        label?: string;
        value: any;
        error: any;
        config: FieldConfig;
      }) => string); // Dynamic message function
}

Custom Validation 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,
    },
  },
};

Custom Inputs

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;

Theming

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;

Contributing

Contributions are welcome! Please read the contributing guidelines (TODO: create this file later) for details on how to contribute.

License

This project is licensed under the MIT License - see the LICENSE file for details.

0.0.37

10 months ago

0.0.36

10 months ago

0.0.35

10 months ago

0.0.34

10 months ago

0.0.33

10 months ago

0.0.32

10 months ago

0.0.31

10 months ago

0.0.30

10 months ago

0.0.29

10 months ago

0.0.28

10 months ago

0.0.27

10 months ago

0.0.26

11 months ago

0.0.25

11 months ago

0.0.24

11 months ago

0.0.23

11 months ago

0.0.22

11 months ago

0.0.21

11 months ago

0.0.20

11 months ago

0.0.19

11 months ago

0.0.18

11 months ago

0.0.17

11 months ago

0.0.16

11 months ago

0.0.15

11 months ago

0.0.14

11 months ago

0.0.12

11 months ago

0.0.11

11 months ago

0.0.10

11 months ago

0.0.9

11 months ago

0.0.8

11 months ago

0.0.7

11 months ago

0.0.6

11 months ago

0.0.5

11 months ago

0.0.4

11 months ago

0.0.3

11 months ago

0.0.2

11 months ago

0.0.1

11 months ago

0.0.0

11 months ago