Multi-column support for react-jsonschema-form with antd

Priyank Rupareliya
7 min readMar 25, 2024

--

react-jsonschema-form is meant to automatically generate a React form based on a JSON Schema. If you want to generate a form for any data, sight unseen, simply given a JSON schema, react-jsonschema-form may be for you.

Problem Statement

Although we can create brilliant forms with a lot of validations and dependencies using react-jsonschema-form or rjsf, when it comes to creating a multi-column structure for our forms, the library falls short.

For someone who believes that if a library does 90% of your job with limited effort, you shouldn’t hesitate in building the remaining 10% functionality (That in fact is also the true spirit of Open Source).

Let’s see the problem here with a simple example. Please note, I will be using the antd theme (https://www.npmjs.com/package/@rjsf/antd) in this example. But you can apply this configuration to almost any theme with limited changes.

Consider the following JSON schema input as my form-schema:

{
"title": "A registration form",
"description": "A simple form example.",
"type": "object",
"required": [
"firstName",
"lastName"
],
"properties": {
"firstName": {
"type": "string",
"title": "First name",
"default": "Chuck"
},
"lastName": {
"type": "string",
"title": "Last name"
},
"age": {
"type": "integer",
"title": "Age"
},
"bio": {
"type": "string",
"title": "Bio"
},
"password": {
"type": "string",
"title": "Password",
"minLength": 3
},
"telephone": {
"type": "string",
"title": "Telephone",
"minLength": 10
}
}
}

The output of such a schema in rjsf looks something like this:

All fields take the full-width

Ew, gross.

Solution

Creating an ObjectFieldTemplate.tsx that can be used across multiple forms and supports multi-column configuration.

But before that, what is a Template in context on the react-jsonschema-form library ? Templates are nothing but extensions provided by the lib for customizing certain behaviours of the form. Following is the list of Templates provided by the library: (v. 5.18.1)

export interface TemplatesType<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any> {
/** The template to use while rendering normal or fixed array fields */
ArrayFieldTemplate: ComponentType<ArrayFieldTemplateProps<T, S, F>>;
/** The template to use while rendering the description for an array field */
ArrayFieldDescriptionTemplate: ComponentType<ArrayFieldDescriptionProps<T, S, F>>;
/** The template to use while rendering an item in an array field */
ArrayFieldItemTemplate: ComponentType<ArrayFieldTemplateItemType<T, S, F>>;
/** The template to use while rendering the title for an array field */
ArrayFieldTitleTemplate: ComponentType<ArrayFieldTitleProps<T, S, F>>;t
/** The template to use while rendering the standard html input */
BaseInputTemplate: ComponentType<BaseInputTemplateProps<T, S, F>>;
/** The template to use for rendering the description of a field */
DescriptionFieldTemplate: ComponentType<DescriptionFieldProps<T, S, F>>;
/** The template to use while rendering the errors for the whole form */
ErrorListTemplate: ComponentType<ErrorListProps<T, S, F>>;
/** The template to use while rendering the errors for a single field */
FieldErrorTemplate: ComponentType<FieldErrorProps<T, S, F>>;
/** The template to use while rendering the errors for a single field */
FieldHelpTemplate: ComponentType<FieldHelpProps<T, S, F>>;
/** The template to use while rendering a field */
FieldTemplate: ComponentType<FieldTemplateProps<T, S, F>>;
/** The template to use while rendering an object */
ObjectFieldTemplate: ComponentType<ObjectFieldTemplateProps<T, S, F>>;
/** The template to use for rendering the title of a field */
TitleFieldTemplate: ComponentType<TitleFieldProps<T, S, F>>;
/** The template to use for rendering information about an unsupported field type in the schema */
UnsupportedFieldTemplate: ComponentType<UnsupportedFieldProps<T, S, F>>;
/** The template to use for rendering a field that allows a user to add additional properties */
WrapIfAdditionalTemplate: ComponentType<WrapIfAdditionalTemplateProps<T, S, F>>;
/** The set of templates associated with buttons in the form */
ButtonTemplates: {
/** The template to use for the main `Submit` button */
SubmitButton: ComponentType<SubmitButtonProps<T, S, F>>;
/** The template to use for the Add button used for AdditionalProperties and Array items */
AddButton: ComponentType<IconButtonProps<T, S, F>>;
/** The template to use for the Copy button used for Array items */
CopyButton: ComponentType<IconButtonProps<T, S, F>>;
/** The template to use for the Move Down button used for Array items */
MoveDownButton: ComponentType<IconButtonProps<T, S, F>>;
/** The template to use for the Move Up button used for Array items */
MoveUpButton: ComponentType<IconButtonProps<T, S, F>>;
/** The template to use for the Remove button used for AdditionalProperties and Array items */
RemoveButton: ComponentType<IconButtonProps<T, S, F>>;
};
}

Now, in order to support customizing our column widths, we have to provide our own implementation of ObjectFieldTemplate, which is responsible for rendering an object inside the form.

In your react application, just create a new ObjectFieldTemplate.tsx file and paste the following code:

import classNames from 'classnames';
import isObject from 'lodash/isObject';
import isNumber from 'lodash/isNumber';
import isString from 'lodash/isString';
import {
FormContextType,
GenericObjectType,
ObjectFieldTemplateProps,
ObjectFieldTemplatePropertyType,
RJSFSchema,
StrictRJSFSchema,
UiSchema,
canExpand,
descriptionId,
getTemplate,
getUiOptions,
titleId,
} from '@rjsf/utils';
import Col from 'antd/lib/col';
import Row from 'antd/lib/row';
import { ConfigConsumer, ConfigConsumerProps } from 'antd/lib/config-provider/context';

const DESCRIPTION_COL_STYLE = {
paddingBottom: '8px',
};

/** The `ObjectFieldTemplate` is the template to use to render all the inner properties of an object along with the
* title and description if available. If the object is expandable, then an `AddButton` is also rendered after all
* the properties.
*
* @param props - The `ObjectFieldTemplateProps` for this component
*/
export default function ObjectFieldTemplate<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any
>(props: ObjectFieldTemplateProps<T, S, F>) {
const {
description,
disabled,
formContext,
formData,
idSchema,
onAddClick,
properties,
readonly,
required,
registry,
schema,
title,
uiSchema,
} = props;
console.log('ObjectFieldTemplate props: ')
console.log(props)
const uiOptions = getUiOptions<T, S, F>(uiSchema);
const TitleFieldTemplate = getTemplate<'TitleFieldTemplate', T, S, F>('TitleFieldTemplate', registry, uiOptions);
const DescriptionFieldTemplate = getTemplate<'DescriptionFieldTemplate', T, S, F>(
'DescriptionFieldTemplate',
registry,
uiOptions
);
// Button templates are not overridden in the uiSchema
const {
ButtonTemplates: { AddButton },
} = registry.templates;
const { colSpan = 24, labelAlign = 'right', rowGutter = 24 } = formContext as GenericObjectType;

const findSchema = (element: ObjectFieldTemplatePropertyType): S => element.content.props.schema;

const findSchemaType = (element: ObjectFieldTemplatePropertyType) => findSchema(element).type;

const findUiSchema = (element: ObjectFieldTemplatePropertyType): UiSchema<T, S, F> | undefined =>
element.content.props.uiSchema;

const findUiSchemaField = (element: ObjectFieldTemplatePropertyType) => getUiOptions(findUiSchema(element)).field;

const findUiSchemaWidget = (element: ObjectFieldTemplatePropertyType) => getUiOptions(findUiSchema(element)).widget;

const calculateColSpan = (element: ObjectFieldTemplatePropertyType) => {
const type = findSchemaType(element);
const field = findUiSchemaField(element);
const widget = findUiSchemaWidget(element);

const defaultColSpan =
properties.length < 2 || // Single or no field in object.
type === 'object' ||
type === 'array' ||
widget === 'textarea'
? 24
: 12;

if (isObject(colSpan)) {
const colSpanObj: GenericObjectType = colSpan;
if (isString(widget)) {
return colSpanObj[widget];
}
if (isString(field)) {
return colSpanObj[field];
}
if (isString(type)) {
return colSpanObj[type];
}
}
if (isNumber(colSpan)) {
return colSpan;
}
return defaultColSpan;
};

return (
<ConfigConsumer>
{(configProps: ConfigConsumerProps) => {
console.log('config props are')
console.log(configProps)
const { getPrefixCls } = configProps;
const prefixCls = getPrefixCls('form');
const labelClsBasic = `${prefixCls}-item-label`;
const labelColClassName = classNames(
labelClsBasic,
labelAlign === 'left' && `${labelClsBasic}-left`
// labelCol.className,
);

return (
<fieldset id={idSchema.$id}>
<Row gutter={rowGutter}>
{title && (
<Col className={labelColClassName} span={24}>
<TitleFieldTemplate
id={titleId<T>(idSchema)}
title={title}
required={required}
schema={schema}
uiSchema={uiSchema}
registry={registry}
/>
</Col>
)}
{description && (
<Col span={24} style={DESCRIPTION_COL_STYLE}>
<DescriptionFieldTemplate
id={descriptionId<T>(idSchema)}
description={description}
schema={schema}
uiSchema={uiSchema}
registry={registry}
/>
</Col>
)}
{uiSchema?.['ui:grid'] && Array.isArray(uiSchema['ui:grid']) ?
uiSchema['ui:grid'].map((ui_row) => {
return Object.keys(ui_row).map((row_item) => {
let element = properties.find((p => p.name == row_item))
if (element) {
return <Col key={element.name} span={ui_row[row_item]}>
{element.content}
</Col>
} else {
return <></>
}
})

})
: properties
.filter((e) => !e.hidden)
.map((element: ObjectFieldTemplatePropertyType) => (
<Col key={element.name} span={calculateColSpan(element)}>
{element.content}
</Col>
))}
</Row>

{canExpand(schema, uiSchema, formData) && (
<Col span={24}>
<Row gutter={rowGutter} justify='end'>
<Col flex='192px'>
<AddButton
className='object-property-expand'
disabled={disabled || readonly}
onClick={onAddClick(schema)}
uiSchema={uiSchema}
registry={registry}
/>
</Col>
</Row>
</Col>
)}
</fieldset>
);
}}
</ConfigConsumer>
);
}

In the above code, I’ve only extended the default implementation of ObjectFieldTemplate.tsx provided by rjsf library. Because of this, none of the existing features will be impacted. What I’ve essentially changed, is just added this block in the ObjectFieldTemplate:

{uiSchema?.['ui:grid'] && Array.isArray(uiSchema['ui:grid']) ?
uiSchema['ui:grid'].map((ui_row) => {
return Object.keys(ui_row).map((row_item) => {
let element = properties.find((p => p.name == row_item))
if (element) {
return <Col key={element.name} span={ui_row[row_item]}>
{element.content}
</Col>
} else {
return <></>
}
})

})
: properties
.filter((e) => !e.hidden)
.map((element: ObjectFieldTemplatePropertyType) => (
<Col key={element.name} span={calculateColSpan(element)}>
{element.content}
</Col>
))}

Make sure to install lodash and it’s types:

npm install lodash
npm install @types/lodash --save-dev

Now, apply this ObjectFieldTemplate by passing it in the <Form /> component provided by the react-jsonschema-form library, as a prop.

import Form from "@rjsf/antd";
import { RJSFSchema } from "@rjsf/utils";
import validator from "@rjsf/validator-ajv8";
import { ReactElement } from "react";
import schema from "./FormSchema.json";
import ObjectFieldTemplate from "./ObjectFieldTemplate";

let _RJSFSchema: RJSFSchema = JSON.parse(JSON.stringify(schema));

const BasicForm: () => ReactElement = () => {
return (
<Form
schema={_RJSFSchema}
validator={validator}
templates={{
ObjectFieldTemplate: ObjectFieldTemplate,
}}
/>
);
};

export default BasicForm;

Next, specify the columns configuration in the uiSchema prop within the same form component, as follows:

import Form from "@rjsf/antd";
import { RJSFSchema } from "@rjsf/utils";
import validator from "@rjsf/validator-ajv8";
import { ReactElement } from "react";
import schema from "./FormSchema.json";
import ObjectFieldTemplate from "./ObjectFieldTemplate";

let _RJSFSchema: RJSFSchema = JSON.parse(JSON.stringify(schema));

const BasicForm: () => ReactElement = () => {
return (
<Form
schema={_RJSFSchema}
validator={validator}
templates={{
ObjectFieldTemplate: ObjectFieldTemplate,
}}
uiSchema={{
"ui:grid": [
{ firstName: 12, lastName: 12 },
{ age: 6, bio: 18 },
{password: 12, telephone: 12}
],
}}
/>
);
};

export default BasicForm;

Since antd follows a 24-column grid layout, we’ve set firstName and lastName to have 12 columns taking up a full row, age and bio taking up the next row and the remaining fields in the third row.

One thing to note, that if you pass a “ui:grid property” inside the uiSchema, you will have to specify all the fields explicitly. If you do not specify a field, it will not be rendered by default.

This is how our form will look like after the above setup:

Not bad, is it ?

You can find the complete code provided in this repository: https://github.com/priyank-R/rjsf-multi-col-blog

Priyank Rupareliya is a Senior Software Engineer focused on architecting solutions revolving around Cloud, DevOps, Containerization, Backend and Frontend.

--

--

Priyank Rupareliya
Priyank Rupareliya

Written by Priyank Rupareliya

AWS certified, senior engineer @AT&T. Building reliable, secure and scalable web applications. https://www.linkedin.com/in/priyank-rupareliya-9a562b157/

No responses yet