24
loading...
This website collects cookies to deliver better user experience
$ npx create-react-app my-app --template typescript
$ npm install --save @jsonforms/core
$ npm install --save @jsonforms/react
$ npm install --save @jsonforms/material-renderers
$ npm install --save @material-ui/core
$ npm install --save @material-ui/icons
src/components/PersonData.ts
const Person = {
schema: {
type: "object",
properties: {
people: {
type: "array",
title: "People",
items: {
type: "object",
properties: {
name: {
type: "string",
minLength: 3,
description: "Please enter your name",
},
vegetarian: {
type: "boolean",
},
birthDate: {
type: "string",
format: "date",
},
nationality: {
type: "string",
oneOf: [
{
const: "DE",
title: "German",
},
{
const: "IT",
title: "Italian",
},
{
const: "JP",
title: "Japanese",
},
{
const: "US",
title: "North-American",
},
{
const: "RU",
title: "Russian",
},
],
},
personalData: {
type: "object",
properties: {
age: {
type: "integer",
description: "Please enter your age.",
},
height: {
type: "number",
},
drivingSkill: {
type: "number",
maximum: 10,
minimum: 1,
default: 7,
},
},
required: ["age", "height"],
},
occupation: {
type: "string",
},
postalCode: {
type: "string",
maxLength: 5,
},
items: {
type: "array",
title: "items",
uniqueItems: true,
errorMessage: {
uniqueItems: "Items must be unique",
},
maxItems: 3,
items: {
type: "object",
properties: {
name: {
type: "string",
enum: ["Type 1", "Type 2", "Type 3"],
},
price: {
type: "number",
maximum: 10,
minimum: 1,
default: 1,
},
},
},
},
},
required: ["occupation", "nationality"],
},
},
},
},
uischema: {
type: "VerticalLayout",
elements: [
{
type: "Control",
scope: "#/properties/people",
options: {
detail: {
type: "VerticalLayout",
elements: [
{
type: "Label",
text: "Person Info",
},
{
type: "HorizontalLayout",
elements: [
{
type: "Control",
scope: "#/properties/name",
},
{
type: "Control",
scope: "#/properties/personalData/properties/age",
},
{
type: "Control",
scope: "#/properties/birthDate",
},
],
},
{
type: "Label",
text: "Additional Information",
},
{
type: "HorizontalLayout",
elements: [
{
type: "Control",
scope: "#/properties/personalData/properties/height",
},
{
type: "Control",
scope: "#/properties/nationality",
},
{
type: "Control",
scope: "#/properties/occupation",
suggestion: [
"Accountant",
"Engineer",
"Freelancer",
"Journalism",
"Physician",
"Student",
"Teacher",
"Other",
],
},
],
},
{
type: "Label",
text: "Items",
},
{
type: "Control",
scope: "#/properties/items",
options: {
detail: {
type: "VerticalLayout",
elements: [
{
type: "Label",
text: "Item Info",
},
{
type: "Control",
scope: "#/properties/price",
},
{
type: "Control",
scope: "#/properties/name",
},
],
},
},
},
],
},
},
},
],
},
data: {
people: [
{
name: "Dummy Name",
vegetarian: false,
birthDate: "1987-01-10",
personalData: {
age: 28,
},
postalCode: "87100",
items: [
{
price: 1,
name: "Item 1",
},
],
},
],
},
};
export default Person;
data
, schema
, uischema
. Please take a moment to notice that our data will hold an array of people, each one containing a nested array of items. ArrayLayout.tsx
file under src/ui/Layouts/ArrayLayout.tsx
export const ArrayLayoutRenderer = ({
visible,
enabled,
id,
uischema,
schema,
label,
rootSchema,
renderers,
cells,
data,
path,
errors,
uischemas,
addItem,
removeItems,
}: ArrayLayoutProps) => {
const addItemCb = useCallback(
(p: string, value: any) => {
return addItem(p, value);
},
[addItem]
);
const removeItemsCb = useCallback(
(path: string, toDelete: number[]) => {
return removeItems ? removeItems(path, toDelete) : () => {};
},
[removeItems]
);
const toRender = Array(data)
.fill(0)
.map((_, i) => {
return (
<CardRenderer
key={i}
index={i}
schema={schema}
uischema={uischema}
path={composePaths(path, `${i}`)}
renderers={renderers}
cells={cells}
onRemove={removeItemsCb(path, [i])}
/>
);
});
return (
<div>
<button onClick={addItemCb(path, createDefaultValue(schema))}>Add</button>
{toRender}
<div></div>
</div>
);
};
export default React.memo(
withJsonFormsArrayLayoutProps(ArrayLayoutRenderer),
(prevProps, props) => areEqual(prevProps, props)
);
withJsonFormsArrayLayoutProps
we wrap our custom layout component with JsonForms props like data
, schema
, ui schema
and so on. Speaking of data
, this prop contains the number of items in the array, hence we can use this information to create an array fill it with zeros and iterate over it to create a list of CardRenderer
components. In the end, we are simply rendering an array of items and a button with the handler to add a new empty item to the collection.CardRenderer
component, we need to provide a tester for our ArrayLayoutRenderer
, this gives a priority value to JsonForms and it will be used to choose the correct renderer to use. So, let's add the following to ArrayLayout.tsx
:export const arrayLayoutTester: RankedTester = rankWith(
5,
isObjectArrayWithNesting
);
ArrayLayout.tsx
to render a list of CardRenderer
Items which are defined as follow in the CardRenderer.tsx
file:interface DispatchPropsOfCardRenderer {
onRemove(): () => void;
}
interface CardRendererProps extends LayoutProps, DispatchPropsOfCardRenderer {
index: number;
}
export const CardRenderer = (props: CardRendererProps) => {
const { uischema, schema, path, renderers, cells, onRemove } = props;
const elements = uischema.options?.["detail"]["elements"];
const itemsToRender = elements.map((element: any, index: number) => {
return (
<ResolvedJsonFormsDispatch
schema={schema}
uischema={element}
path={path}
enabled={true}
renderers={renderers}
cells={cells}
key={index}
/>
);
});
return (
<Card>
{itemsToRender}
<button onClick={onRemove}>Remove</button>
</Card>
);
};
const withContextToCardRenderd =
(
Component: ComponentType<CardRendererProps>
): ComponentType<CardRendererProps> =>
({ ctx, props }: JsonFormsStateContext & CardRendererProps) => {
return <Component {...props}/>;
};
const withCustomProps = (Component: ComponentType<CardRendererProps>) => {
return withJsonFormsContext(
withContextToCardRenderd(
React.memo(Component, (prevProps, props) => areEqual(prevProps, props))
)
);
};
export default withCustomProps(CardRenderer);
onRemove
handler that can be used to remove items from the array.schema
and uischema
delegating the actual rendering to ResolvedJsonFormsDispatch
. Before doing this, I wrap the component with a Card
component which simply provides some styling to it (I won't discuss this as this is out of the scope of this post), and I add the Remove
button which fires the onRemove
handler.src/components/FormContainer/FormContainer.tsx
import {
materialCells,
materialRenderers,
} from "@jsonforms/material-renderers";
import { JsonForms } from "@jsonforms/react";
import { useState } from "react";
import Person from "../PersonData";
import ArrayLayout, {
arrayLayoutTester,
} from "../UI/Layouts/PeopleControl/ArrayLayout";
import classes from "./FormContainer.module.css";
const renderers = [
...materialRenderers,
//register custom renderers
{ tester: arrayLayoutTester, renderer: ArrayLayout },
];
const FormContainer = () => {
const [data, setData] = useState(Person.data);
const value = JSON.stringify(data, null, 2);
return (
<div className={classes.Container}>
<div className={classes.Box}>
<pre style={{ textAlign: "left" }}>{value}</pre>
</div>
<div className={classes.Side}>
<JsonForms
schema={Person.schema}
uischema={Person.uischema}
data={data}
renderers={renderers}
cells={materialCells}
onChange={({ data }) => setData(data)}
/>
</div>
</div>
);
};
export default FormContainer;
FormContainer
component will display the form panel side-by-side with a preview of the actual-data gathered: