@nolwenture/formik-jsonschema-form
TypeScript icon, indicating that this package has built-in type declarations

0.10.0 • Public • Published

formik-jsonschema-form

Installation

With npm:

npm install @nolwenture/formik-jsonschema-form

With yarn:

yarn add @nolwenture/formik-jsonschema-form

Testing

Run unit tests:

npm run test

To run storybook tests make sure you have Playwright installed:

npx playwright install

Run storybook tests:

npm run test-storybook

Usage

Basic usage

Pass your schema to the Form component, which should be passed as a child to the Formik component, for example:

import { Form } from "@nolwenture/formik-jsonschema-form";
import { Formik } from "formik";

const schema = {
  title: "My form",
  type: "object",
  properties: {
    firstName: { type: "string", title: "First name" },
    lastName: { type: "string", title: "Last name" },
  },
};

const uiSchema = {
  "ui:description": "Form description",
  firstName: { "ui:description": "Field description" },
};

export function MyFormComponent() {
  return (
    <Formik
      initialValues={{ firstName: "", lastName: "" }}
      onSubmit={handleSubmit}
    >
      <Form schema={schema} uiSchema={uiSchema}></Form>
    </Formik>
  );
}

Pass your uiSchema to the Formcomponent. which should be passed as a child to the Formikcomponent, just like schema. uiSchema will tell the form more about its own layout and styles.

Basic available elements to add to the form or a field of the form via uiSchema are:

  • "ui:title"
  • "ui:description"
  • "ui:help"
  • "ui:options"
  • "ui:info"

Most of those elements take a string for value, but ui:info takes an object with the possible keys of

  • label for the clickable anchor (if not provided, default value is "i")
  • content for the content of the popup container, and
  • position taking a string of either "top", "bottom", "left" or "right" (if not provided, default value is "top")

All elements of ui:info are customisable. The styledComponent is called InfoFieldButton and it consists of the following elements:

  • InfoFieldContainerElement
  • InfoFieldBoxElement
  • InfoFieldBoxInnerElement
  • InfoFieldBoxArrowElement
  • InfoFieldLabel
import { Form } from "@nolwenture/formik-jsonschema-form";
import { Formik } from "formik";

const schema = {
  title: "My form",
  type: "object",
  properties: {
    firstName: { type: "string", title: "First name" },
    lastName: { type: "string", title: "Last name" },
  },
};

const uiSchema = {
  "ui:description": "Form description",
  firstName: { "ui:description": "Field description" },
  lastName: {
    "ui:info": {
      label: "?",
      content: "Add your family name",
    },
  },
};

export function MyFormComponent() {
  return (
    <Formik
      initialValues={{ firstName: "", lastName: "" }}
      onSubmit={handleSubmit}
    >
      <Form schema={schema} uiSchema={uiSchema}></Form>
    </Formik>
  );
}

Using custom components

Form components (such as wrappers) can be overridden with custom ones

Here is an example of overriding FieldWrapper (wrapper used on all fields):

function CustomFieldWrapper({ children }) {
  return (
    <div
      style={{
        backgroundColor: "lightgrey",
        boxShadow: "1px 1px 5px gray",
        margin: "10px",
        padding: "10px",
      }}
    >
      {children}
    </div>
  );
}

...

return (
  <Formik initialValues={initialValues} onSubmit={handleOnSubmit}>
    <Form
      schema={schema}
      uiSchema={uiSchema}
      components={{
        FieldWrapper: CustomFieldWrapper,
      }}
    ></Form>
  </Formik>
);

And another example how to override InfoFieldButton:

const CustomInfoFieldButton = styled(InfoFieldButton)`
  outline: 2px red solid;
  margin: 0.5em;
  ${InfoFieldButton.styles.InfoFieldBoxInnerElementStyle} {
    min-width: 200px;
    background: orange;
    color: blue;
  }
  ${InfoFieldButton.styles.InfoFieldLabelStyle} {
    color: green;
  }
`;

...

return (
  <Formik initialValues={initialValues} onSubmit={handleOnSubmit}>
    <Form
      schema={schema}
      uiSchema={uiSchema}
      components={{
        InfoFieldButton: CustomInfoFieldButton,
      }}
    ></Form>
  </Formik>
);

Custom fields can be used in addition to all the default fields:

import { Form, FormRegistry } from "@nolwenture/formik-jsonschema-form";
import { useContext } from "react";

const schema = {
  title: "My form",
  type: "object",
  properties: {
    firstField: { title: "First field" },
  },
};

function MyCustomFieldComponent(props) {
  const {
    templates: { FieldTemplate },
  } = useContext(FormRegistry);

  return (
    <FieldTemplate {...props}>
      <p>My awesome field!</p>
    </FieldTemplate>
  );
}

const fields = { myField: MyCustomFieldComponent };

// Specify the fields that should use your custom field component in uiSchema
const uiSchema = {
  firstField = { "ui:field": "myField" },
};

...

return (
  <Formik initialValues={initialValues} onSubmit={handleOnSubmit}>
    <Form schema={schema} uiSchema={uiSchema} fields={fields}></Form>
  </Formik>
);

The style for buttons to add and remove (and move) array or object items can be overridden within the uiSchema by giving inline style.

The buttons that can be customized that way are:

  • "ui:addButtonOptions"
  • "ui:removeButtonOptions"
  • "ui:upButtonOptions"
  • "ui:downButtonOptions"
  • "ui:customButtonOptions"

content takes a ReactNode as input.

const schema = {
  title: "Form with customized buttons",
  type: "object",
  properties: {
    someArray: {
      type: "array",
      title: "Some array",
      description: "Array with customized buttons",
      additionalItems: {
        type: "string",
      },
    },
  }
}

const uiSchema = {
  "ui:description": "Form description",
  // make a green ADD button for someArray that reads "Hello!"
    someArray: {
    "ui:addButtonOptions": {
      content: <b>Hello!</b>,
      props: { style: { background: "green" } },
    },
};

...

return (
  <Formik initialValues={initialValues} onSubmit={handleOnSubmit}>
    <Form
      schema={schema}
      uiSchema={uiSchema}
    ></Form>
  </Formik>
);

Required input

Required input can be displayed with an asterisk next to the title. The respective fieldname needs to be given inside the required-array of the object holding the field in question in the schema. In the example, required fields are 'lastName', 'favouriteFood' inside the arrayOfObject, and 'tasks' in nestedObject.

The styling of the asterisk can be customised by applying style to styledComponents.RequiredIndicator.

import { Form } from "@nolwenture/formik-jsonschema-form";
import { Formik } from "formik";

const CustomizedRequiredIndicator = styled(styledComponents.RequiredIndicator)`
  color: aqua;
`;

const schema = {
  title: "My Form",
  required: ["lastName"],
  properties: {
    firstName: { type: "string", title: "First name" },
    lastName: { type: "string", title: "Last name" },
    arrayOfObject: {
      type: "array",
      title: "Array of objects",
      items: {
        type: "object",
        title: "Favourites",
        required: ["favouriteFood"],
        properties: {
          favouriteFood: { type: "string", title: "Favourite food" },
          favouriteSong: { type: "string", title: "Favourite song" },
        },
      },
    },
    nestedObject: {
      type: "object",
      title: "Nested Object",
      required: ["tasks"],
      properties: {
        hobbies: { type: "string", title: "Hobbies" },
        tasks: { type: "string", title: "Tasks" },
      },
    },
  },
};

const uiSchema = {
  "ui:description": "Form description",
  firstName: { "ui:description": "Field description" },
};

export function MyFormComponent() {
  return (
    <Formik
      initialValues={{ firstName: "", lastName: "" }}
      onSubmit={handleSubmit}
    >
      <Form
        schema={schema}
        uiSchema={uiSchema}
        components={{ RequiredIndicator: CustomizedRequiredIndicator }}
      ></Form>
    </Formik>
  );
}

Enum fields

Enum fields are fields with multiple options to either choose oneOf or anyOf from. There is an alternative field implementation for "anyOf" that when given "ui:field": "SearchMultiTagSelectField" results in a searchable multi tag select field.

const schema = {
  properties: {
    enum: {
      type: "number",
      title: "Enum",
      enum: [1, 2, 3, 4],
    },
    enumOneOf: {
      type: "number",
      title: "Enum oneOf (non-standard)",
      oneOf: [
        { const: 1, title: "one" },
        { const: 2, title: "two" },
        { const: 3, title: "three" },
        { const: 4, title: "four" },
      ],
    },
    enumAnyOf: {
      type: "number",
      title: "Enum anyOf (non-standard)",
      anyOf: [
        { const: 1, title: "one" },
        { const: 2, title: "two" },
        { const: 3, title: "three" },
        { const: 4, title: "four" },
      ],
    },
    enumTagSelectAnyOf: {
      type: "string",
      title: "Enum searchable tag select anyOf",
      anyOf: [
        { const: 1, title: "one" },
        { const: 2, title: "two" },
        { const: 3, title: "three" },
        { const: 4, title: "four" },
      ],
    },
  },
};

const uiSchema = {
  enumTagSelectAnyOf: {
    "ui:field": "SearchMultiTagSelectField",
    "ui:options": {
      placeholder: "Search...",
      emptySearchMessage: "...no matching results found",
    },
  },
};

Alternative fields

There are alternative fields available for certain types. For type: "string" there is DateField, MultipleChoiceField and TextAreaField.

DateField has a "dd.mm.yyyy" date input field and a calendar date picker and will save the date input as "yyyy-mm-dd" string.

const schema = {
  properties: {
    date: { type: "string", title: "Date" },
  },
};

const uiSchema = {
  date: { "ui:field": "DateField" },
};

MultipleChoiceField takes an array of oneOf or anyOf. The choices provided inside an oneOfarray will be presented on the UI as Radio button selectables and only one of the given values can be selected. The choices provided inside an anyOf array will be presented on the UI as Checkbox selectables and multiple values can be selected. MultipleChoiceField comes with a small number of predefined styles, which are inline, oneColumn, twoColumns and threeColumns. When no style is given, the choice options are listed in a row under the title.

const schema = {
  properties: {
    feeling: {
      type: "string",
      title: "How are you feeling today?",
      oneOf: [
        { const: "happy", title: "Happy" },
        { const: "creative", title: "Creative" },
        { const: "tired", title: "Tired" },
        { const: "nope", title: "Please don't even ask..." },
      ],
    },
  },
};

const uiSchema = {
  feeling: {
    "ui:field": "MultipleChoiceField",
    "ui:option": {
      style: "twoColumns",
    },
  },
};

TextAreaField gives more space for a longer string input.

const schema = {
  properties: {
    story: { type: "string", title: "A short story" },
  },
};

const uiSchema = {
  story: { "ui:field": "TextAreaField" },
};

For type: "boolean" there is RadioField and ToggleField. RadioField can take labels for the "true" and "false" options. If left empty, the default labels will be "true" and "false".

const schema = {
  properties: {
    question: { type: "boolean", title: "Are you ready?" },
    mode: { type: "boolean", title: "Toggle mode" },
  },
};

const uiSchema = {
  question: {
    "ui:field": "RadioField",
    "ui:label": { true: "yes", false: "no" },
  },
  mode: { "ui:field": "ToggleField" },
};

The alternative fields are customisable. The customisable elements are:

ToggleSwitchField.styles = {
  ToggleLabelStyle: ToggleLabel,
  ToggleFieldInputStyle: ToggleFieldInput,
  ToggleStyle: Toggle,
};

RadioButtonField.styles = {
  RadioButtonFieldContainerStyle: RadioButtonFieldContainer,
  RadioButtonFieldElementStyle: RadioButtonFieldElement,
  RadioButtonFieldInputStyle: RadioButtonFieldInput,
  ButtonLabelStyle: ButtonLabel,
  ButtonLabelTrueStyle: ButtonLabelTrue,
  ButtonLabelFalseStyle: ButtonLabelFalse,
};

Conditional fields

Conditional fields depending on a parent field input can be given with an if-then-else logic inside the schema making use of allOf. While ifand then are required to make the condition work, else is optional.

export const conditionsSchema = {
  type: "object",
  properties: {
    animal: {
      type: "string",
      oneOf: [
        { const: "cat", title: "Cat" },
        { const: "fish", title: "Fish" },
        { const: "dog", title: "Dog" },
      ],
    },
  },
  allOf: [
    {
      if: {
        properties: {
          animal: {
            const: "cat",
          },
        },
      },
      then: {
        properties: {
          food: {
            type: "string",
            enum: ["meat", "grass", "fish"],
          },
        },
        required: ["food"],
      },
    },
    {
      if: {
        properties: {
          animal: {
            const: "fish",
          },
        },
      },
      then: {
        properties: {
          food: {
            type: "string",
            enum: ["insect", "worms"],
          },
          water: {
            type: "string",
            enum: ["lake", "sea"],
          },
        },
        required: ["food", "water"],
      },
      else: {
        properties: {
          likesWalkiesWhere: {
            type: "string",
            enum: ["park", "forest", "garden"],
          },
        },
      },
    },

    {
      required: ["animal"],
    },
  ],
};

Another option to give conditional fields is using dependencies-object inside the main schema. Dependencies can either be triggered by the conditional-giving property being present in the form values, or by the conditional-giving property having a certain value.

To have value-dependent conditionals, the dependencies-object needs to have an oneOf-array listing the possible conditional-giving values matching the enum-values of the conditional-giving field. Each case in in the oneOf array can either have no conditional fields, then the only key-value pair given inside properties is the matching conditional (see "no type"), or an unlimited amount of conditional fields listed after the matching conditional.

export const refDefSchema: Schema = {
  type: "object",
  properties: {
    creditCard: {
      type: "number",
      title: "Credit card number",
      description:
        "Giving any input to this field will trigger its dependent field 'billing_address'",
    },
    fieldType: {
      type: "string",
      description:
        "Selecting an option in this field will render the correct dependent field from the provided oneOf-array",
      enum: ["string", "object", "array"],
    },
  },
  dependencies: {
    creditCard: {
      properties: {
        billing_address: {
          type: "object",
          description: "This object depends on creditCard field input",
          properties: {
            street: { type: "string", title: "Street" },
            city: { type: "string", title: "City" },
            postalCode: { type: "number", title: "Postal code" },
          },
        },
      },
      required: ["billing_address"],
    },
    fieldType: {
      oneOf: [
        {
          properties: {
            fieldType: {
              enum: ["no type"],
            },
          },
        },
        {
          properties: {
            fieldType: {
              enum: ["string"],
            },
            stringField: {
              type: "string",
              title: "StringField",
            },
          },
        },
        {
          properties: {
            fieldType: {
              enum: ["object"],
            },
            objectField: {
              type: "object",
              properties: {
                objectString: {
                  type: "string",
                  title: "StringField in Object",
                },
              },
            },
          },
        },
        {
          properties: {
            fieldType: {
              enum: ["array"],
            },
            arrayField: {
              type: "array",
              items: [{ type: "string", title: "StringField in fixed array" }],
            },
          },
        },
      ],
    },
  },
};

Using if-else-then or dependencies depends mostly on the use case. For recursion, a combination of $ref and dependenciesis recommended.

Schema reference $ref

Schemas can be referenced. A referenced schema can be given to a property using $ref keyword. The definition for the referenced schema needs to be inside definitions object inside the main schema. The value-string given to $ref-key must always look like this: '#/definitions/{addMatchingKeyHere}'

const ageFieldDefinition: Schema = {
  type: "number",
  title: "Age",
};

export const refDefSchema: Schema = {
  title: "References and Definitions",
  type: "object",
  properties: {
    name: {
      $ref: "#/definitions/name",
    },
    age: {
      $ref: "#/definitions/age",
    },
  },
  definitions: {
    name: {
      type: "string",
      title: "Name",
    },
    age: ageFieldDefinition,
  },
};

Ordering fields

The order of fields can be set using "ui:order": ["fieldName"] inside the uiSchema. If using "ui:order", all fields that should be visible in the form need to be included in the given array.

const schema = {
  properties: {
    secondField: { type: "string" },
    firstField: { type: "string" },
    thirdField: { type: "string" },
  },
};

const uiSchema = {
  "ui:order": ["firstField", "secondField", "thirdField"],
};

Props

Form component props:

<Form
  // Required
  schema={schema} // Form schema
  // Optional
  uiSchema={uiSchema} // Form UI schema
  className={"className"} // Form class name
  fields={fields} // Additional custom field components
  templates={templates} // Additional custom field templates
  components={components} // Override form components (e.g. wrappers) with custom ones
  formContext={formContext} // Form context
  useItemsAsAdditionalItems={true} // Allow "items" to be used as "additionalItems" in array fields that are missing "additionalItems", false by default
/>

Readme

Keywords

Package Sidebar

Install

npm i @nolwenture/formik-jsonschema-form

Weekly Downloads

125

Version

0.10.0

License

MIT

Unpacked Size

363 kB

Total Files

155

Last publish

Collaborators

  • rammohan.balasubramanian.nolwenture
  • antti.vikman
  • it_nolwenture
  • mika.kytojoki