Skip to main content
This guide will help you create your first custom component for FormEngine Core. We’ll start with a simple alert component and gradually add more features. A custom component in FormEngine Core is meta-information about a React component, which consists of: a React component, the component type, and a description of the component properties. Meta-information on component properties in FormEngine Core is called an annotation. FormEngine Core has the following APIs for describing a component:
  1. define: is the primary method for defining a component.
  2. string: annotation builder for properties of type string.
  3. boolean: annotation builder for properties of type boolean.
  4. number: annotation builder for properties of type number.
  5. size: annotation builder for properties of type ‘CSS unit’.
  6. date: annotation builder for properties of type Date.
  7. time: annotation builder for properties of type Time.
  8. array: annotation builder for properties of type array.
  9. color: annotation builder for properties of type color.
  10. className: annotation builder for properties containing the CSS class name.
  11. event: annotation builder for properties of type event.
  12. node: annotation builder for properties of type ReactNode.
  13. oneOf: annotation builder for properties of type enum, the property value can only be one of enum.
  14. someOf: annotation builder for properties of type enum, the property value can contain multiple enum values.
  15. readOnly: annotation builder for boolean properties that make a component read-only.
  16. fn: annotation builder for function properties.
  17. 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).

Prerequisites

Before starting, ensure you have:
  • A FormEngine Core project set up
  • Basic knowledge of React and TypeScript
  • Understanding of how forms work in FormEngine

Step 1: Setting Up Your Environment

1.1 Create a Component Directory

Create a folder named components/custom inside the src folder. The directory structure in this example will be as follows:
src/
├── components/
│   └── custom/
│       ├── FeMyAlert.test.tsx
│       ├── MyAlert.css
│       ├── MyAlert.test.tsx
│       ├── MyAlert.tsx
│       └── myAlertComponent.ts
├── views/
│   └── customView.ts
├── forms/
│   └── exampleForm.json
└── App.tsx

1.2 Install Required Dependencies

Make sure you have FormEngine Core installed:
npm install @react-form-builder/core

Step 2: Creating Your First Component

Let’s create a simple alert component that displays a message.

2.1 Create the React Component

Create a file src/components/custom/MyAlert.tsx:
MyAlert.tsx

// Step 1: Create a regular React component
interface MyAlertProps {
  /** The message to display */
  message: string;

  /** The type of alert (success, warning, error) */
  type?: 'success' | 'warning' | 'error';

  /** Whether the alert is dismissible */
  dismissible?: boolean;
}

export const MyAlert: React.FC<MyAlertProps> = ({
                                                  message,
                                                  type = 'success',
                                                  dismissible = false
                                                }) => {
  const [isVisible, setIsVisible] = React.useState(true)

  if (!isVisible) return null

  return (
    <div
      className={`alert alert--${type}`}
      role="alert"
    >
      <span className="alert__message">{message}</span>
      {dismissible && (
        <button
          className="alert__close"
          onClick={() => setIsVisible(false)}
          aria-label="Close alert"
        >
          ×
        </button>
      )}
    </div>
  )
}

2.2 Wrap with FormEngine’s define()

Now wrap your component to make it available in FormEngine. Create a file src/components/custom/myAlertComponent.ts:
myAlertComponent.ts

// Step 2: Define the component for FormEngine
export const myAlertComponent = define(MyAlert, 'MyAlert')
  .props({
    // The message prop - required string
    message: string.default('This is an alert'),

    // The type prop - enum with default
    type: oneOf('success', 'warning', 'error').default('success'),

    // The dismissible prop - boolean
    dismissible: boolean.default(false)
  })
  .build()
Of course, you can do everything in one file. In this tutorial, the separation is done for ease of understanding.

Step 3: Adding to Your View

3.1 Create or Update Your View

In src/views/customView.ts:

// Create a custom view that includes Material UI components + custom components
export const customView = muiView

// Adds the myAlertComponent component
// The model property is meta-information about the component for FormEngine Core
customView.define(myAlertComponent.model)

Step 4: Using in a Form

4.1 Create a Form JSON

Create src/forms/exampleForm.json:
{
  "form": {
    "key": "Screen",
    "type": "Screen",
    "children": [
      {
        "key": "welcomeAlert",
        "type": "MyAlert",
        "props": {
          "message": {
            "value": "Welcome to our form! Please fill in all required fields."
          },
          "type": {
            "value": "success"
          },
          "dismissible": {
            "value": true
          }
        }
      },
      {
        "key": "firstName",
        "type": "MuiTextField",
        "props": {
          "label": {
            "value": "First Name"
          }
        }
      },
      {
        "key": "warningAlert",
        "type": "MyAlert",
        "props": {
          "message": {
            "value": "This is a warning alert that cannot be dismissed."
          },
          "type": {
            "value": "warning"
          },
          "dismissible": {
            "value": false
          }
        }
      }
    ]
  }
}

4.2 Render the Form

In your React app:

const App: React.FC = () => {
  return (
    <div className="app">
      <h1>My Custom Component Demo</h1>
      <FormViewer
        view={customView}
        getForm={() => JSON.stringify(formJson)}
      />
    </div>
  )
}

export default App

Step 5: Adding Some Style

Create src/components/custom/MyAlert.css:
.alert {
  padding: 16px;
  margin: 16px 0;
  border-radius: 4px;
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.alert--success {
  background-color: #d4edda;
  border: 1px solid #c3e6cb;
  color: #155724;
}

.alert--warning {
  background-color: #fff3cd;
  border: 1px solid #ffeaa7;
  color: #856404;
}

.alert--error {
  background-color: #f8d7da;
  border: 1px solid #f5c6cb;
  color: #721c24;
}

.alert__message {
  flex: 1;
  margin-right: 16px;
}

.alert__close {
  background: none;
  border: none;
  font-size: 24px;
  cursor: pointer;
  padding: 0;
  width: 32px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.alert__close:hover {
  opacity: 0.7;
}
Update MyAlert.tsx to import the CSS:
After making all the changes, you will see a page that looks something like this: Getting Started 01

Step 6: Testing Your Component

6.1 Create a Test

Create src/components/custom/MyAlert.test.tsx:

describe('MyAlert', () => {
  afterEach(() => {
    cleanup()
  })

  test('renders message correctly', () => {
    const {getByText} = render(<MyAlert message="Test message"/>)
    expect(getByText('Test message')).toBeInTheDocument()
  })

  test('applies correct type class', () => {
    const {container} = render(<MyAlert message="Test" type="error"/>)
    const alert = container.querySelector('.alert')
    expect(alert).toHaveClass('alert--error')
  })

  test('shows close button when dismissible', () => {
    const {getByLabelText} = render(<MyAlert message="Test" dismissible={true}/>)
    expect(getByLabelText('Close alert')).toBeInTheDocument()
  })

  test('hides alert when close button is clicked', async () => {
    const {getByLabelText, queryByText} = render(<MyAlert message="Test" dismissible={true}/>)
    const closeButton = getByLabelText('Close alert')
    fireEvent.click(closeButton)
    await waitFor(() => expect(queryByText('Test')).not.toBeInTheDocument())
  })
})

6.2 Test with FormEngine

Create an integration test in src/components/custom/FeMyAlert.test.tsx:

test('MyAlert works within FormViewer', () => {
  const formJson = {
    form: {
      key: 'Screen',
      type: 'Screen',
      children: [
        {
          key: 'testAlert',
          type: 'MyAlert',
          props: {
            message: {value: 'Test alert message'},
            type: {value: 'success'}
          }
        }
      ]
    },
  }

  const {getByText} = render(
    <FormViewer
      view={customView}
      getForm={() => JSON.stringify(formJson)}
    />
  )

  expect(getByText('Test alert message')).toBeInTheDocument()
})

Live example

live
function App() {
  const MyAlert = ({
                     message,
                     type = 'success',
                     dismissible = false
                   }) => {
    const [isVisible, setIsVisible] = useState(true)

    if (!isVisible) return null

    return (
      <div
        className={`alert alert--${type}`}
        role="alert"
      >
        <span className="alert__message">{message}</span>
        {dismissible && (
          <button
            className="alert__close"
            onClick={() => setIsVisible(false)}
            aria-label="Close alert"
          >
            ×
          </button>
        )}
      </div>
    )
  }

  const myAlertComponent = define(MyAlert, 'MyAlert')
    .props({
      // The message prop - required string
      message: string.default('This is an alert'),

      // The type prop - enum with default
      type: oneOf('success', 'warning', 'error').default('success'),

      // The dismissible prop - boolean
      dismissible: boolean.default(false)
    })
    .build()

  // Create a custom view that includes Material UI components + custom components
  const customView = muiView

  // Adds the myAlertComponent component
  // The model property is meta-information about the component for FormEngine Core
  customView.define(myAlertComponent.model)

  const form = {
    "form": {
      "key": "Screen",
      "type": "Screen",
      "children": [
        {
          "key": "welcomeAlert",
          "type": "MyAlert",
          "props": {
            "message": {
              "value": "Welcome to our form! Please fill in all required fields."
            },
            "type": {
              "value": "success"
            },
            "dismissible": {
              "value": true
            }
          }
        },
        {
          "key": "firstName",
          "type": "MuiTextField",
          "props": {
            "label": {
              "value": "First Name"
            }
          }
        },
        {
          "key": "warningAlert",
          "type": "MyAlert",
          "props": {
            "message": {
              "value": "This is a warning alert that cannot be dismissed."
            },
            "type": {
              "value": "warning"
            },
            "dismissible": {
              "value": false
            }
          }
        }
      ]
    }
  }

  const getForm = useCallback(() => JSON.stringify(form), [form])

  return <FormViewer
    view={customView}
    getForm={getForm}
  />
}

Summary

Congratulations! You’ve created your first custom component. Let’s recap what we learned:
  1. Create a React Component - Write a standard React component with props
  2. Wrap with define() - Use FormEngine’s define() function to register it
  3. Add Properties - Define component properties using type builders (string, boolean, oneOf, etc.)
  4. Add to View - Include your component in a view configuration
  5. Use in JSON - Reference it in form JSON using the type name
  6. Style it - Add CSS for visual appeal
  7. Test it - Write unit and integration tests

Next Steps

Now that you understand the basics, you can explore more advanced topics:

Common Pitfalls

❌ Forgetting to Add to View

// Wrong: Component not in view
customView = createView([
  // MyAlert is missing!
])

// Right: Include all components
customView = createView([
  ...otherComponents,
  myAlertComponent.model,
])

// Or with the existing view
view.define(myAlertComponent.model)

❌ Mismatched Type Names

// Component definition
export const myAlert = define(MyAlert, 'MyAlert'); // Type name is 'MyAlert'
// Form JSON
{
  // Wrong: case-sensitive!
  "type": "Myalert"
}
// Should be:
{
  // Correct: matches exactly
  "type": "MyAlert"
}

Tips for Success

  • Start Simple - Begin with display-only components before adding data binding
  • Test Incrementally - Test each feature as you add it
  • Use Descriptive Names - Clear component and property names help everyone
  • Document Your Components - Add JSDoc comments for properties
  • Follow Patterns - Look at built-in components for best practices
Happy component building! 🎉
Last modified on April 16, 2026