With npm:
npm install @nolwenture/formik-jsonschema-form
With yarn:
yarn add @nolwenture/formik-jsonschema-form
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
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 Form
component, which should be passed as a child to the Formik
component, 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>
);
}
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>
);
}
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 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 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).
anyOf
enums 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",
},
},
};
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 oneOf
array 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,
};
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 depending on a parent field input can be given with an if-then-else
logic inside the schema making use of allOf
. While if
and 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 dependencies
is recommended.
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,
},
},
},
},
};
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.
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
/>