/react-material-ui-form-builder

Simple form builder using Material UI components

Primary LanguageJavaScriptMIT LicenseMIT

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
  • KeyboardDatePicker
  • KeyboardDateTimePicker
  • KeyboardTimePicker
  • 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 install for this package to work.

@date-io/date-fns
date-fns
@jeremyling/react-material-ui-rich-text-editor
@material-ui/core
@material-ui/icons
@material-ui/lab
@material-ui/pickers
lodash
react-beautiful-dnd
react-color
react-player

Usage Example

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 = ["Active", "Inactive"];

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

import React, { useState } from "react";
import FormBuilder from "@jeremyling/react-material-ui-form-builder";
import _ from "lodash";
import { makeStyles } from "@material-ui/core/styles";

const useStyles = makeStyles((theme) => ({
  flexRow: {
    flexDirection: "row",
  },
}));

export default function EmployeeForm(props) {
  const [form, setForm] = useState({});

  const updateForm = (key, value) => {
    const copy = JSON.parse(JSON.stringify(form));
    _.set(copy, key, value);
    setForm(copy);
  };

  const fields = [
    {
      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",
        },
      },
    },
    {
      // Default component is Material UI's TextField
      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,
      },
    },
    {
      attribute: "email",
      label: "Email",
      col: {
        sm: 6,
      },
      component: "text-field",
      props: {
        // Here you can pass any props that are accepted by Material UI's TextField component
      },
      validationType: "string",
      validations: {
        required: true,
        email: true,
      },
    },
    {
      attribute: "jobId",
      label: "Job",
      col: {
        sm: 6,
      },
      component: "select",
      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
      },
    },
    {
      attribute: "details.joinDate",
      label: "Join Date",
      col: {
        sm: 6,
      },
      component: "date-picker",
      props: {
        // Here you can pass any props that are accepted by Material UI's KeyboardDatePicker component
      },
    },
    {
      attribute: "status",
      label: "Status",
      col: {
        sm: 6,
      },
      component: "select",
      options: statuses, // optionConfig not required as options is an array of strings
      props: {
        // Here you can pass any props that are accepted by Material UI's Select component
      },
      idPrefix: "select",
    },
    {
      attribute: "status",
      title: "Status", // You can omit this if you do not want a title for the field
      col: {
        sm: 6,
      },
      component: "checkbox-group",
      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) {
            updateForm("status", "Active");
          } else {
            updateForm("status", "Inactive");
          }
        },
      },
      labelProps: {
        // Here you can pass any props that are accepted by Material UI's FormControlLabel component
        variant: "body2",
      },
      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",
      },
      labelProps: {
        // Here you can pass any props that are accepted by Material UI's FormControlLabel component
        variant: "body2",
      },
      groupContainerProps: {
        // Here you can pass any props that are accepted by Material UI's FormControl component
        classes: {
          root: classes.flexRow,
        },
      },
      idPrefix: "radio",
    },
    {
      attribute: "status",
      label: "Active",
      col: {
        sm: 6,
      },
      component: "switch",
      props: {
        // Here you can pass any props that are accepted by Material UI's Radio component
        color: "secondary",
        checked: _.get(form, "status") === "Active",
        onChange: (event) =>
          event.target.checked
            ? updateForm("status", "Active")
            : updateForm("status", "Inactive"),
      },
      idPrefix: "switch",
    },
    {
      attribute: "skills",
      label: "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
        style: { overflow: auto },
      },
    },
    {
      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:
        (jobs.find((j) => j.id === form.jobId) || {}).title ===
        "Entry Level Staff", // This will hide the form field if the condition is truthy
    },
    {
      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
      },
    },
  ];

  return (
    <FormBuilder
      fields={fields}
      form={form}
      updateForm={(key, value) => updateForm(key, value)}
    />
  );
}

Props

Prop Type Default Description
title string undefined Form title
fields array required Array of form fields along with props (details below)
form object required Form object to be filled
updateForm func (key, value) => {} Method to update form[key] to value
children node undefined Additional content to the right of the form
index string or number undefined To uniquely identify fields if FormBuilder is used in a loop
idPrefix string undefined To uniquely identify fields if multiple fields use the same attribute

Field Props

Prop Type Default Description
attribute string undefined Form attribute that controls input and is modified by input
label string undefined Component label for text-field, select, autocomplete, date-picker, date-time-picker, time-picker, switch. Can be omitted if label is not required.
title string undefined Title for component. Can be used to describe input or hold a question.
titleProps object undefined Title props passed to Typography component wrapping title
titleSuffix string undefined Title suffix to append to title. Could be used to denote required fields
titleSuffixProps object undefined Title suffix props passed to Typography component wrapping title suffix
titleContainerProps object undefined Props passed to container wrapping the title and titleSuffix
col object { xs: 12 } Grid columns that component should take
component string text-field One of:
text-field,
select,
date-picker,
date-time-picker,
time-picker,
autocomplete,
chip-group,
checkbox-group,
radio-group,
switch,
file-upload,
image-picker,
rating
display-text,
display-image,
display-media,
rich-text
custom
options array [] Required if component is one of select, autocomplete, chip-group, checkbox-group or radio-group
optionConfig object select, chip-group, checkbox-group, radio-group:
{ key: option, value: option, label: option }
autocomplete:
{ value: option, label: option }
Required if options is an array of objects
randomizeOptions bool undefined If true, randomises option order on each render
multiple bool undefined Only for chip-group, checkbox-group and image-picker. If true, multiple options will be selectible
sortable bool undefined Only for autocomplete. If true, selected options will be sortable via drag and drop
acceptTypes string or array [".pdf", ".doc", ".docx", ".xml", "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", ".xls", ".xlsx", ".csv", "image/*", "audio/*", "video/*"] Only for file-upload. Concatenated value will be passed as accept prop to input
maxSizeMb number 2 Only for file-upload. Max size of each uploaded file.
fileType string undefined Only for file-upload. One of: file, image, audio, video.
imageUrl string undefined Only for file-upload. If file type is an image, you may specify the url of the existing image here.
imageSize array undefined Only for file-upload. Size of image preview in the form [width, height]. imageSize supercedes aspectRatio.
aspectRatio array undefined Only for file-upload and image-picker. Aspect ratio of image preview in the form [width, height]. imageSize supercedes aspectRatio.
src string undefined Only for display-image and display-media. Source of image or media.
alt string undefined Only for display-image. Alt passed to img node.
images array undefined Only for image-picker. This should contain an array of objects with attributes src, label and alt (defaults to label)
imageCols number { xs: 2 } Only for image-picker. 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
iconColor string undefined Only for rating. Icon colour
width number undefined Only for display-media. Width of media player.
height number undefined Only for display-media. Height of media player.
labelLines number 2 Only for image-picker. Number of lines allowed for label
props object undefined Any additional props to pass to the Material UI component
containerProps object undefined Any additional props to pass to the Material UI Grid item that contains the component
labelProps object undefined Only for checkbox-group, radio-group and switch, image-picker. Any additional props to pass to Material UI's FormControlLabel or Typography (image-picker) that wraps the label.
groupContainerProps object undefined Only for chip-group, checkbox-group, radio-group and image-picker. Any additional props to pass to Material UI's FormControlGroup or ImageList (image-picker) that wraps the individual components within the group.
hideCondition bool undefined Hides field if truthy
customComponent func undefined Function that accepts the props (field, form, updateForm) and returns a node
validationType^ string undefined One of: mixed, string, number, date, boolean, array.
validations^ object undefined These are validation options accepted by yup in the form of {validation: arguments}. Arguments can be a string or an array of strings in the order that it is accepted by the yup option. For validations that do not require any arguments, set the argument to true.

^See below for examples

Rich Text Field Props

Prop Type Default Description
html string undefined HTML to be deserialized as content. Document takes precedence.
document array undefined Document set as value for the Slate Context Provider. Takes precedence over HTML
onChange func () => {} Method passed to Slate Context Provider's onChange prop
onBlur func (html) => {} Additional method to run triggered by the onBlur event on the wrapper component. By default, the serialized html will be passed as the only argument.
containerProps object undefined Props to pass to the Material UI Paper wrapper
editableProps object undefined Props 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,
  }
}

For dates, most validation can already be done with Material UI's pickers.