Skip to main content

Custom Components

Fjorm's real power comes from swapping in your own UI library. Register custom display components for each field type, and the builder renders them everywhere — on the canvas, in the preview, and in the final form.

The Adapter Pattern

  1. Define display components wrapping your UI library's primitives
  2. Define editor components (or use declarative editor objects)
  3. Create a FormComponentRegistration[] array
  4. Call config.addComponents(yourArray)
  5. Pass config to <FormBuilder>

Example: Mantine

import { TextInput, Select } from '@mantine/core'
import type { FormComponentRegistration, FormComponentProps } from 'fjorm'

function MantineTextInput({ settings, label }: FormComponentProps) {
return (
<TextInput
label={label}
name={settings.name}
placeholder={String(settings.placeholder ?? '')}
required={settings.required}
/>
)
}

function MantineSelect({ settings, label, options }: FormComponentProps) {
return (
<Select
label={label}
name={settings.name}
required={settings.required}
data={options?.map(o => ({ value: o.value, label: o.title })) ?? []}
/>
)
}

const myComponents: FormComponentRegistration[] = [
{
key: 'TextInput',
icon: FaFont,
settings: { label: 'Text input', name: 'TextInput' },
component: MantineTextInput,
editor: { label: 'EditorInput', placeholder: 'EditorInput', name: 'EditorInput', required: 'EditorCheckbox' },
},
{
key: 'SelectInput',
icon: FaList,
options: [],
settings: { label: 'Select', name: 'Select' },
component: MantineSelect,
editor: { label: 'EditorInput', name: 'EditorInput', required: 'EditorCheckbox', options: 'EditorOptions' },
},
]

Custom Form Wrapper

Use the form prop on FormBuilder or FormDisplay to wrap fields in a UI library's form container:

function FormWrapper({ children, onSubmit, fjormValues }: Record<string, unknown> & {
children?: React.ReactNode
onSubmit?: (data: Record<string, unknown>) => void
fjormValues?: Record<string, unknown>
}) {
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
const formData = new FormData(e.target as HTMLFormElement)
const data: Record<string, unknown> = {}
formData.forEach((value, key) => { data[key] = value })
if (fjormValues) Object.assign(data, fjormValues)
onSubmit?.(data)
}

return <Box component="form" onSubmit={handleSubmit}>{children}</Box>
}

const formConfig: FormConfig = { component: FormWrapper }

<FormBuilder config={config} form={formConfig} />

Display-Only Components

Use providesValue: false for components that are purely decorative or structural — headers, paragraphs, dividers, images. These components are excluded from form submissions.

const myComponents: FormComponentRegistration[] = [
{
key: 'Header',
icon: FaHeading,
settings: { label: 'Header', name: 'Header' },
component: HeaderDisplay,
editor: HeaderEditor,
providesValue: false, // excluded from submitted form data
},
{
key: 'TextInput',
icon: FaFont,
settings: { label: 'Text input', name: 'TextInput' },
component: TextInputDisplay,
editor: { label: 'EditorInput', name: 'EditorInput' },
// providesValue defaults to true — included in form submissions
},
]

The built-in formComponents already has providesValue: false on Header, Paragraph, and Container.

Grid / Layout Containers

Containers let you build multi-column layouts by dragging components into nested drop zones. The built-in Container component uses CSS Grid, but you can swap in any UI framework's grid system.

Framework-specific container examples

// Ant Design — Row/Col
import { Row, Col } from 'antd'
function AntRowContainer({ children, settings }: FormComponentProps) {
const cols = (settings.columns as number) || 2
return (
<Row gutter={[16, 16]}>
{Children.map(children, child => <Col span={24 / cols}>{child}</Col>)}
</Row>
)
}

// Mantine — SimpleGrid
import { SimpleGrid } from '@mantine/core'
function MantineGridContainer({ children, settings }: FormComponentProps) {
return <SimpleGrid cols={(settings.columns as number) || 2}>{children}</SimpleGrid>
}

// MUI — Grid
import { Grid } from '@mui/material'
function MuiGridContainer({ children, settings }: FormComponentProps) {
const cols = (settings.columns as number) || 2
return (
<Grid container spacing={2}>
{Children.map(children, child => <Grid size={12 / cols}>{child}</Grid>)}
</Grid>
)
}

Register the container like any other component — use providesValue: false since containers don't produce form values:

{
key: 'Container',
icon: FaTh,
settings: { label: 'Container', name: 'container', columns: 2 },
component: AntRowContainer, // your framework grid component
editor: { label: 'EditorInput', name: 'EditorInput', columns: 'EditorInput' },
providesValue: false,
}

The children prop contains rendered child form components, automatically passed by the display renderer. The container infrastructure (nested drop zones, tree serialization, recursive rendering) is framework-agnostic.

Example Apps

All examples are consolidated in the playground SPA (demo/). Each demonstrates a full integration with a major UI library, including a custom FormWrapper, multiple field types, and editor definitions.

LibraryPlayground RouteSource
Ant Design v6/#/antddemo/src/examples/antd/
Material UI v9/#/muidemo/src/examples/mui/
Mantine v9/#/mantinedemo/src/examples/mantine/
Custom Builder/#/customdemo/src/custom/

Run all examples from one dev server: cd demo && yarn dev