1.0.4 • Published 3 months ago

react-form-crafter v1.0.4

Weekly downloads
-
License
ISC
Repository
-
Last release
3 months ago

react-form-crafter: A React-Based Form Generator

It is a React package designed to dynamically generate forms based on a provided JSON schema. It streamlines the process of building interactive, data-driven forms with support for custom fields, widgets, templates, and validation.

Installation

Install react-form-crafter with npm

  npm install react-form-crafter

Usage/Examples

function App() {
  const [formData, setFormData] = useState();

  const templates = {
    myCustomRowTemplate: CustomTemplate,
  };

  const fields = {
    CustomPassword: PasswordWidget,
    CustomGenPassword: PasswordGenWidget,
    DatePickerWidget: DatePickerWidget,
  };

  const handleFormSubmit = (data) => {
    setFormData(data);
    console.log("data : ", data);
  };

  const handleOnError = () => {
    console.log("Error occured");
  };

  const handleOnSuccess = () => {
    window.alert("Form Submitted");
  };

  const handleOnChange = (data) => {
    console.log("data : ", data);
  };

  return (
    <MyForm
      schema={schema}
      uiSchema={uiSchema}
      templates={templates}
      widgets={widgets}
      fields={fields}
      onSubmit={handleFormSubmit}
      onChange={handleOnChange}
      onSuccess={handleOnSuccess}
      onError={handleOnError}
      formData={formData}
    />
  );
}

export default App;

Schema

The schema defines the structure of the form fields.

The uiSchema customizes the appearance and behavior of the form elements

{
        name: 'login',
        title: 'Sign In',
        **schema**: {  // 🟢 Schema Definition
          type: 'object',
          properties: {
            email: {
              type: 'string',
              title: 'Email',
            },
            password: {
              type: 'string',
              title: 'Password',
              minLength: 3,
              maxLength: 15,
            },
          },
          required: ['email', 'password'],
        },
        uiSchema: {   // 🟢 uiSchema Definition
          'ui:submitButtonOptions': {
            submitText: 'Login',
            hide: false, //Default to false
            props: {
              disabled: false,
              className: 'LoginBtn primaryButton d-flex justify-content-center align-items-center',
            },
          },
          email: {
            'ui:widget': 'email',
            'ui:placeholder': 'Enter your email',
            className: 'w-100 fw-bold primary p-0 mb-2 title-m',
            'ui:options': {
              label: true,
            },
          },
          password: {
            'ui:widget': 'CustomPassword',
            'ui:placeholder': 'Enter your password',
            className: 'w-100 fw-bold primary p-0 mb-1 title-m',
            'ui:options': {
              label: true,
            },
          },
        },
      },
KeyTypeLocationDescription
hidebooleanui:submitButtonOptionsDetermines whether the submit button is hidden (true) or shown (false).
labelbooleanui:optionsDetermines whether the field label is displayed (true) or hidden (false).

Equivalent HTML code:

    <div class="form-group w-100 fw-bold primary p-0 mb-2 title-m">
      <label class="form-label">Email<span>*</span></label>
      <input type="text" name="email" class="form-control" placeholder="Enter your email" value="kartik.gupta@aloissolutions.com">
    </div>
    <div class="form-group w-100 fw-bold primary p-0 mb-1 title-m m-0">
      <label class="form-label">Password<span>*</span></label>
      <div class="input-group">
        <input type="password" class="form-control" placeholder="Enter your password" value="password1">
        <button type="button" class="btn" style="border-top: 1px solid rgb(206, 212, 218); border-right: 1px solid rgb(206, 212, 218); border-bottom: 1px solid rgb(206, 212, 218); border-left: none; border-image: initial; background-color: white;">
          <svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 640 512" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg">
          </svg>
        </button>
      </div>
    </div>
  • Classes mentioned in className of individual field are applied to div which is wrapping label and input of that field.
  • Classes mentioned in className inside ui:options are applied on the input tag.

eg. for above two statements

email: {
    'ui:widget': 'email',
    'ui:placeholder': 'Enter your email',
    'className': 'w-100 fw-bold primary p-0 mb-2 title-m'
  },
<div class="form-group w-100 fw-bold primary p-0 mb-2 title-m">
      <label class="form-label">Email<span>*</span></label>
      <input type="text" name="email" class="form-control" placeholder="Enter your email" value="kartik.gupta@aloissolutions.com">
</div>
email: {
    'ui:widget': 'email',
    'ui:placeholder': 'Enter your email',
    'className': 'w-100 fw-bold primary p-0 mb-2 title-m'
    'ui:options': {
      'className': 'bg-light'
    }
  },
<div class="form-group w-100 fw-bold primary p-0 mb-2 title-m">
      <label class="form-label">Email<span>*</span></label>
      <input type="text" name="email" class="form-control bg-light" placeholder="Enter your email" value="kartik.gupta@aloissolutions.com">
</div>
uiSchema: {
    hideTitle: true,
    layout: [
      {
        type: 'section',
        title: 'Employee Information',
        className: 'row',
        fields: [
          {
            type: 'section',
            className: 'col-4',
            fields: ['info.profilePic',]
          },
          {
            type: 'section',
            className: 'col-4',
            fields: ['info.firstName', 'info.lastName'],
          },
          {
            type: 'section',
            className: 'col-4',
            fields: ['info.empId', 'info.designation'],
          },
        ],
      },
      {
        type: 'section',
        title: 'Personal Information',
        className: 'row',
        fields: [
          {
            type: 'section',
            className: 'col-4',
            fields: ['personalInfo.gender', 'personalInfo.nationality', 'personalInfo.residence'],
          },
          {
            type: 'section',
            className: 'col-4',
            fields: ['personalInfo.dateOfBirth', 'personalInfo.phoneNumber'],
          },
          {
            type: 'section',
            className: 'col-4',
            fields: ['personalInfo.bloodGroup', 'personalInfo.address']
          }
        ],
      },
    ],

    "ui:submitButtonOptions": {
      submitText: "Submit",
      props: {
        disabled: false,
        className: "primaryButton",
      },
    },
    info: {
      className: "employe-info",
      profilePic: {
        "ui:widget": "ProfileImage",
        "ui:options": {
          accept: [
            "image/jpeg",
            "image/png",
            "image/gif",
            "image/bmp",
            "image/webp",
            "image/svg+xml",
            "image/tiff",
            "image/x-icon",
          ],
          className: "profile-pic",
        },
      },
      firstName: {
        className: "first-name",
        "ui:placeholder": "Enter First Name",
      },
      lastName: {
        className: "last-name",
        "ui:placeholder": "Enter Last Name",
      },
      empId: {
        className: "emp-id",
        "ui:placeholder": "Enter Employee ID",
      },
      designation: {
        className: "designation",
        "ui:placeholder": "Enter Designation",
      },
      preview: true,
    },
    personalInfo: {
      className: "personal-info employe-info",
      gender: {
        className: "gender",
        "ui:placeholder": "Select Gender",
        "ui:widget": "select",
      },
      dateOfBirth: {
        className: "date-of-birth",
        "ui:widget": "DatePickerWidget",
      },
      bloodGroup: {
        className: "blood-group",
        "ui:placeholder": "Select Blood Group",
        "ui:widget": "select",
      },
      nationality: {
        className: "nationality",
        "ui:placeholder": "Enter Nationality",
      },
      phoneNumber: {
        "ui:widget": "CustomPhoneNumber",
        className: "phone-number",
        pattern_message: ["Invalid phone number"],
      },
      address: {
        className: "address",
        "ui:placeholder": "Enter Address",
      },
      residence: {
        className: "residence",
        "ui:placeholder": "Select Residence",
        "ui:widget": "select",
      },
    }
  }

Layout key divides fields into several divisions. className key can be used to apply classes to individual section. (Below html code demonstrates row and col-4 classes applied to sections by use of schema). titleClass key can be used to give classes to title.

<div class="w-100 row justify-content-around">
  <div class="w-100 mb-2">
    <h5 class="mb-2">Employee Information</h5>
    <div class="row">
      <div class="col-4">
        <div class="form-group profile-pic m-0">
          <label class="form-label">Profile Image Upload</label>
          <div class="fileupload-preview d-flex align-items-center">
            <img
              src="https://t3.ftcdn.net/jpg/05/53/79/60/360_F_553796090_XHrE6R9jwmBJUMo9HKl41hyHJ5gqt9oz.jpg"
              alt="Profile preview"
              class="single-user-profile"
              style="width: 130px; height: 130px; margin-right: 10px; cursor: pointer;">
            <div class="d-flex gap-2">
              <button class="btn btn-sm btn btn-primary primaryButton" type="button">Change Picture</button>
            </div>
            <input type="file" capture="user" accept="image/*" style="display: none;">
          </div>
        </div>
      </div>
      <div class="col-4">
        <div class="form-group first-name">
          <label class="form-label">First Name</label>
          <input type="text" class="form-control" name="firstName" placeholder="Enter First Name" value="">
        </div>
        <div class="form-group last-name">
          <label class="form-label">Last Name</label>
          <input type="text" class="form-control" name="lastName" placeholder="Enter Last Name" value="patel">
        </div>
      </div>
      <div class="col-4">
        <div class="form-group emp-id">
          <label class="form-label">Employee ID</label>
          <input type="text" class="form-control" name="empId" placeholder="Enter Employee ID" value="">
        </div>
        <div class="form-group designation">
          <label class="form-label">Designation</label>
          <input type="text" class="form-control" name="designation" placeholder="Enter Designation" value="">
        </div>
      </div>
    </div>
  </div>

  <div class="w-100 mb-2">
    <h5 class="mb-2">Personal Information</h5>
    <div class="row">
      <div class="col-4">
        <div class="form-group gender">
          <label class="form-label">Gender</label>
          <select name="gender" class="form-select" placeholder="Select Gender">
            <option value="">Select an option</option>
            <option value="male">Male</option>
            <option value="female">Female</option>
            <option value="other">Other</option>
          </select>
        </div>
        <div class="form-group nationality">
          <label class="form-label">Nationality</label>
          <input type="text" class="form-control" name="nationality" placeholder="Enter Nationality" value="">
        </div>
        <div class="form-group residence">
          <label class="form-label">Residence</label>
          <select name="residence" class="form-select" placeholder="Select Residence">
            <option value="">Select an option</option>
            <option value="Non-US">Non-US</option>
            <option value="US">US</option>
          </select>
        </div>
      </div>
      <div class="col-4">
        <div class="form-group date-of-birth m-0">
          <label class="form-label">Date Of Birth</label>
          <div class="form-group">
            <input type="date" class="form-control" value="" max="2004-02-07">
          </div>
        </div>
        <div class="form-group phone-number m-0">
          <label class="form-label">Phone Number</label>
          <div class="mb-3">
            <div class="input-group">
              <select class="form-select" name="countryCode" style="max-width: 4.4rem;">
                <option value="+91">🇮🇳 (+91)</option>
                <option value="+1">🇺🇸 (+1)</option>
                <option value="+506">🇨🇷 (+506)</option>
                <option value="+44">🇬🇧 (+44)</option>
                <option value="+31">🇳🇱 (+31)</option>
                <option value="+48">🇵🇱 (+48)</option>
                <option value="+971">🇦🇪 (+971)</option>
                <option value="+61">🇦🇺 (+61)</option>
              </select>
              <input type="tel" class="form-control" placeholder="Enter phone number" name="phoneNumber" pattern="^[0-9]{6,14}$" title="Enter a valid phone number" value="">
            </div>
          </div>
        </div>
      </div>
      <div class="col-4">
        <div class="form-group blood-group">
          <label class="form-label">Blood Group</label>
          <select name="bloodGroup" class="form-select" placeholder="Select Blood Group">
            <option value="">Select an option</option>
            <option value="A+">A+</option>
            <option value="A-">A-</option>
            <option value="B+">B+</option>
            <option value="B-">B-</option>
            <option value="AB+">AB+</option>
            <option value="AB-">AB-</option>
            <option value="O+">O+</option>
            <option value="O-">O-</option>
          </select>
        </div>
        <div class="form-group address">
          <label class="form-label">Address</label>
          <input type="text" class="form-control" name="address" placeholder="Enter Address" value="">
        </div>
      </div>
    </div>
  </div>
</div>

If layout is not provided then fields having type object are assigned ids which can be further used in custom css files to apply styling.

type: "object",
    properties: {
      info: {
        type: "object",
        title: "Employee Information",
        properties: {
          profilePic: {
            type: "string",
            format: "data-url",
            title: "Profile Image Upload",
          },
          firstName: {
            type: "string",
            title: "First Name",
            default: "DefaultFirstName",
          },
        },
        required: ["firstName", "lastName", "empId", "designation"],
      },
      personalInfo: {
        type: "object",
        title: "Personal information",
        properties: {
          gender: {
            type: "string",
            title: "Gender",
            oneOf: [
              { const: "male", title: "Male" },
              { const: "female", title: "Female" },
              { const: "other", title: "Other" },
            ],
            default: "other",
          },
          dateOfBirth: {
            type: "string",
            format: "date",
            title: "Date Of Birth",
            dateType: "dateOfBirth",
          },
        },
      }
    }

Below html code demonstrates that according to schema having two objects info and personalInfo, the id root_info is provided to the div containing fields mentioned in info object.

<div>
  <legend id="root_info__title" class="mt-2">
    Employee Information<span>*</span>
  </legend>

  <div id="root_info" class="employee-info">
    <div class="form-group profile-pic m-0">
      <label class="form-label">Profile Image Upload</label>
      <div class="fileupload-preview d-flex align-items-center">
        <img
          src="https://t3.ftcdn.net/jpg/05/53/79/60/360_F_553796090_XHrE6R9jwmBJUMo9HKl41hyHJ5gqt9oz.jpg"
          alt="Profile preview"
          class="single-user-profile"
          style="width: 130px; height: 130px; margin-right: 10px; cursor: pointer;">

        <div class="d-flex gap-2">
          <button class="btn btn-sm btn-primary primaryButton" type="button">Change Picture</button>
        </div>

        <input type="file" capture="user" accept="image/*" style="display: none;">
      </div>
    </div>

    <div class="form-group first-name">
      <label class="form-label">First Name<span>*</span></label>
      <input type="text" class="form-control" name="firstName" placeholder="Enter First Name" value="DefaultFirstName">
    </div>
  </div>
</div>

Widgets

  • can be mentioned using ui:widget:"widget_name" in uiSchema.
Widget NameDescription
stringRenders a standard text input field for single-line text input.
textAlias for string, renders a single-line text input.
TextWidgetRenders a standard text input field for single-line text input.
alt-dateRenders an alternate date input field with customized formatting.
passwordRenders a password input field that masks user input for sensitive data.
emailRenders an input field for email addresses with validation for email format.
fileRenders a file upload input allowing users to select and upload files.
buttonRenders a button element, typically used for form submission or actions.
calendarRenders a calendar picker for selecting dates directly from a calendar view.
checkboxesRenders a group of checkboxes for multiple selection options.
dateRenders a standard date input field for selecting dates.
daterangeRenders inputs to select a start date and an end date for date range selection.
datetimeRenders a date and time input field.
dayRenders an input field specifically for selecting a day of the month.
monthRenders an input field specifically for selecting a month.
progressRenders a progress bar input, useful for visualizing completion percentage.
radioRenders a group of radio buttons for single-option selection.
rangeRenders a slider input for selecting a range value.
selectRenders a dropdown (select) menu for single-option or multi-option selection.
timeRenders an input field for selecting time.
updownRenders a numeric input with increment and decrement controls.
UpDownWidgetRenders a numeric input with increment and decrement controls.
yearRenders an input field for selecting a year.
numberEnumRenders a group of radio buttons for numeric options.
textareaRenders a multi-line text input area for longer text input.

Usage of button widget mentioned above :

const schema = {
  type: "object",
  properties: {
    actionButton: {
      title: "Action Button",
    },
    myButton: {
      title: "My Button",
    },
  },
};
const uiSchema = {
  type: "object",
  properties: {
    actionButton: {
      "ui:widget": "button",
      "ui:options": {
        value: "Custom button 1",
        onClick: () => {
          window.alert("Custom button pressed");
        },
      },
      className: "btn btn-success mt-3",
    },
    myButton: {
      "ui:widget": "button",
      "ui:options": {
        value: "Custom button 2",
        onClick: () => {
          window.alert("My button pressed");
        },
      },
      className: "btn border rounded-1 btn-danger mt-3",
    },
  },
};

Output : Alt text

Custom Widgets

import React from "react";
import { FaRegEyeSlash, FaEye } from "react-icons/fa6";

const PasswordWidget = (props) => {
  const [showPassword, setShowPassword] = React.useState(false);

  const togglePasswordVisibility = () => {
    setShowPassword((prev) => !prev);
  };

  const handleChange = (e) => {
    props.onChange(e.target.value);
  };

  return (
    <div className="input-group">
      <input
        type={showPassword ? "text" : "password"}
        className="form-control"
        placeholder={props.placeholder}
        value={props.value || ""}
        onChange={handleChange}
        onBlur={props.onBlur}
        onFocus={props.onFocus}
        required={props.required}
      />
      <button
        type="button"
        onClick={togglePasswordVisibility}
        className="btn"
        style={{
          border: "1px solid #ced4da",
          borderLeft: "none",
          borderRadius: "0.5",
          backgroundColor: "white",
        }}
      >
        {showPassword ? <FaEye /> : <FaRegEyeSlash />}
      </button>
    </div>
  );
};

export default PasswordWidget;

Users just need to define input field. Label and errors are displayed by react-form-crafter.

Custom Fields

Whole component has to be designed by user including label, input fields and errors.

Props Provided to Custom Widgets and Custom Fields

When creating custom widgets, the following props are passed to them:

Prop NameDescription
schemaThe JSON Schema definition for the field.
uiSchemaThe UI Schema for customizing field appearance and behavior.
valueThe current value of the field in the form.
onChangeCallback function triggered when the field value changes.
onResetFunction to handle form reset.
onSubmitFunction to handle form submission.
errorsList of validation errors for the field, if any.
placeholderThe placeholder text specified in uiSchema["ui:placeholder"].
optionsAdditional options for the field as specified in uiSchema.

Custom Templates

  • To customize layout of the form

Props Provided to Custom Templates

Prop NameDescription
schemaThe JSON Schema definition for the form.
uiSchemaThe UI Schema for customizing form appearance and behavior.
contentThe rendered form fields and components. Place this inside the form tag in your custom template.
onSubmitFunction to handle form submission.
onResetFunction to handle form reset.
submitBtnOptionsOptions for customizing the submit button appearance and behavior.
resetBtnOptionsOptions for customizing the reset button appearance and behavior.

Example of customizing submit and reset buttont through schema

'ui:submitButtonOptions': {
            // hide: 'true', For hiding the submit button
            submitText: 'Send', //Will display text "Send" inside button
            props: {
              disabled: false,
              className: 'LoginBtn primaryButton d-flex justify-content-center align-items-center',
            },
          },
'ui:resetButtonOptions': {
            // show: true, To display the reset button
            clearText: 'Clear', //Will display text "Clear" inside button
            props: {
              disabled: false,
              className: 'LoginBtn primaryButton d-flex justify-content-center align-items-center',
            },
          }
  • submitText: Defines the text displayed on the submit button.
  • props: Allows customization of button properties, such as disabled state and className for styling.

Example of Custom Template

import React from "react";
import Button from "@mui/material/Button";

export default function CustomTemplate(props) {
  const { schema, content, onSubmit } = props;

  return (
    <div className="d-flex flex-column">
      <!-- Header is customizable -->
      <header className="mb-4 text-center">
        <h3> Custom Title </h3>
        <p> Custom Description </p>
      </header>

      <!-- Customizable form tag -->
      <form
        onSubmit={onSubmit}
        className="d-flex flex-column align-items-center p-5 border border-dark border-opacity-25 rounded-4 shadow"
        style={{ overflow: "auto" }}
      >
        {content} <!-- Use content prop to render input fields -->

        <!-- Customizable submit button -->
        <button
          className="mt-3"
          type="submit"
          style={{
            border: "2px solid rgb(0, 79, 162)",
            borderRadius: "2px",
            padding: "6px",
            fontWeight: "600",
            fontSize: "16px",
            backgroundColor: "rgb(0, 79, 162)",
            color: "#fff",
            transition: "all 0.3s ease",
            cursor: "pointer",
            boxShadow: "0 4px 8px rgba(0, 0, 0, 0.1)",
          }}
          onMouseEnter={(e) => {
            e.target.style.backgroundColor = "#0056b3";
            e.target.style.borderColor = "#0056b3";
          }}
          onMouseLeave={(e) => {
            e.target.style.backgroundColor = "#007bff";
            e.target.style.borderColor = "#007bff";
          }}
        >
          Submit
        </button>
      </form>

      <!-- Customizable footer -->
      <footer className="mt-4 text-center">Custom footer content</footer>
    </div>
  );
}

Validation

  • Required Fields: Enforces mandatory completion of specific fields.
  • Min/Max Length: Sets minimum and maximum character limits for string inputs (e.g., passwords).

    • How to Use:
      • To set a minimum length for a string input, use the minLength property.
      • To set a maximum length for a string input, use the maxLength property.
      {
        "type": "string",
        "minLength": 8,
        "maxLength": 20
      }
      • This ensures the field value falls within the defined character limits.
  • Minimum & Maximum value for field: Sets minimum and maximum value possible for input field.

    • How to Use:
      • To set a minimum length for a string input, use the minLength property.
      • To set a maximum length for a string input, use the maxLength property.
      {
        "type": "string",
        "minLength": 8,
        "maxLength": 20
      }
      • This ensures the field value falls within the defined character limits.
  • Pattern Matching: Ensures input conforms to a specific regex pattern (e.g., email and phone number formats).

    • How to Use:
      • Use the pattern property to provide a regular expression for the input value.
      {
        "type": "string",
        "pattern": "^\\S+@\\S+\\.\\S+$"
      }
      • This pattern checks for an email format.
  • Pattern Messages: Customizes error messages for pattern mismatches and allows multiple messages to be displayed if needed.

    • How to Use:
      • You can define the patternMessage property to specify custom error messages that should be displayed when the input doesn't match the defined pattern.
      • If you want to display multiple error messages for different patterns, you can use an array of messages.
      {
        "type": "string",
        "pattern": "^\\S+@\\S+\\.\\S+$",
        "patternMessage": [
          "Please enter a valid email address.",
          "Email format should be 'example@domain.com'."
        ]
      }
      • This allows flexibility in displaying multiple messages to guide the user in correcting their input.
  • Range Validation: Defines minimum and maximum values for numeric fields (e.g., integer range and progress).

    • How to Use:
      • For numeric fields, use minimum and maximum properties to specify valid numeric ranges.
      {
        "type": "number",
        "minimum": 1,
        "maximum": 100
      }
      • This ensures the numeric value is within the specified range.
  • Format Validation: Ensures data conforms to specific formats like email, date, time, or date-time.

    • How to Use:
      • Use the format property to specify predefined formats.
      {
        "type": "string",
        "format": "email"
      }
      • This validates that the input is a valid email.
  • File Type Check: Ensures that the selected file matches one of the accepted MIME types, as specified in the ui:options.accept property (e.g., ui:options.accept: "image/jpeg, image/png").

    • How to Use:
      • Set the ui:options.accept property to specify the valid MIME types.
      {
        "type": "string",
        "format": "data-url",
        "ui:options": {
          "accept": ["image/jpeg, image/png"]
        }
      }
      • This ensures the file matches the specified file types.

Error Messages

  • Required Fields: "This <field_name> is required."
  • Min/Max Length: "Input must be between 8 and 20 characters."
  • Pattern Matching: "Invalid email format."
  • Range Validation: "Value must be between 1 and 100."
  • File Type Check: "The selected file type is not supported."
1.0.4

3 months ago

1.0.3

3 months ago

1.0.2

3 months ago

1.0.1

3 months ago

1.0.0

4 months ago