--- title: "Custom form fields" description: "Extend FlowDrop's form system with custom field components." source: https://flowdrop.io/docs/editor/custom-form-fields site: FlowDrop documentation --- # 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 **1. Write a Svelte field component:** ```svelte onChange(e.currentTarget.value)} /> ``` **2. Register it:** ```typescript import { getInstance } from '@flowdrop/flowdrop/editor'; import ColorPickerField from './ColorPickerField.svelte'; const fd = getInstance(); // or app.instance outside the component tree fd.fields.register('color-picker', { component: ColorPickerField, matcher: (schema) => schema.format === 'color', priority: 100 }); ``` **3. Use it in a config schema:** ```json { "accentColor": { "type": "string", "format": "color", "title": "Accent Color", "default": "#3b82f6" } } ``` ## How it works When `FormFieldLight` renders a field, it: 1. Calls `resolveFieldComponent(schema)` to check the registry 2. If a registered matcher returns `true`, renders the registered component 3. 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 Your component receives these props: ```typescript interface Props { id: string; value: unknown; placeholder?: string; required?: boolean; ariaDescribedBy?: string; onChange: (value: unknown) => void; } ``` Only the props listed above are guaranteed. Read any additional schema properties (like `schema.minDate`) directly from the schema via the form context — see [Reading sibling field values](#reading-sibling-field-values) below. ## Matcher functions A matcher decides whether your component handles a given schema: ```typescript // 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 When multiple registrations match, the highest priority wins: ```typescript // Priority 50 — general fallback fd.fields.register('text-basic', { component: BasicTextField, matcher: (schema) => schema.type === 'string', priority: 50 }); // Priority 100 — more specific, checked first fd.fields.register('rich-text', { component: RichTextField, matcher: (schema) => schema.type === 'string' && schema.format === 'rich-text', priority: 100 }); ``` You can use this to **override built-in fields** by registering your own component with a higher priority. ## Lazy registration For heavy dependencies, use dynamic imports: ```typescript import type { FieldComponentRegistry } from '@flowdrop/flowdrop/form'; export function registerMyHeavyField(fields: FieldComponentRegistry, priority = 100): void { if (fields.has('my-heavy-field')) return; import('./MyHeavyField.svelte').then((module) => { fields.register('my-heavy-field', { component: module.default, matcher: (schema) => schema.format === 'heavy', priority }); }); } // Call with the instance's registry: registerMyHeavyField(getInstance().fields) ``` ## 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): Each installer takes the target field registry (`fd.fields`) as its first argument: | Schema | Import path | Registration function | | ------------------------------------ | ---------------------------------- | ---------------------------------------- | | `format: "json"` or `format: "code"` | `@flowdrop/flowdrop/form/code` | `registerCodeEditorField(fd.fields)` | | `format: "template"` | `@flowdrop/flowdrop/form/code` | `registerTemplateEditorField(fd.fields)` | | `format: "markdown"` | `@flowdrop/flowdrop/form/markdown` | `registerMarkdownEditorField(fd.fields)` | ## Field management Field management is done through the instance's `fd.fields` registry (a `FieldComponentRegistry`): ```typescript import { getInstance } from '@flowdrop/flowdrop/editor'; const fd = getInstance(); // or app.instance outside the component tree fd.fields.unregister('color-picker'); // returns boolean fd.fields.getKeys(); // ["color-picker", ...] fd.fields.has('color-picker'); // true or false fd.fields.size; // number of registrations fd.fields.clear(); // clear all (useful in tests) ``` ## Reading sibling field values Custom components registered for `format: "autocomplete"` fields receive the full `schema` object as a prop and can read the current values of other fields in the same form using the `FORM_VALUES_KEY` context. This is the building block for dependent autocomplete fields — for example a `project` field whose suggestions depend on the currently selected `account`. **1. Define the schema** — use any custom property to declare the dependency: ```json { "account": { "type": "string", "title": "Account" }, "project": { "type": "string", "format": "autocomplete", "title": "Project", "autocomplete": { "url": "/api/projects", "labelField": "name", "valueField": "id" }, "dependencies": { "account": "account" } } } ``` **2. Write the component** — wrap `FormAutocomplete` and patch the URL: ```svelte ``` **3. Register it** — match on the custom `dependencies` property: ```typescript import { getInstance } from '@flowdrop/flowdrop/editor'; import DependentAutocomplete from './DependentAutocomplete.svelte'; const fd = getInstance(); // or app.instance outside the component tree fd.fields.register('dependent-autocomplete', { component: DependentAutocomplete, matcher: (schema) => schema.format === 'autocomplete' && 'dependencies' in schema, priority: 150 }); ``` The registered component is only activated when a schema has both `format: "autocomplete"` and a `dependencies` property. All other autocomplete fields continue to use FlowDrop's built-in `FormAutocomplete`. **`FormAutocomplete` is a named export.** Import it with `import { FormAutocomplete } from '@flowdrop/flowdrop/form/autocomplete'`.