Custom Components
Overview
The Shesha Form Builder is a powerful feature that includes a variety of form components to cover a wide range of common scenarios. However, given the diversity of use cases, it's not feasible to address all of them. Therefore, Shesha provides an option to develop custom components for any unique requirements. These custom components can be seamlessly integrated withing the Form Builder, enabling you to easily add them to any form through the drag-and-drop interface.
Background
Before diving into the code, we need to briefly mention a few topic areas.
-
The form is assembled using a JSON schema. Once this JSON data is available it is injected into the Form Builder and interpreted to render specific components with exact configurations.
{
"components": [
{
"id": "LAuoz8VcEzPdMTc5zFK-n",
"type": "rate",
"propertyName": "ratings",
"componentName": "ratings",
"label": "Ratings",
"labelAlign": "right",
"parentId": "root",
"hidden": false,
"isDynamic": false,
"version": 1,
"count": 8,
"settingsValidationErrors": []
}
],
"formSettings": {
"layout": "horizontal",
"colon": true,
"labelCol": {
"span": 6
},
"wrapperCol": {
"span": 18
}
}
}
NOTE: It is important to note that in the JSON schema in the above image, two properties are depicted (
components
,formSettings
). Also take note that the components property is anarray
and if we had more than one component in the UI form it would be shown in this array.
Folder Structure
-
Looking at the file structure, we have gone with the monorepo approach. NPM allows for workspaces to be used in a single project. The use of workspaces allows us to share dependencies (node_modules) between projects/modules in a single application.
-
The recommended naming conventions is to name the root workspace directory as
packages
and create all relevant modules in thispackages
directory.Above image shows the
packages
folder in the root of theadminportal
directory -
Inside the
packages
directory we can see that only one module exists, namely thehis
module. -
The
hisApplicationPlugin.tsx
file inside of thesrc/providers
folder is where components are exposed.HisApplicationPlugin
will need to be wrapped around the main application's provider.
import React, { FC, PropsWithChildren, useEffect } from 'react';
import { useSheshaApplication } from '@shesha-io/reactjs';
import { allComponents } from '@/designer';
export interface HisApplicationPluginProps {}
export const HIS_PLUGIN_NAME = 'His-Plugnin';
export const HisApplicationPlugin: FC<PropsWithChildren<HisApplicationPluginProps>> = ({ children }) => {
const { registerFormDesignerComponents } = useSheshaApplication();
useEffect(() => {
registerFormDesignerComponents(HIS_PLUGIN_NAME, allComponents);
}, []);
return <>{children}</>;
};
-
By opening the
allComponents
file as shown in the image below, we can see the list of exposed components to the Shesha Form Builder. -
An array is the data structure used for
allComponents
, this allows us to group components and create multiple component modules if needed. -
The array is typed with a IToolboxComponentGroup interface which enforces that we follow the correct pattern. It is recommended that allComponents be typed as shown in the image below
import { IToolboxComponentGroup } from '@shesha-io/reactjs';
import CalendarComponent from 'components/global/bookingCalendar/formComponent';
import FacilityContextPickerComponent from 'components/global/facilityContextPicker/formComponent';
import StatsListComponent from 'components/global/statsList/formComponent';
import TableFormComponent from 'components/global/tableBuilder/formComponent';
import UrinalysisComponent from 'components/global/urinalysis';
import SampleComponent from 'components/global/urinalysis';
export const allComponents: IToolboxComponentGroup[] = [
{
name: 'HIS Components',
components: [
CalendarComponent,
FacilityContextPickerComponent,
StatsListComponent,
TableFormComponent,
UrinalysisComponent,
SampleComponent,
],
visible: true,
},
];
The square bracket after the interface means it is an array of the IToolboxComponentGroup interface.
Implementation
-
A simple example was constructed for demo purposes
import { DingtalkOutlined } from '@ant-design/icons';
import {
ComponentFactoryArguements,
ConfigurableFormItem,
IToolboxComponent,
validateConfigurableComponentSettings,
} from '@shesha-io/reactjs';
import React from 'react';
import { settingsForm } from './settings';
const SampleComponent: IToolboxComponent<ISampleComponentProps> = {
type: 'sampleComponent',
name: 'Sample Component',
icon: <DingtalkOutlined />,
Factory: ({ model }: ComponentFactoryArguements<ISampleComponentProps>) => {
const style = {
height: model.height,
width: model.width,
border: model.hasBorder ? '1px solid black' : 'none'
};
const onMouseMove = (event: MouseEvent, onChange: Function) => {
onChange({ x: event.movementx, y: event.movementy });
};
return (
<ConfigurableFormItem model={model}>
{(value, onChange) => (
<div style={style} onMouseMove={(e: any) => onMouseMove(e, onChange)}>
<div>{model.title}</div>
<span>X: {value?.x}</span>
<span>Y: {value?.y}</span>
</div>
)}
</ConfigurableFormItem>
);
},
initModel: (model) => ({
...model,
hasBorder: true,
}),
settingsFormMarkup: settingsForm,
validateSettings: (model) => validateConfigurableComponentSettings(settingsForm, model),
};
export default SampleComponent;
SampleComponent
is an object which implements the IToolboxComponent interface, this is to ensure consistency.
Type
- A key which is used to find specific components, it is essential that this is unique
Name
- Property name is displayed on the components toolbox and is often the default value of the label once dragged to the form
Icon
-
Icon property refers to the selected icon on the toolbox
Form Configuration
-
Form configuration are the configurations that render the side menu or metadata of the component
import { DesignerToolbarSettings } from '@shesha-io/reactjs';
import { nanoid } from 'nanoid';
export const settingsForm = new DesignerToolbarSettings()
.addSectionSeparator({
id: nanoid(),
propertyName: 'separatorl',
parentld: 'root',
label: 'Display',
})
.addContextPropertyAutocomplete({
id: nanoid(),
propertyName: 'propertyName',
parentld: 'root',
label: 'Property name',
validate: {
required: true,
},
})
.addTextField({
id: nanoid(),
propertyName: 'title',
parentld: 'root',
label: 'Title',
})
.addCheckbox({
id: nanoid(),
propertyName: 'hideLabel',
parentld: 'root',
label: 'Hide Label',
})
.addNumberField({
id: nanoid(),
propertyName: 'height',
parentld: 'root',
description: 'This property determines the height of the selector in question.',
label: "Selector's Height",
validate: {
required: true,
},
})
.addNumberField({
id: nanoid(),
propertyName: 'width',
parentld: 'root',
description: 'This property determines the width of the selector in question.',
label: "Selector's Width",
validate: {
required: true,
},
})
.addCheckbox({
id: nanoid(),
propertyName: 'hasBorder',
parentld: 'root',
label: 'Has Border',
})
.tolson();
- DesignerToolbarSettings is a class that assists to create the configuration. Simply append the relevant method to the class to build the configuration and pass the relevant options to the method. Import the configuration settings and inject it to the builder.
shown on lines 10, 40, 41
Factory
-
The
factory
property is arguably the most important property implemented from theIToolboxComponent
interface. In essence, the property returns a JSX element and is what will be rendered in the forms. -
Property
factory
is actually a method that returns a JSX element/component, the first and only parameter is an object of type ComponentFactoryArguments. -
We will be focusing on the model property that is inside the
ComponentFactoryArguments
interface and neglect the rest of the properties. -
From our parameter we destructure the model property. Property of model is passed to the ConfigurableFormItem component
shown on line 25
NOTE: It is important to note that
ConfigurableFormItem
is a form item and is responsible for handling state, validation, visibility and many more features on the Shesha Form Builder.
Model
-
The model property is the values which were set in component form configuration
-
Once this is set the values are available on the model property
shown on line 17
-
It is important to note the type of the model, the model is an object of type ISampleComponentProps. This is passed as a generic type on the ComponentFactoryArguements interface.
import { IConfigurableFormComponent } from '@shesha-io/reactjs';
export interface ISampleComponentProps extends IConfigurableFormComponent {
title: string;
height: string;
width: string;
hasBorder: boolean;
}
Rending of the factory
property includes the ConfigurableFormItem
component as the top parent, this is not mandatory only but done as preference. The children of ConfigurableFormItem
return a function of parameters value
and onChange
. value is the current value of the active component, onChange
is the event which will trigger the value to change
shown on line 26
The function which is the child of ConfigurableFormItem
is required to return the component which will be rendered on the form builder. Direct values can be passed down or muted depending on the specification, in the example used the values from model were directly used in the components
shown on lines 28, 29, 30
See working example below
Init Model
-
Initial values can be set using the initModel property. These values will be initialized on the form configuration
shown on line 36
Exposing Component
-
Navigate to the
app-provider.tsx
file from the root directory of theadminportal
directory.src
>app
>app-provider.tsx
-
The
HisApplicationPlugin
is wrapped around the main application's provider as seen in the illustration below. Simply import the hook and wrap it around theShaApplicationProvider
.
"use client";
import React, { FC, PropsWithChildren } from "react";
import {
GlobalStateProvider,
ShaApplicationProvider,
StoredFilesProvider,
useNextRouter,
} from "@shesha-io/reactjs";
import { HisApplicationPlugin } from "@shesha-io/pd-publicportal";
import { AppProgressBar } from "next-nprogress-bar";
import { useTheme } from "antd-style";
export interface IAppProviderProps {
backendUrl: string;
}
export const AppProvider: FC<PropsWithChildren<IAppProviderProps>> = ({
children,
backendUrl,
}) => {
const nextRouter = useNextRouter();
const theme = useTheme();
return (
<GlobalStateProvider>
<AppProgressBar height="4px" color={theme.colorPrimary} shallowRouting />
<ShaApplicationProvider
backendUrl={backendUrl}
router={nextRouter}
noAuth={nextRouter.path?.includes("/no-auth")}
>
<HisApplicationPlugin>
<StoredFilesProvider baseUrl={backendUrl} ownerId={""} ownerType={""}>
{children}
</StoredFilesProvider>
</HisApplicationPlugin>
</ShaApplicationProvider>
</GlobalStateProvider>
);
};