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

0.24.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 create-functions for initialValues and validationSchema

This library offers create-functions that, based on the JSON schema provided, will return a basic object of either initialValues or yup validationSchema.

import { Form } from "@nolwenture/formik-jsonschema-form";
import {
  createInitialValues,
  createValidationSchema,
} from "@nolwenture/formik-jsonschema-form/dist/utils";
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={createInitialValues(schema)}
      validationSchema={createValidationSchema(schema)}
      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.

A searchable single select field is also available for oneOf and enum. To use it, add the following to the field's uiSchema: "ui:field": "SearchableSelectField"

The type of the field holding the enum is defined by its return value. oneOf enums will only return a single value and therefore, the type of the returned value is used as the field type (in the following example, "number" is the correct field type).

anyOfenums will return the value(s) inside an array, even if only one option was selected. Therefore, the correct field type for the field holding the anyOf-enum is "array".

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: "array",
      title: "Enum anyOf (non-standard)",
      anyOf: [
        { const: 1, title: "one" },
        { const: 2, title: "two" },
        { const: 3, title: "three" },
        { const: 4, title: "four" },
      ],
    },
    enumTagSelectAnyOf: {
      type: "array",
      title: "Enum searchable tag select anyOf",
      anyOf: [
        { const: 1, title: "one" },
        { const: 2, title: "two" },
        { const: 3, title: "three" },
        { const: 4, title: "four" },
      ],
    },
    enumSearchableOneOf: {
      type: "string",
      title: "Enum searchable select oneOf",
      oneOf: [
        { const: "one", title: "One" },
        { const: "two", title: "Two" },
        { const: "three", title: "Three" },
        { const: "four", title: "Four" },
      ],
    },
  },
};

const uiSchema = {
  enumTagSelectAnyOf: {
    "ui:field": "SearchMultiTagSelectField",
    "ui:options": {
      placeholder: "Search...",
      emptySearchMessage: "...no matching results found",
    },
  },
  enumSearchableOneOf: {
    "ui:field": "SearchableSelectField",
    "ui:options": {
      placeholder: "Search...",
      clearButtonText: "Clear",
      expandText: "More results",
      emptySearchMessage: "...no matching results found",
    },
  },
};

Alternative fields

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

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" },
};

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" },
};

MultipleChoiceField takes an array of oneOf or anyOf. If you are creating a schema using oneOf, the type of the field is the value returned by the chosen enum option (e.g. "string" or "number"). If you are creating a schema using anyOf, the type of the field is "array", since the returned value will be in an array format, even if only one option is selected.

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..." },
      ],
    },
    food: {
      type: "array",
      title: "What would you like to eat today? (style: oneColumn)",
      anyOf: [
        { const: "breakfast", title: "Breakfast" },
        { const: "secondBreakfast", title: "What about second breakfast?" },
        { const: "elevenses", title: "Elevenses" },
        { const: "luncheon", title: "Luncheon" },
        { const: "afternoonTea", title: "Afternoon tea" },
        { const: "dinner", title: "Dinner" },
        { const: "supper", title: "Supper" },
      ],
    },
  },
};

const uiSchema = {
  feeling: {
    "ui:field": "MultipleChoiceField",
    "ui:options": {
      style: "twoColumns",
    },
  },
  food: {
    "ui:field": "MultipleChoiceField",
    "ui:options": {
      style: "oneColumn",
    },
  },
};

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,
};

Collapsible object field

Fields of type: "object" can be made collapsible via the respective uiSchema setting.

const collapsibleSchema: Schema = {
  title: "FORM WITH COLLAPSIBLE OBJECT",
  properties: {
    colObject: {
      type: "object",
      title: "Collapsible Object",
      properties: { summer: { type: "string" }, winter: { type: "string" } },
    },
  },
};

const collapsibleUiSchema: UiSchema = {
  colObject: { props: { isCollapsible: true } },
};

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.

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.

const dependenciesSchema: 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}'

In order to give uiSchema to reused or recursive fields defined in schema.definitons, the uiSchema needs to be given to the matching prop of definitions-object. This way, the given uiProps are added to the referencing field, no matter how deeply nested it is inside the schema.

The schema of the field and the referenced schema are merged, meaning that aspects of the reused schema can be adapted:

const schema: Schema = {
  type: "object",
  properties: {
    address: {
      $ref: "#/definitions/address",
      required: ["postcode"],
      default: { country: "US" },
      properties: {
        apartmentNumber: { type: "string", title: "Apartment number" },
      },
    },
  },
  definitions: {
    address: {
      type: "object",
      title: "Address",
      required: ["country", "city", "street"],
      properties: {
        country: { type: "string", title: "Country" },
        city: { type: "string", title: "City" },
        street: { type: "string", title: "Street" },
        postcode: { type: "string", title: "Postcode" },
      },
    },
  },
};

The resulting schema of the address field:

{
  type: "object",
  title: "Address",
  required: ["country", "city", "street", "postcode"],
  default: { country: "US" },
  properties: {
    country: { type: "string", title: "Country" },
    city: { type: "string", title: "City" },
    street: { type: "string", title: "Street" },
    postcode: { type: "string", title: "Postcode" },
    apartmentNumber: { type: "string", title: "Apartment number" },
  },
}

If aspects of the uiSchema need to be adapted, uiSchema.definitions of that field can be overridden inside uiSchema by giving the desired properties to the respective field key. In the following schema/uiSchema petBirthDate can be used as an example. The settings are overriding the title of the field from the original "When is your birthday?" to "When is the birthday of your pet?".

const familyObjectField: Schema = {
  type: "object",
  title: "My family member",
  properties: {
    relationship: { type: "string", title: "Relationship status" },
    names: { $ref: "#/definitions/names" },
  },
};

const refDefSchema: Schema = {
  title: "References and Definitions",
  type: "object",
  properties: {
    names: {
      $ref: "#/definitions/names",
    },
    birthDate: {
      $ref: "#/definitions/birthDate",
    },
    family: {
      type: "object",
      title: "Family",
      properties: {
        mother: {
          type: "array",
          title: "Maternal family",
          additionalItems: familyObjectField,
        },
        father: {
          type: "array",
          title: "Paternal family",
          additionalItems: familyObjectField,
        },
      },
    },
    pet: { type: "string", title: "Pet name" },
    petBirthDate: { $ref: "#/definitions/birthDate" },
  },

  definitions: {
    names: {
      type: "object",
      title: "Name",
      description:
        "The schema for this object comes from the 'definitions' object inside the main schema",
      properties: {
        firstName: { type: "string", title: "First name" },
        middleNames: {
          type: "array",
          title: "Middle names",
          additionalItems: { $ref: "#/definitions/middleName" },
        },
        lastName: { type: "string", title: "Last name" },
      },
    },
    birthDate: { type: "string", title: "When is your birthday?" },
    middleName: { type: "string", title: " Middle name" },
  },
};

const refDefUiSchema: UiSchema = {
  petBirthDate: { "ui:title": "When is the birthday of your pet?" },
  definitions: {
    birthDate: { "ui:field": "DateField" },
    names: {
      history: { "ui:field": "TextAreaField" },
      middleNames: { "ui:info": { content: "You can leave those blank" } },
    },
    middleName: {
      props: {
        title: {
          hidden: true,
        },
      },
    },
  },
};

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. Fields excluded will not show on the form, in the following they will be referred to as "hidden fields".

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

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

Fields excluded will not show on the form. So in the following example "thirdField" will be a hidden field.

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

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

Using "*"-wildcard inside the array will add all property fields that were not specifically added to the order-array.

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

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

In this example, the resulting order would be ["secondField", "firstField", "thirdField"].

Wildcard "*" and hidden fields cannot be combined.

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
  hideSubmit={true} // hides submit-button of the form
/>

Readme

Keywords

Package Sidebar

Install

npm i @nolwenture/formik-jsonschema-form

Weekly Downloads

43

Version

0.24.0

License

MIT

Unpacked Size

491 kB

Total Files

183

Last publish

Collaborators

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