1.2.1 • Published 2 years ago

@jeremyling/react-material-ui-form-builder v1.2.1

Weekly downloads
-
License
MIT
Repository
github
Last release
2 years ago

React Material UI Form Builder

An easy-to-use and quick form builder with validation using the following React Material UI input components:

  • TextField
  • Select
  • DatePicker
  • DateTimePicker
  • TimePicker
  • Autocomplete
  • Chip
  • Checkbox
  • Radio
  • Switch
  • ImageList
  • Rating

Validation is done using yup.

This project aims to make building standard forms a breeze while leveraging Material UI components to create a more familiar UI. See below for usage examples.

Installation

npm install --save @jeremyling/react-material-ui-form-builder

The following packages are peer dependencies and must be installed for this package to be fully functional. To reduce redundant packages, you need not install the peer dependencies of the components listed if not used.

// Required
@emotion/react
@emotion/styled
@mui/material
lodash
react-hook-forms

// Required if using yup resolver for validation
yup

// Date/Time Pickers
@date-io/date-fns
date-fns
@mui/icons-material
@mui/lab

// Rating
@mui/icons-material

// Autocomplete
react-beautiful-dnd

// Rich Text
slate
slate-react
react-mui-color
@jeremyling/react-material-ui-rich-text-editor
@jeremyling/react-material-ui-rich-text-editor/dist/editor.css

Usage Example

The example below is replicated in this CodeSandbox.

Suppose you need to submit a form with the following structure:

// Employee
{
  name: "First Last",
  email: "firstlast@email.com",
  jobId: 1,
  status: "Active",
  skills: ["People Management"],
  subordinates: [2, 3],
  details: {
    joinDate: "2021-01-01",
  },
  profilePicFile: "",
}

Subordinates are other employees with the same data structure as above. Other data you have include:

const employees = [
  {
    id: 1,
    name: "First Employee"
    ...
  },
  {
    id: 2,
    name: "Second Employee"
    ...
  },
  {
    id: 3,
    name: "Third Employee"
    ...
  },
];

const jobs = [
  {
    id: 1,
    title: "Manager",
  },
  {
    id: 2,
    title: "Entry Level Staff",
  },
];

const skills = [
  'Data Entry',
  'People Management',
];

const statuses = ["Inactive", "Active"];

With the predefined data above, the following functional component illustrates how FormBuilder is used.

import React from "react";
import { yupResolver } from "@hookform/resolvers/yup";
import {
  FieldProp,
  FormBuilder,
  validationUtils,
} from "@jeremyling/react-material-ui-form-builder";
import { useForm, UseFormReturn } from "react-hook-form";
import { parseISO, isAfter } from "date-fns";
import { Button } from "@mui/material";

const employees = [
  {
    id: 1,
    name: "First Employee",
  },
  {
    id: 2,
    name: "Second Employee",
  },
  {
    id: 3,
    name: "Third Employee",
  },
];

const jobs = [
  {
    id: 1,
    title: "Manager",
  },
  {
    id: 2,
    title: "Entry Level Staff",
  },
];

const skills = ["Data Entry", "People Management"];

const statuses: [string, string] = ["Inactive", "Active"];

interface Props {
  defaultValue: {
    status: "Active";
  };
}

interface Employee {
  name: string;
  email: string;
  jobId: number;
  status: "Active" | "Inactive";
  skills: string[];
  subordinates: number[];
  details: {
    joinDate: string;
  };
  profilePicFile: string;
}

export default function EmployeeForm(props: Props) {
  const { defaultValue } = props;

  function fields(
    methods?: UseFormReturn<Employee>,
    watch?: [number]
  ): Array<FieldProp> {
    return [
      {
        component: "display-text",
        titleProps: {
          variant: "h6",
        },
        title: "Create Employee",
      },
      {
        component: "display-image",
        src: "https://via.placeholder.com/800x450?text=Create+Employee",
        alt: "Create Employee",
        props: {
          style: {
            height: 225,
            width: 400,
            objectFit: "cover",
          },
        },
      },
      {
        component: "text-field",
        attribute: "name",
        label: "Name",
        col: {
          // Here you can specify how many Grid columns the field should take for the corresponding breakpoints
          sm: 6,
        },
        validationType: "string",
        validations: [
          ["required", true],
          ["max", 50],
        ],
      },
      {
        component: "text-field",
        attribute: "email",
        label: "Email",
        col: {
          sm: 6,
        },
        props: {
          // Here you can pass any props that are accepted by Material UI's TextField component
        },
        validationType: "string",
        validations: [
          ["required", true],
          ["email", true],
        ],
      },
      {
        component: "select",
        attribute: "jobId",
        label: "Job",
        col: {
          sm: 6,
        },
        options: jobs,
        // If options is an array of objects, optionConfig is required
        optionConfig: {
          key: "id", // The attribute to use for the key required for each option
          value: "id", // The attribute to use to determine the value that should be passed to the form field
          label: "title", // The attribute to use to determine the label for the select option
        },
      },
      {
        component: "date-picker",
        attribute: "details.joinDate",
        label: "Join Date",
        col: {
          sm: 6,
        },
        props: {
          // Here you can pass any props that are accepted by Material UI's DatePicker component
          clearable: true,
          inputFormat: "dd MMM yyyy",
          disableFuture: true,
          openTo: "year",
          views: ["year", "month", "day"],
        },
        validationType: "string",
        validations: [
          [
            "test",
            [
              "notAfterToday",
              "Date of Birth must be in the past",
              (value: string) => {
                const parsed = parseISO(value);
                return !isAfter(parsed, new Date());
              },
            ],
          ],
        ],
      },
      {
        component: "switch",
        attribute: "status",
        label: "Status",
        col: {
          sm: 6,
        },
        options: statuses, // options in the form [offValue, onValue]
        props: {
          // Here you can pass any props that are accepted by Material UI's Switch component
        },
        idPrefix: "switch",
      },
      {
        component: "checkbox-group",
        attribute: "status",
        title: "Status", // You can omit this if you do not want a title for the field
        col: {
          sm: 6,
        },
        options: ["Active"], // Single option for a single checkbox
        props: {
          // Here you can pass any props that are accepted by Material UI's Checkbox component
          onChange: (event) => {
            if (event.target.checked) {
              methods?.setValue("status", "Active");
            } else {
              methods?.setValue("status", "Inactive");
            }
          },
        },
        labelProps: {
          // Here you can pass any props that are accepted by Material UI's FormControlLabel component
        },
        groupContainerProps: {
          // Here you can pass any props that are accepted by Material UI's FormControl component
        },
        idPrefix: "checkbox",
      },
      {
        attribute: "status",
        title: "Status", // You can omit this if you do not want a title for the field
        col: {
          sm: 6,
        },
        component: "radio-group",
        options: statuses,
        props: {
          // Here you can pass any props that are accepted by Material UI's Radio component
          color: "secondary",
        },
        groupContainerProps: {
          // Here you can pass any props that are accepted by Material UI's FormControl component
          sx: {
            flexDirection: "row",
          },
        },
        labelProps: {
          // Here you can pass any props that are accepted by Material UI's FormControlLabel component
          sx: {
            padding: "8px 16px",
          },
        },
        idPrefix: "radio",
      },
      {
        attribute: "skills",
        title: "Skills",
        col: {
          sm: 12,
        },
        component: "chip-group",
        options: skills, // optionConfig not required as options is an array of strings
        multiple: true, // Allow multiple selections
        props: {
          // Here you can pass any props that are accepted by Material UI's Chip component
        },
        groupContainerProps: {
          // Here you can pass any props that are accepted by Material UI's FormControl component
          sx: {
            flexDirection: "row",
          },
        },
        labelProps: {
          // Here you can pass any props that are accepted by the span component
          style: {
            padding: "8px 16px",
          },
        },
      },
      {
        attribute: "subordinates",
        label: "Subordinates",
        component: "autocomplete",
        options: employees,
        optionConfig: {
          value: "id",
          label: "name",
        },
        props: {
          // Here you can pass any props that are accepted by Material UI's Autocomplete component
          autoHighlight: true,
          multiple: true,
        },
        hideCondition:
          // This will hide the form field if the condition is true
          jobs.find((j) => j.id === Number(watch?.[0]))?.title ===
          "Entry Level Staff",
      },
      {
        attribute: "profilePicFile",
        label: "Select Image",
        component: "file-upload",
        acceptTypes: "image/*",
        maxSizeMb: 1,
        props: {
          // Here you can pass any props that are accepted by the input component
        },
      },
    ];
  }

  const schema = validationUtils.getFormSchema(fields());
  const methods =
    useForm <
    Employee >
    {
      mode: "onTouched",
      resolver: yupResolver(schema),
    };

  const watch = methods.watch(["jobId"]);

  const onSubmit = (data: Employee) => {
    console.log(data);
    // handle form submission
  };

  const submitButton = (
    <Button
      fullWidth
      variant="contained"
      color="primary"
      sx={{ mt: 1 }}
      onClick={methods.handleSubmit(onSubmit)}
    >
      Submit
    </Button>
  );

  return (
    <FormBuilder
      fields={fields(methods, watch)}
      defaultValue={defaultValue}
      methods={methods}
    >
      {submitButton}
    </FormBuilder>
  );
}

Props

PropTypeDefaultDescription
fieldsarrayrequiredArray of form fields along with props (details below)
defaultValueobjectrequiredInitial value of form
childrennodeundefinedAdditional content to the right of the form
indexstring or numberundefinedTo uniquely identify fields if FormBuilder is used in a loop
idPrefixstringundefinedTo uniquely identify fields if multiple fields use the same attribute
errorsarrayundefinedExternal errors outside of FormBuilder that you would like to pass to be rendered
methodsobjectUseFormReturn<any>React Hook Form's UseFormReturn type for updating and syncing form
sxstringundefinedMaterial UI's sx prop to pass to Grid component wrapping form fields

Field Props

Common

PropTypeDefaultDescription
attributestringundefinedForm attribute that controls input and is modified by input
labelstringundefinedComponent label for text-field, select, autocomplete, date-picker, date-time-picker, time-picker, switch. Can be omitted if label is not required.
titlestringundefinedTitle for component. Can be used to describe input or hold a question.
titlePropsobjectundefinedTitle props passed to Typography component wrapping title
titleSuffixstringundefinedTitle suffix to append to title. Could be used to denote required fields
titleSuffixPropsobjectundefinedTitle suffix props passed to span component wrapping title suffix
titleContainerPropsobjectundefinedProps passed to container wrapping the title and titleSuffix
colobject{ xs: 12 }Grid columns that component should take
componentstringtext-fieldOne of: text-field,select,date-picker,date-time-picker,time-picker,autocomplete,chip-group,checkbox-group,radio-group,switch,file-upload,image-picker,rating,counter,display-text,display-image,rich-text,custom
propsobjectundefinedAny additional props to pass to the Material UI component
containerPropsobjectundefinedAny additional props to pass to the Material UI Grid item that contains the component
hideConditionboolundefinedIf true, hides field
validationType^stringundefinedOne of: mixed, string, number, date, boolean, array.
validations^objectundefinedThese are validation options accepted by yup in the form of [validation, arguments]. Arguments can be a string, number, true, regex or an array of such in the order that it is accepted by the yup option. For validations that do not require any arguments, set the argument to true.
customComponentfuncundefinedFunction that accepts the props (field, methods, hookField) and returns a node

^See below for examples

Components With Options

This includes select, autocomplete, chip-group, checkbox-group and radio-group.

PropTypeDefaultDescription
optionsarray[]Required
optionConfigobjectselect, chip-group, checkbox-group, radio-group: { key: optionKey, value: optionKey, label: optionKey }autocomplete: { value: optionKey, label: optionKey }Required if options is an array of objects. Leave value undefined for entire object.
randomizeOptionsboolundefinedIf true, randomises option order on each render
multipleboolundefinedOnly for chip-group, checkbox-group. If true, multiple options will be selectible
labelPropsobjectundefinedOnly for checkbox-group, radio-group. Any additional props to pass to Material UI's FormControlLabel that wraps the label.
groupContainerPropsobjectundefinedOnly for chip-group, checkbox-group, radio-group. Any additional props to pass to Material UI's FormGroup that wraps the individual components within the group.
sortableboolundefinedOnly for autocomplete. If true, selected options will be sortable via drag and drop

Switch

This includes switch.

PropTypeDefaultDescription
optionsarray[false, true]Options for switch in the form of [offValue, onValue]. Values must be
* string, number or boolean.
labelPropsobjectundefinedAny additional props to pass to Material UI's FormControlLabel that wraps the label.

Date/Time Pickers

This includes date-picker, date-time-picker and time-picker.

PropTypeDefaultDescription
keyboardboolundefinedIf true, will use the Keyboard equivalent components of the pickers

File Upload

This includes file-upload.

PropTypeDefaultDescription
acceptTypesstring or array[".pdf", ".doc", ".docx", ".xml", "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", ".xls", ".xlsx", ".csv", "image/*", "audio/*", "video/*"]Concatenated value will be passed as accept prop to input
maxSizeMbnumber2Max size of each uploaded file.
fileTypestringundefinedOne of: file, image, audio, video.
imageUrlsarrayundefinedIf file type is image, you may specify the urls of the existing images here.
imageSizearrayundefinedSize of image preview in the form [width, height]. imageSize supercedes aspectRatio.
aspectRatioarrayundefinedAspect ratio of image preview in the form [width, height]. imageSize supercedes aspectRatio.

Display Image

PropTypeDefaultDescription
srcstringundefinedSource of image.
altstringundefinedAlt passed to img node.

Image Picker

This includes image-picker.

PropTypeDefaultDescription
imagesarrayundefinedThis should contain an array of objects with attributes src, label, subLabel, and alt (defaults to label)
imageColsnumber{ xs: 2 }Number of columns in image list. This should be an object with breakpoints xs, sm, md, lg, xl as keys. Columns for each breakpoint default to the previous breakpoint is not specified
labelLinesnumber2Number of lines allowed for label
subLabelLinesnumber2Number of lines allowed for sublabel
aspectRatioarray[1, 1]Aspect ratio of image preview in the form [width, height].
multipleboolundefinedIf true, multiple options will be selectible
imagePropsobjectundefinedAny additional props to pass to the Box component that wraps the img.
labelPropsobjectundefinedAny additional props to pass to the Typography component that wraps the label.
subLabelPropsobjectundefinedAny additional props to pass to the Typography component that wraps the sublabel.
groupContainerPropsobjectundefinedAny additional props to pass to the ImageList component that wraps the individual components within the group.

Rating

This includes rating.

PropTypeDefaultDescription
iconColorstringundefinedIcon colour

Counter

This includes counter.

PropTypeDefaultDescription
inputMinnumber0Minimum value allowed
inputMaxnumber1000000Maximum value allowed
fontSizenumberundefinedCounter font size

Rich Text

PropTypeDefaultDescription
containerPropsobjectundefinedProps to pass to the Material UI Paper wrapper
editablePropsobjectundefinedProps to pass to the Slate Editable component

Validation

Validation is done using yup, which has 6 core types that inherit from the mixed type: string, number, boolean, date, array and object. For this project, it should be sufficient to use only string and number for the various components. In fact, other than the text-field component, it is unlikely you would need any validation beyond required. Here are some examples of how you might use validation.

// Example field 1
{
  attribute: ...,
  component: 'text-field',
  label: ...,
  validationType: 'string',
  validations: [
    ["required", true],
    ["length", 10],
    ["min", 5],
    ["max", 20],
    ["matches", [/[a-z]/i, "Can only contain letters"]],
    ["email", true],
    ["url", true],
    ["uuid", true],
  ]
}

// Example field 2
{
  attribute: ...,
  component: 'text-field',
  props: {
    type: 'number',
  },
  label: ...,
  validationType: 'number',
  validations: [
    ["required", true],
    ["min", 5],
    ["max", 20],
    ["lessThan", 20],
    ["moreThan", 5],
    ["positive", true],
    ["negative", true],
    ["integer", true],
  ]
}
1.2.0

2 years ago

1.2.1

2 years ago

1.1.16

2 years ago

1.1.15

2 years ago

1.1.14

2 years ago

1.1.13

2 years ago

1.1.17

2 years ago

1.1.9

2 years ago

1.1.8

2 years ago

1.1.7

2 years ago

1.1.6

2 years ago

1.1.5

2 years ago

1.1.4

2 years ago

1.1.12

2 years ago

1.1.11

2 years ago

1.1.10

2 years ago

1.1.1

2 years ago

1.1.0

2 years ago

1.1.3

2 years ago

1.1.2

2 years ago

1.0.19

2 years ago

1.0.2

2 years ago

1.0.1

2 years ago

1.0.17

2 years ago

1.0.0

2 years ago

1.0.16

2 years ago

1.0.9

2 years ago

1.0.8

2 years ago

1.0.7

2 years ago

1.0.6

2 years ago

1.0.5

2 years ago

1.0.4

2 years ago

1.0.3

2 years ago

1.0.22

2 years ago

1.0.21

2 years ago

1.0.20

2 years ago

1.0.26

2 years ago

1.0.25

2 years ago

1.0.24

2 years ago

1.0.23

2 years ago

1.0.29

2 years ago

1.0.28

2 years ago

1.0.27

2 years ago

1.0.33

2 years ago

1.0.32

2 years ago

1.0.31

2 years ago

1.0.30

2 years ago

1.0.11

2 years ago

1.0.10

2 years ago

1.0.15

2 years ago

1.0.14

2 years ago

1.0.13

2 years ago

1.0.12

2 years ago

0.9.8

2 years ago

0.9.7

2 years ago

0.9.9

2 years ago

0.9.30

2 years ago

0.9.4

2 years ago

0.9.31

2 years ago

0.9.32

2 years ago

0.9.6

2 years ago

0.9.5

2 years ago

0.9.23

2 years ago

0.9.24

2 years ago

0.9.25

2 years ago

0.9.26

2 years ago

0.9.20

2 years ago

0.9.21

2 years ago

0.9.22

2 years ago

0.9.27

2 years ago

0.9.28

2 years ago

0.9.29

2 years ago

0.9.12

2 years ago

0.9.13

2 years ago

0.9.14

2 years ago

0.9.15

2 years ago

0.9.10

2 years ago

0.9.11

2 years ago

0.9.16

2 years ago

0.9.17

2 years ago

0.9.18

2 years ago

0.9.19

2 years ago

0.8.9

3 years ago

0.8.8

3 years ago

0.8.5

3 years ago

0.8.4

3 years ago

0.8.7

3 years ago

0.8.6

3 years ago

0.9.3

3 years ago

0.8.3

3 years ago

0.9.0

3 years ago

0.9.2

3 years ago

0.9.1

3 years ago

0.7.22

3 years ago

0.7.21

3 years ago

0.7.24

3 years ago

0.7.23

3 years ago

0.7.20

3 years ago

0.7.2

3 years ago

0.7.4

3 years ago

0.7.3

3 years ago

0.7.11

3 years ago

0.7.10

3 years ago

0.7.13

3 years ago

0.7.12

3 years ago

0.7.19

3 years ago

0.7.18

3 years ago

0.7.15

3 years ago

0.7.14

3 years ago

0.7.17

3 years ago

0.7.16

3 years ago

0.8.1

3 years ago

0.8.0

3 years ago

0.8.2

3 years ago

0.7.9

3 years ago

0.7.6

3 years ago

0.7.5

3 years ago

0.7.8

3 years ago

0.7.7

3 years ago

0.7.0

3 years ago

0.6.7

3 years ago

0.6.6

3 years ago

0.6.8

3 years ago

0.6.5

3 years ago

0.6.4

3 years ago

0.5.41

3 years ago

0.5.42

3 years ago

0.5.40

3 years ago

0.6.3

3 years ago

0.6.2

3 years ago

0.6.1

3 years ago

0.6.0

3 years ago

0.5.39

3 years ago

0.5.32

3 years ago

0.5.33

3 years ago

0.5.30

3 years ago

0.5.31

3 years ago

0.5.38

3 years ago

0.5.36

3 years ago

0.5.37

3 years ago

0.5.34

3 years ago

0.5.35

3 years ago

0.5.29

3 years ago

0.5.27

3 years ago

0.5.28

3 years ago

0.5.25

3 years ago

0.5.26

3 years ago

0.5.24

3 years ago

0.5.18

3 years ago

0.5.19

3 years ago

0.5.17

3 years ago

0.5.21

3 years ago

0.5.22

3 years ago

0.5.20

3 years ago

0.5.23

3 years ago

0.5.16

3 years ago

0.5.14

3 years ago

0.5.15

3 years ago

0.5.13

3 years ago

0.5.11

3 years ago

0.5.12

3 years ago

0.5.10

3 years ago

0.5.9

3 years ago

0.5.8

3 years ago

0.5.4

3 years ago

0.5.6

3 years ago

0.5.5

3 years ago

0.5.7

3 years ago

0.5.0

3 years ago

0.5.2

3 years ago

0.5.1

3 years ago

0.4.17

3 years ago

0.4.18

3 years ago

0.4.15

3 years ago

0.4.16

3 years ago

0.4.14

3 years ago

0.4.9

3 years ago

0.4.8

3 years ago

0.4.10

3 years ago

0.4.13

3 years ago

0.4.11

3 years ago

0.4.5

3 years ago

0.4.4

3 years ago

0.4.7

3 years ago

0.4.6

3 years ago

0.4.3

3 years ago

0.4.1

3 years ago

0.4.2

3 years ago

0.3.12

3 years ago

0.3.11

3 years ago

0.3.10

3 years ago

0.3.6

3 years ago

0.3.5

3 years ago

0.3.8

3 years ago

0.3.7

3 years ago

0.3.9

3 years ago

0.3.2

3 years ago

0.3.1

3 years ago

0.3.4

3 years ago

0.3.3

3 years ago

0.2.30

3 years ago

0.2.34

3 years ago

0.2.33

3 years ago

0.2.32

3 years ago

0.2.31

3 years ago

0.2.29

3 years ago

0.2.27

3 years ago

0.2.26

3 years ago

0.2.25

3 years ago

0.2.28

3 years ago

0.2.24

3 years ago

0.2.23

3 years ago

0.2.22

3 years ago

0.2.21

3 years ago

0.2.20

3 years ago

0.2.19

3 years ago

0.2.18

3 years ago

0.2.17

3 years ago

0.2.16

3 years ago

0.2.15

3 years ago

0.2.14

3 years ago

0.2.13

3 years ago

0.2.12

3 years ago

0.2.11

3 years ago

0.2.10

3 years ago

0.2.9

3 years ago

0.2.8

3 years ago

0.2.7

3 years ago

0.2.1

3 years ago

0.2.0

3 years ago

0.1.8

3 years ago

0.2.6

3 years ago

0.1.7

3 years ago

0.1.9

3 years ago

0.2.3

3 years ago

0.2.2

3 years ago

0.2.5

3 years ago

0.1.6

3 years ago

0.2.4

3 years ago

0.1.5

3 years ago

0.1.4

3 years ago

0.1.3

3 years ago

0.1.2

3 years ago

0.1.1

3 years ago

0.1.0

3 years ago