Build lightning fast web forms from JSON

react-json-form-engine

Build lightning fast web forms from JSON.

:heart: Conditional logic


:heart: Flexible validation


:heart: Infinite depth


:heart: Rehydratable

While other libraries might utilize react-redux, or the refs or context API for complex form managagement, react-json-form-engine relies on React as little as possible, and offloads its core logic to plain JavaScript. The result is scalable, lightning fast performance with neglible reliance on the React lifecycle.

Before proceeding, it's important to note that this library was designed to manage large forms (multi-section and multi-subsection), that may contain complex field dependencies (e.g Only show the Select Guardian field if the Age response is less than 18). This may or may not be for you, but it can also handle simple forms with extreme ease.

It also offers an easy mechanism for serializing all form responses to JSON for persistence. The reverse also stands, as any form can be easily rehydrated from historical data, and returned to its previous state.


Table of Contents


Live Demo

https://mikechabot.github.io/react-json-form-engine-storybook/

Storybook repository located here

Installing

Requires React 15.0.0+

$ npm install --save react-json-form-engine

Note: This library renders Bulma semantics; you'll need to include the styles on your own for everything to look nice. You can either install it with npm, or have it served from a CDN.

Note: Font Awesome is supported.

Bulma via CDN

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.1/css/bulma.min.css">

Bulma via npm

$ npm install --save bulma

  1. If your project supports Sass/SCSS, Bulma can be over easily overriden:
/* my-awesome-styles.scss */

// 1. Import the initial variables
@import "../sass/utilities/initial-variables";
@import "../sass/utilities/functions";

// 2. Set your own initial variables
$blue: #72d0eb;

// 3. Import the rest of Bulma
@import "../bulma";
  1. Depending on your build pipeline, either import the compiled CSS, or uncompiled SCSS.
// App.js
import './scss/my-awesome-styles.scss';

Font Awesome

If you'd like to use Font Awesome, be sure to also include the icon pack:

<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.0.13/css/all.css">

Starter Template

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>react-json-form-engine</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.1/css/bulma.min.css">
    <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.0.13/css/all.css">
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

Getting Started

First, let's import the API:

import { Form, FormEngine } from 'react-json-form-engine';

Next, we'll need to build a Form Definition, which is the skeleton structure that describes how the form should look and behave. The definition must adhere to a strict schema, and can be represented as a JavaScript object or a JSON Schema. But don't worry about the details yet, we'll get into those.

Once we've built our definition, we'll feed it to the FormEngine, which returns an instance:

const instance = new FormEngine(definition);

Then, we just pass the instance to the <Form /> component, and react-json-form-engine takes care of the rest:

<Form
  instance={instance}
  onSubmit={() => {
    const responses = instance.serializeModel();
    // Do stuff
  }}
/>

Login Form Example

Let's create a simple login form. Either follow along below, or check out the codesandbox.

Show Title Hide Title

Login Form Definition

Here's our definition, which is a rather simple one. It consists of just a single section with a single subsection, which houses three fields. Note, we're also using a Field Decorator to ensure user_pass renders as a password field:

const loginForm = {
  id: "login_form",
  title: "Welcome to Foo!",
  sections: [
    {
      id: "section_1",
      title: "Login Section",
      subsections: [
        {
          id: "subsection_1",
          title: "Login",
          subtitle: "Please enter your credentials.",
          fields: [
            {
              id: "user_name",
              title: "Username",
              type: "string"
            },
            {
              id: "user_pass",
              title: "Password",
              type: "string"
            },
            {
              id: "remember_me",
              title: "Remember me",
              type: "boolean"
            }
          ]
        }
      ]
    }
  ],
  decorators: {
    "user_pass": {
      component: {
        type: "password"
      }
    }
  }
};

Now that we have our definition, let's create an instance of FormEngine:

const instance = new FormEngine(loginForm); 

With the instance in hand, we can pass it our <Form /> component:

const LoginForm = () => (
  <Form
    instance={instance}
    onSubmit={() => {
       // Do stuff
    }}
  />
);

And once filled out, onSubmit will get us the form responses:

const LoginForm = () => (
  <Form
    hideTitle
    submitButtonLabel="Login"
    instance={instance}
    onSubmit={() => {
      console.log(instance.getModelValues());   // Get form responses
      console.log(instance.serializeModel());   // Serialize form responses
    }}
  />
);

Login Form Codesandbox

Have a look at the Login Form demo:


Form Engine

Form Definition

Form definitions adhere to a strict schema. They must contain at least one section, which contains at least one subsection, which contains at least one Field Definition. You may find this schema verbose for smaller forms, however it scales well for significantly complex forms.

View the full schema in the FormAPIService

In forms with a single section, vertical tabs are not displayed. In sections with a single subsection, horizontal tabs are not displayed.

// The most minimal form possible
export default {
    id: <string>,
    title: <string>,
    sections: [
        {
            id: <string>,
            title: <string>,
            subsections: [
                {
                    id: <string>,
                    title: <string>,
                    fields: [
                        {
                            ...
                        }
                    ]
                }
            ]
        }
    ]
};

Have a look the Simple Form demo:


Form Definition Validation

Don't worry about making mistakes with your definition. If the FormEngine is instantiated with a malformed definition, the UI will be notified of the failure location:

Have a look at the Malformed Form demo:


Field Definition

Field definitions also adhere to a strict schema. At minimum, they must contain an id, type and title:

// The most minimal field object
{
  id: <string>,       // Uniquely identifies the field within the DOM, and FormEngine instance
  type: <string>,     // Determines the data type of the field response
  title: <string>     // Label of the field
}

Field Type

Determines the data type of the response value stored in the model, and which Default Control to render. To override the default and render an Allowed Control instead, use a Field Decorator.

Field Type Default Control Allowed Controls Supports options?
string <Text /> <Password />, <Textarea />, <Select />, <Radio /> Yes*
boolean <Checkbox /> <Radio /> Yes*
number <Number /> <Range /> No
array <Select /> <Checkboxgroup /> Yes
date <DateTime /> N/A No

Some field types will automatically transition from their Default Control to another Allowed Control if an options array is present in the field definition. (See Field Type Transitions). However, in most cases, you must use a
Field Decorator to use another Allowed Control.


Field Children

Any field can contain child fields. Simply create a fields array on the field, and drop in valid Field Definitions. Here's an example of some nested fields:

Note: Field children can recurse infinitely, and also be placed on Field Options.

{
  id: "parent",
  type: "number",
  title: "Parent",             
  fields: [
    {
      id: "child",
      type: "string",
      title: "Child",
      fields: [
        {
          id: "grandchild",
          type: "number",
          title: "Grandchild"
        }
      ]
    },
    {
      id: "child-2",
      type: "array",
      title: "Child",
      options: [
        { id: 'op1', title: 'Option 1'},
        { id: 'op2', title: 'Option 2' },
      ]
    }
  ]
}

Have a look at the Child Fields demo:


Field Options

Applies to string, boolean, and array field types only.

boolean

Fields of type boolean only accept a maximum of two options; each of which should contain just a title property. The first option is considered the affirmative response:

{
  id: 'my_bool',
  title: 'How often does it occur?',
  type: 'boolean',
  options: [
    { title: 'Always' },
    { title: 'Never' },
  ]
}

string / array

For field types that accept unlimited options (string, array), you must include both an id and title. The ids of the selected option(s) are stored in the model.

{
  id: 'my_arr',
  title: 'Pick some',  
  type: 'array',      // Array type allows for multiple selections
  options: [
    { id: "op1", title: "Option 1" },
    { id: "op2", title: "Option 2" },
    { id: "op3", title: "Option 3" },
  ]
},
{
  id: 'my_str',
  title: 'Pick one',
  type: 'string',    // String type allows for single selection
  options: [
    { id: "op1", title: "Option 1" },
    { id: "op2", title: "Option 2" },
    { id: "op3", title: "Option 3" },
  ]
}

Field Children on Options

For field controls that render selectable options, like <Radio /> (incarnated as a string or boolean) or <Checkboxgroup />, you can include Field Children on any of the options:

{
  id: "field_2",
  type: "string",
  title: "Select One (Field Type: String)",
  options: [
    {
      id: "op1",
      title: "Option 1",
      fields: [{ id: "explain_1", type: "string", title: "Explain" }]
    },
    {
      id: "op2",
      title: "Option 2",
      fields: [{ id: "explain_2", type: "string", title: "Explain" }]
    },
    {
      id: "op3",
      title: "Option 3",
      fields: [{ id: "explain_3", type: "string", title: "Explain" }]
    }
  ]
}

Have a look at the Field Options demo:


Field Props List

Here's the complete list of props that can be passed to Field Definitions:

Property Type Required Description
id string Yes See Field ID
type string Yes See Field Type
title string Yes Display label for the field
options array No See Field Options
fields array No See Field Children
placeholder string No Placeholder text to display
showCondition object No Condition object (See Conditions)
required boolean No Whether the field is required (See Validation)
pattern string No Pattern to match during validation (See Validation)
min number Yes* Minimum value. (Used for number field types)
max number Yes* Maximum value. (Used for number field types)
hideTime boolean No Only show the Date in Date/Time. (Used for date field types)
hideCalendar boolean No Only show the Time in Data/Time. (Used for date field types)

min and max are only required for <Range /> component types.


Field Type Transitions

string

By default, a string field is rendered as <Text /> (See Field Type), but with options it automatically renders as a <Select />.

[
  { 
    // Renders as <Text />
    id: 'field_1',
    type: 'string', 
    title: 'Text Field'
  },
  {             
    // Renders as <Select />
    id: 'field_2',
    type: 'string',
    title: 'Select Field',
    options: [
      { id: "op1", title: "Option 1" },
      { id: "op2", title: "Option 2" },
    ]
  }
]

Have a look at the String Transition demo:


boolean

By default, a boolean field is rendered as <Checkbox /> (See Field Type), but with options it automatically renders as a <Radio />.

[
  {
    id: "field_1",
    type: "boolean",
    title: "Checkbox Field"
  },
  {
    id: "field_2",
    type: "boolean",
    title: "Radio Field",
    options: [
      { title: "Yes" },
      { title: "No" }
    ]
  }
]

A maximum of two (2) options is allowed for boolean fields. For unlimited <Radio /> options, use the string type with a component of radio.

Have a look at the Boolean Transition demo:


Field Decorators

Field decorators contain metadata about the fields you've configured in your form definition. Add the decorators object to the root of the Form Definition:

  {
    id: 'my_form'
    title: 'My Form',
    sections: [...],
    decorators: {}
  }

The decorators object will be keyed by Field ID, and can contain the properties hint and component.


Hint Decorator

Add hint text to any field:

  id: "Form_ID",
  title: "Form Title",
  sections: [{
    ...
    subsections: [{
      ...     
      fields: [{
        id: "field_1",
        type: "string",
        title: "Field title"
      }]
    }]
  }],
  decorators: {
    field_1: {
      hint: "This is some hint text!"   // Add hint text to any field
    }
  }
}

Take a look at the Hint Decorator demo:


Component Decorator

Every field type renders a Default Control (See Field Type), however you'll often want to explicitly override the default component type in favor of another. In some cases, this occurs automatically (See Field Type Transitions, however most times you'll need to specify a component decorator.

Let's update field_1 from a <Select /> to a <Checkboxgroup />:

  id: "Form_ID",
  title: "Form Title",
  sections: [{
    ...
    subsections: [{
      ...     
      fields: [{
        id: "field_1",
        type: "array",
        title: "Field title",
        options: [
           ...
        ]
      }]
    }]
  }],
  decorators: {
    field_1: {
      hint: 'More hint text!',
      component: {
        type: 'checkboxgroup'   // Override the default component type
      }
    }
  }
}

Here's a list of field types with overrideable components:

Field Type Component Decorator Overrides
string password, textarea, radio
number range
array checkboxgroup

Take a look a the Component Override demo:

GitHub