Custom components
Overview
Custom components in Form Builder are regular React components that are described using the Form Builder API. Describing a component using the API makes it clear to the Form Builder designer which properties of the component can be edited and with which property editors.
To add a custom component to the Form Builder tool, you must do the following:
- Have a custom component (obviously).
- Describe your custom component using the Form Builder API.
- Connect your component to the Form Builder designer.
Component description consists of defining meta-information about the component - component name, component type, component properties. Meta-information on component properties in Form Builder is called an annotation.
Form Builder has the following APIs for describing a component:
- define - is the primary method for defining a component.
- string - annotation builder for properties of type
string
. - boolean - annotation builder for properties of type
boolean
. - number - annotation builder for properties of type
number
. - size - annotation builder for properties of type 'CSS unit'.
- date - annotation builder for properties of type
Date
. - time - annotation builder for properties of type
Time
. - array - annotation builder for properties of type
array
. - color - annotation builder for properties of type
color
. - className - annotation builder for properties containing the CSS class name.
- event - annotation builder for properties of type
event
. - node - annotation builder for properties of type
ReactNode
. - oneOf - annotation builder for properties of type
enum
, the property value can only be one of enum. - someOf - annotation builder for properties of type
enum
, the property value can contain multiple enum values. - readOnly - annotation builder for
boolean
properties that make a component read-only. - There are other APIs for describing component properties, you can find them in the documentation, these APIs deal with describing synthetic properties of a component (e.g. a set of arbitrary HTML attributes).
Example of a custom component definition
Well, let's describe some existing component from the popular MUI library. As an example, let's take a Button:
import {Button} from '@mui/material'
import {boolean, define, event, oneOf, string} from '@react-form-builder/core'
// Let's call our component matButton, using the prefix 'mat' to make it easy to understand
// from the name that the component belongs to the MUI library.
//
// Here we call the define function and pass it two parameters - the Button component
// and the unique name of the component type.
export const matButton = define(Button, 'MatButton')
// component name displayed in the component panel in the designer
.name('Button')
// define the component properties that we want to edit in the designer
.props({
// button text
children: string.named('Caption').default('Button'),
// button color
color: oneOf('inherit', 'primary', 'secondary', 'success', 'error', 'info', 'warning'),
// button disable flag
disabled: boolean,
// callback fired when the button is clicked.
onClick: event,
})
Define a custom component from own component library
The definition from your own component is completely similar to the example. You can use any React component.
import {define, event, string} from '@react-form-builder/core'
const MyButton = ({children, disabled, ...props}: ComponentProps<any>) => {
return <button {...props} disabled={disabled}>{children}</button>
}
export const myButton = define(MyButton, 'MyButton')
.name('My Button')
.category('Custom')
.props({
// button text
children: string.named('Caption').default('Button'),
// callback fired when the button is clicked.
onClick: event,
})
Example of an application with a custom component
Let's create a React application from scratch and connect this button to the application.
We will use Create React App to create the application. Open a shell and run the following commands:
Creating an application that displays Form Builder
npx create-react-app my-app --template typescript
cd my-app
Then install the Form Builder dependencies:
npm install @react-form-builder/core @react-form-builder/designer
- @react-form-builder/core - this package is needed to define components and display them.
- @react-form-builder/designer - this package is the actual Form Builder designer.
Then open the file src/index.tsx
and copy and paste the following contents into it (we have removed the styles imported from
the index.css
file):
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import reportWebVitals from './reportWebVitals'
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
)
root.render(
<React.StrictMode>
<App/>
</React.StrictMode>
)
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals()
Now open the file src/App.tsx
and copy and paste the following contents there:
import React from 'react'
import {BuilderView, FormBuilder} from '@react-form-builder/designer'
const builderView = new BuilderView([])
function App() {
return <FormBuilder view={builderView}/>
}
export default App
Open a shell and run the application using the following command:
npm run start
Once the application is compiled, the Form Builder should appear on the web page:
Now let's break down what we did.
- Imported BuilderView,
FormBuilder from
@react-form-builder/designer
package. - Created a
builderView
variable of typeBuilderView
. We passed an empty array as a parameter to theBuilderView
constructor - this is an empty array with the component's metadata. - In the React component
App
, we rendered theFormBuilder
component with the component metadata from thebuilderView
variable.
Adding a custom component
Open the shell and stop the application (Ctrl+C). Then add the @mui/material
library with the following command:
npm install @mui/material
Create a file src/MatButton.tsx
with the following contents (similar to the one discussed earlier in this article):
import {Button} from '@mui/material'
import {boolean, define, oneOf, string, event} from '@react-form-builder/core'
// Let's call our component matButton, using the prefix 'mat' to make it easy to understand
// from the name that the component belongs to the MUI library.
//
// Here we call the define function and pass it two parameters - the Button component
// and the unique name of the component type.
export const matButton = define(Button, 'MatButton')
// component name displayed in the component panel in the designer
.name('Button')
// define the component properties that we want to edit in the designer
.props({
// button text
children: string.named('Caption').default('Button'),
// button color
color: oneOf('inherit', 'primary', 'secondary', 'success', 'error', 'info', 'warning'),
// button disable flag
disabled: boolean,
// callback fired when the button is clicked.
onClick: event,
})
And now we only need to connect our custom component to BuilderView
. To do this, we need to import our custom component into the
src/App.tsx
file and add it to the array in the BuilderView
constructor parameter. Take a look at the modified src/App.tsx
file:
import React from 'react'
import {BuilderView, FormBuilder} from '@react-form-builder/designer'
import {matButton} from './MatButton'
const builderView = new BuilderView([matButton.build()])
function App() {
return <FormBuilder view={builderView}/>
}
export default App
We use the matButton.build()
method because the matButton
variable is the component's metadata builder. After calling
the build method, we will
get an instance of BuilderComponent, which contains the metadata
needed at design time and at runtime.
Now it's time to check how our component looks in the designer. Launch the application using the command:
npm run start
You will see in your browser that the component has appeared in the list of components on the left:
Drag the component onto the form from the panel on the left, or add it using the "+" button. In the right pane, you can see that the component properties we defined are available for editing:
Now you can work with a custom component in the same way as inbuilt components, based on React Suite. By changing the properties of the component we can see how the component changes on the form:
Form with custom component
After creating a form in the designer, you can copy (or download) the form code and pass the form code to the FormViewer, a component for displaying forms. Here is an example of a form with a custom component:
{
"version": "1",
"form": {
"key": "Screen",
"type": "Screen",
"props": {},
"children": [
{
"key": "MatButton 1",
"type": "MatButton",
"props": {
"children": {
"value": "Hello World!"
},
"color": {
"value": "warning"
}
},
"events": {
"onClick": [
{
"name": "log",
"type": "common"
}
]
}
}
]
},
"localization": {},
"languages": [
{
"code": "en",
"dialect": "US",
"name": "English",
"description": "American English",
"bidi": "ltr"
}
],
"defaultLanguage": "en-US"
}
Let's display this form in the FormViewer
component. Copy and paste the following code into the src/App.tsx
file:
import {createView, FormViewer} from '@react-form-builder/core'
import {BuilderView, FormBuilder} from '@react-form-builder/designer'
import React from 'react'
import {matButton} from './MatButton'
const builderView = new BuilderView([matButton.build()])
const view = createView([matButton.build().model])
const form = {
'version': '1',
'form': {
'key': 'Screen',
'type': 'Screen',
'props': {},
'children': [
{
'key': 'MatButton 1',
'type': 'MatButton',
'props': {
'children': {
'value': 'Hello World!'
},
'color': {
'value': 'warning'
}
},
'events': {
'onClick': [
{
'name': 'log',
'type': 'common'
}
]
}
}
]
},
'localization': {},
'languages': [
{
'code': 'en',
'dialect': 'US',
'name': 'English',
'description': 'American English',
'bidi': 'ltr'
}
],
'defaultLanguage': 'en-US'
}
function App() {
// return <FormBuilder view={builderView}/>
return <FormViewer view={view} getForm={() => JSON.stringify(form)}/>
}
export default App
Let's take a look at what has changed in the application:
- Imported createView, FormViewer from @react-form-builder/core package.
- Created a set of metadata for
FormViewer
and saved it to a view variable. TheFormViewer
component does not need all the metadata of the component, what is in themodel
field is enough. The createView function creates a View object that contains information about the components required to display the form. - Added a form and saved it to the
form
variable. - Finally, instead of the FormBuilder component, we rendered the FormViewer component, passing
view
and thegetForm
function to it. ThegetForm
function should return a string containing the JSON of the form.
Opening the application in a browser you can see a simple form with a custom component. When you click on the component, the event is logged to the console.
Custom component containing data
Okay, but what if we need a component that contains data? For that, we must also define a component, but we must also apply
the valued method to one of the component's properties. Once we do
this, Form Builder will realise that this property of the component contains its value. The component must also have an onChange
property,
which must take the value of the component as a parameter.
To summarise, you need to:
- Define one of the component properties as valued.
- Define a
onChange
functional property in the component, such as this:onChange?: (value: any) => void
Let's add the Input component to the designer. Note that this component already has a onChange property, and its signature doesn't fit Form Builder:
function onChange(event: React.ChangeEvent): void
Well, we can just make a wrapper component and define everything we need in it. Create a file src/MatInput.tsx
and add the following
content to it:
import {Input, InputProps} from '@mui/material'
import {define, string} from '@react-form-builder/core'
type MatInputProps = InputProps & {
onChange?: (value: any) => void
}
const MatInput = (props: MatInputProps) => {
const {onChange, ...otherProps} = props
return <Input {...otherProps} onChange={event => {
onChange?.(event.target.value)
}}/>
}
export const matInput = define(MatInput, 'MatInput')
.name('Input')
.props({
value: string.valued.default('')
})
From the code above, you can see that:
- We defined the
MatInputProps
type, which extendsInputProps
with the requiredonChange
method. - We defined a
MatInput
component where we set the required behaviour for theonChange
property. - We described the component by adding the
value
property as a string containing a value and having the default value of an empty string (this is to prevent React from writing errors about uncontrolled value to the console).
Now you can add this component to src/App.tsx
. For simplicity only the code for the designer is shown:
import {BuilderView, FormBuilder} from '@react-form-builder/designer'
import React from 'react'
import {matButton} from './MatButton'
import {matInput} from './MatInput'
const builderView = new BuilderView([matButton.build(), matInput.build()])
function App() {
return <FormBuilder view={builderView}/>
}
export default App
By launching the application and adding Input to the form, you can enter data into the Input component and observe how the data on the form changes:
Kind of a component
Also, in some cases, you may need to set the kind
of the component. This
is a property that shows how to render the component. For regular components, it is set to 'component'
by
default. You can the kind
value to 'container'
in case you need the component to be only a container, for its children, without having
its own wrappers.
const myContainer = define(MyContainer)
.type('MyContainer')
.name('Container')
.kind('container')
.props({children: node})
.css({
flexDirection: flexDirection.default('column'),
gap: gap.default('10px')
})
Component icon
If you want to change the component icon that is displayed next to the component name, it is quite easy to do so. Just call
the icon method when selecting a component and pass React ComponentType
to
it, which will draw the icon.
Illustrating example:
const MyIcon = () => {
return <span style={
{width: 16, height: 16, backgroundColor: 'rgba(150,130,20,0.5)', borderRadius: 8}
}/>
}
const MyButton = (props: ComponentProps<any>) => {
return <button>{props.children}</button>
}
const myButton = define(MyButton, 'MyButton')
.name('My Button')
.category('Custom')
.icon(MyIcon)
This simple example shows how to connect an icon to a component. On lines 1-5, the React component MyIcon
that displays the icon is
defined - in our case, it's just a circle. On line 14, the MyIcon
component is connected as an icon for the MyButton
component.
Once the component is connected to the designer, the component icon will be displayed next to the text:
Conclusion
This article showed how to easily connect a custom component to use it in the designer. And also how a custom component is connected and displayed on a form.