I'm building a form with React JSON Schema Form (RJSF) that contains arrays of items. I need to add a custom feature: a button next to each array item that, when clicked, displays an input field where users can enter a note/memo for that specific item.
For example, in my Authorization schema (which renders as an array of text areas for JWT tokens), I want each token field to have an associated "Add Note" button. When clicked, it should show an input field where users can add a memo about that specific token.
Here's my current code:
import { RJSFSchema, UiSchema } from '@rjsf/utils';
import { Form as RjsfForm } from '@rjsf/antd';
import validator from '@rjsf/validator-ajv8';
import { App, Button, Tabs } from 'antd';
import { JSX, useEffect, useState } from 'react';
export interface AuthSchema {
id: string;
title?: string;
schema: RJSFSchema;
uiSchema?: UiSchema;
}
export interface RegisterExtractorOptions {
authSchema?: AuthSchema[];
}
// Auth schema definition
const auth = {
authSchema: [
{
id: 'authorization',
title: 'Authorization',
schema: {
type: 'array',
items: {
type: 'string',
pattern: '^[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+$',
},
},
uiSchema: {
items: {
'ui:widget': 'textarea',
},
},
},
{
id: 'account',
title: 'Account',
schema: {
type: 'array',
items: {
type: 'object',
properties: {
username: {
type: 'string',
title: 'Username',
minLength: 5,
pattern: '^[a-z0-9]+$',
},
password: {
type: 'string',
title: 'Password',
minLength: 8,
},
},
required: ['username', 'password'],
},
},
uiSchema: {
items: {
password: {
'ui:widget': 'password',
},
},
},
},
]
};
interface AccountTabProps {
source: SourceInfoClient;
}
function AccountTab({ source }: AccountTabProps): JSX.Element {
return (
<Tabs
tabPosition={'right'}
items={source.authSchema!.map((auth) => {
return {
key: auth.id,
label: auth.title,
children: <TabItem source={source} auth={auth} />,
};
})}
/>
);
}
type TabItemProps = Pick<AccountTabProps, 'source'> & {
auth: AuthSchema;
};
function TabItem({source, auth}: TabItemProps): JSX.Element{
const [formData, setFormData] = useState();
const handleSubmit = async (id: string, event: IChangeEvent): Promise<void> => {
// save data to db
};
useEffect(() => {
// Retrieve data from the database and set it in formData.
}, [auth.id, source.id]);
return (
<RjsfForm
formData={formData}
focusOnFirstError={true}
liveValidate={true}
schema={auth.schema}
uiSchema={auth.uiSchema}
validator={validator}
onSubmit={(e) => handleSubmit(auth.id, e)}
showErrorList={false}
/>
);
}
export default AccountTab;
I'm not sure how to:
- Add a custom "Add Note" button for each array item
- Display an input field when the button is clicked
- Save the notes along with the form data
- Associate each note with its specific array item
I've looked into custom field templates and widgets in RJSF, but I'm not sure how to implement this specific functionality. Should I modify the schema to include a note field, or is there a way to add custom UI elements outside the schema definition?
I'm building a form with React JSON Schema Form (RJSF) that contains arrays of items. I need to add a custom feature: a button next to each array item that, when clicked, displays an input field where users can enter a note/memo for that specific item.
For example, in my Authorization schema (which renders as an array of text areas for JWT tokens), I want each token field to have an associated "Add Note" button. When clicked, it should show an input field where users can add a memo about that specific token.
Here's my current code:
import { RJSFSchema, UiSchema } from '@rjsf/utils';
import { Form as RjsfForm } from '@rjsf/antd';
import validator from '@rjsf/validator-ajv8';
import { App, Button, Tabs } from 'antd';
import { JSX, useEffect, useState } from 'react';
export interface AuthSchema {
id: string;
title?: string;
schema: RJSFSchema;
uiSchema?: UiSchema;
}
export interface RegisterExtractorOptions {
authSchema?: AuthSchema[];
}
// Auth schema definition
const auth = {
authSchema: [
{
id: 'authorization',
title: 'Authorization',
schema: {
type: 'array',
items: {
type: 'string',
pattern: '^[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+$',
},
},
uiSchema: {
items: {
'ui:widget': 'textarea',
},
},
},
{
id: 'account',
title: 'Account',
schema: {
type: 'array',
items: {
type: 'object',
properties: {
username: {
type: 'string',
title: 'Username',
minLength: 5,
pattern: '^[a-z0-9]+$',
},
password: {
type: 'string',
title: 'Password',
minLength: 8,
},
},
required: ['username', 'password'],
},
},
uiSchema: {
items: {
password: {
'ui:widget': 'password',
},
},
},
},
]
};
interface AccountTabProps {
source: SourceInfoClient;
}
function AccountTab({ source }: AccountTabProps): JSX.Element {
return (
<Tabs
tabPosition={'right'}
items={source.authSchema!.map((auth) => {
return {
key: auth.id,
label: auth.title,
children: <TabItem source={source} auth={auth} />,
};
})}
/>
);
}
type TabItemProps = Pick<AccountTabProps, 'source'> & {
auth: AuthSchema;
};
function TabItem({source, auth}: TabItemProps): JSX.Element{
const [formData, setFormData] = useState();
const handleSubmit = async (id: string, event: IChangeEvent): Promise<void> => {
// save data to db
};
useEffect(() => {
// Retrieve data from the database and set it in formData.
}, [auth.id, source.id]);
return (
<RjsfForm
formData={formData}
focusOnFirstError={true}
liveValidate={true}
schema={auth.schema}
uiSchema={auth.uiSchema}
validator={validator}
onSubmit={(e) => handleSubmit(auth.id, e)}
showErrorList={false}
/>
);
}
export default AccountTab;
I'm not sure how to:
- Add a custom "Add Note" button for each array item
- Display an input field when the button is clicked
- Save the notes along with the form data
- Associate each note with its specific array item
I've looked into custom field templates and widgets in RJSF, but I'm not sure how to implement this specific functionality. Should I modify the schema to include a note field, or is there a way to add custom UI elements outside the schema definition?
Share Improve this question asked Mar 23 at 6:48 VQH DEVVQH DEV 332 silver badges9 bronze badges2 Answers
Reset to default 0Instead of an array of single fields, use array of objects. For example, you can use the below schema, where each object has 3 properties.
{
type: "array",
items: {
type: "object",
properties: {
jwt_token: {
type: "string",
pattern: "^[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+$",
},
note: {
type: "string",
},
show_note: {
type: "boolean",
}
}
},
}
With the show_note
boolean field, you can control whether to show the note
field or not. And you can use the custom templates to show Add Note button instead of a radio field for the show_note
field. The note data will be available in the formData.
Here is the custom template code:
function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
const inputRef = useRef(null);
return (
<div>
{props.title}
{props.description}
{props.properties.map((element) => {
if (element.name == 'show_note') {
return (
<div className='property-wrapper'>
<Button type="primary" onClick={() => element.content.props!.onChange(! element.content.props!.formData)}>
{element.content.props!.formData
? 'Remove Note'
: 'Add Note'
}
</Button>
</div>
)
} else if(element.name == 'note') {
let showNote = props.properties.find((element) => {
return element.name === 'show_note';
});
if (showNote && showNote.content.props!.formData) {
return <div className='property-wrapper'>{element.content}</div>
} else {
return <></>;
}
} else {
return <div className='property-wrapper'>{element.content}</div>
}
})}
</div>
);
}
Pass this custom template to the form component:
...
<Form
...
templates={{ObjectFieldTemplate}}
/>
...
Check this working CodeSandbox Demo.
And here is how it looks:
OK, this is a customer array template for RJSF that adds a button next to each item. This allows users to attach a note. Notes toggle on and off per item and are stored in line with the form data. This should integrate cleanly with an array of objects.
Here is the code:
import { ArrayFieldTemplateProps } from '@rjsf/utils'
import { Button, Input } from 'antd'
import { useState } from 'react'
const CustomArrayFieldTemplate = (props: ArrayFieldTemplateProps) => {
const [showNotes, setShowNotes] = useState<boolean[]>(
props.items.map(() => false)
)
const toggleNote = (index: number) => {
const next = [...showNotes]
next[index] = !next[index]
setShowNotes(next)
}
return (
<div>
{props.items.map((item, index) => (
<div key={item.key} style={{ marginBottom: 16 }}>
{item.children}
<Button
type="link"
onClick={() => toggleNote(index)}
style={{ paddingLeft: 0 }}
>
{showNotes[index] ? 'Hide Note' : 'Add Note'}
</Button>
{showNotes[index] && (
<Input.TextArea
value={props.formData?.[index]?.note || ''}
onChange={(e) => {
const updated = [...props.formData]
updated[index].note = e.target.value
props.onChange(updated)
}}
rows={2}
placeholder="Enter a note for this token"
/>
)}
</div>
))}
{props.canAdd && (
<Button type="dashed" onClick={props.onAddClick}>
Add Token
</Button>
)}
</div>
)
}
export default CustomArrayFieldTemplate
Schema
I fot to mention you will need to modify your schema. Instead of an array of strings, make each item an object with token and note.
const auth = {
authSchema: [
{
id: 'authorization',
title: 'Authorization',
schema: {
type: 'array',
items: {
type: 'object',
properties: {
token: {
type: 'string',
title: 'JWT Token',
pattern: '^[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+$',
},
note: {
type: 'string',
title: 'Note',
}
},
required: ['token'],
},
},
uiSchema: {
items: {
token: {
'ui:widget': 'textarea',
},
note: {
'ui:widget': 'hidden', // Hide initially, toggle via button
}
},
},
},
// Account schema unchanged...
]
};