Custom Form Fields
FlowDrop generates configuration forms automatically from JSON Schema. The field registry system lets you add custom field components — for example a color picker, date picker, or rich text editor.
Quick Start
Section titled “Quick Start”1. Write a Svelte field component:
<script lang="ts"> interface Props { id: string; value: unknown; onChange: (value: unknown) => void; }
let { id, value, onChange }: Props = $props();</script>
<input {id} type="color" value={String(value ?? '#000000')} oninput={(e) => onChange(e.currentTarget.value)}/>2. Register it:
import { registerFieldComponent } from '@flowdrop/flowdrop/form';import ColorPickerField from './ColorPickerField.svelte';
registerFieldComponent( 'color-picker', ColorPickerField, (schema) => schema.format === 'color', 100);3. Use it in a config schema:
{ "accentColor": { "type": "string", "format": "color", "title": "Accent Color", "default": "#3b82f6" }}How It Works
Section titled “How It Works”When FormFieldLight renders a field, it:
- Calls
resolveFieldComponent(schema)to check the registry - If a registered matcher returns
true, renders the registered component - Otherwise falls back to built-in fields (text, number, toggle, select, etc.)
Registrations are priority-ordered — higher priority matchers are checked first.
Field Component Props
Section titled “Field Component Props”Your component receives these props:
interface Props { id: string; value: unknown; placeholder?: string; required?: boolean; ariaDescribedBy?: string; onChange: (value: unknown) => void;}Additional schema properties (like minDate, maxDate, etc.) are forwarded to your component as props.
Matcher Functions
Section titled “Matcher Functions”A matcher decides whether your component handles a given schema:
// Match by format(schema) => schema.format === "color"
// Match by type + format(schema) => schema.type === "string" && schema.format === "rich-text"
// Match by custom property(schema) => schema.widget === "my-widget"Priority-Based Resolution
Section titled “Priority-Based Resolution”When multiple registrations match, the highest priority wins:
// Priority 50 — general fallbackregisterFieldComponent('text-basic', BasicTextField, (schema) => schema.type === 'string', 50);
// Priority 100 — more specific, checked firstregisterFieldComponent( 'rich-text', RichTextField, (schema) => schema.type === 'string' && schema.format === 'rich-text', 100);You can use this to override built-in fields by registering your own component with a higher priority.
Lazy Registration
Section titled “Lazy Registration”For heavy dependencies, use dynamic imports:
let registered = false;
export function registerMyHeavyField(priority = 100): void { if (registered) return;
import('./MyHeavyField.svelte').then((module) => { registerFieldComponent( 'my-heavy-field', module.default, (schema) => schema.format === 'heavy', priority ); registered = true; });}Built-in Field Types
Section titled “Built-in Field Types”These fields are always available without registration:
| Schema | Renders as |
|---|---|
type: "string" | Text input |
type: "string", format: "multiline" | Textarea |
type: "number" or type: "integer" | Number input |
type: "number", format: "range" | Range slider |
type: "boolean" | Toggle switch |
type: "string", enum: [...] | Select dropdown |
type: "string", enum: [...], multiple: true | Checkbox group |
type: "string", oneOf: [{const, title}] | Select with labeled options |
type: "array", items: {...} | Dynamic list |
format: "hidden" | Hidden (not rendered) |
These require explicit registration (heavy dependencies):
| Schema | Import path | Registration function |
|---|---|---|
format: "json" or format: "code" | @flowdrop/flowdrop/form/code | registerCodeEditorField() |
format: "template" | @flowdrop/flowdrop/form/code | registerTemplateEditorField() |
format: "markdown" | @flowdrop/flowdrop/form/markdown | registerMarkdownEditorField() |
Field Management
Section titled “Field Management”import { unregisterFieldComponent, getRegisteredFieldTypes, isFieldTypeRegistered, clearFieldRegistry, getFieldRegistrySize} from '@flowdrop/flowdrop/form';
unregisterFieldComponent('color-picker');getRegisteredFieldTypes(); // ["color-picker", ...]isFieldTypeRegistered('color-picker'); // true or falsegetFieldRegistrySize(); // number of registrationsclearFieldRegistry(); // clear all (useful in tests)