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
- Define display components wrapping your UI library's primitives
- Define editor components (or use declarative editor objects)
- Create a
FormComponentRegistration[]array - Call
config.addComponents(yourArray) - Pass
configto<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.
| Library | Playground Route | Source |
|---|---|---|
| Ant Design v6 | /#/antd | demo/src/examples/antd/ |
| Material UI v9 | /#/mui | demo/src/examples/mui/ |
| Mantine v9 | /#/mantine | demo/src/examples/mantine/ |
| Custom Builder | /#/custom | demo/src/custom/ |
Run all examples from one dev server: cd demo && yarn dev