> ## Documentation Index
> Fetch the complete documentation index at: https://flowdrop.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom form fields

> Extend FlowDrop's form system with custom field components.

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 theme={null}
<!-- ColorPickerField.svelte -->
<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:**

```typescript theme={null}
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 theme={null}
{
  "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 theme={null}
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 theme={null}
// 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 theme={null}
// 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 theme={null}
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 theme={null}
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 theme={null}
{
  "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 theme={null}
<!-- DependentAutocomplete.svelte -->
<script lang="ts">
  import { getContext } from 'svelte';
  import { FormAutocomplete } from '@flowdrop/flowdrop/form/autocomplete';
  import {
    FORM_VALUES_KEY,
    type FormValuesGetter,
    type FieldSchema
  } from '@flowdrop/flowdrop/form';
  import type { AutocompleteConfig } from '@flowdrop/flowdrop/form';

  interface Props {
    id: string;
    value: unknown;
    schema: FieldSchema;
    autocomplete: AutocompleteConfig;
    placeholder?: string;
    required?: boolean;
    disabled?: boolean;
    ariaDescribedBy?: string;
    onChange: (value: unknown) => void;
  }

  let { schema, autocomplete, ...rest }: Props = $props();

  const getFormValues = getContext<FormValuesGetter | undefined>(FORM_VALUES_KEY);

  const patchedAutocomplete = $derived.by(() => {
    const values = getFormValues?.() ?? {};
    const deps = (schema as any).dependencies as Record<string, string> | undefined;
    if (!deps) return autocomplete;

    const extra = Object.entries(deps)
      .filter(([, field]) => values[field] != null && values[field] !== '')
      .map(
        ([param, field]) =>
          `${encodeURIComponent(param)}=${encodeURIComponent(String(values[field]))}`
      )
      .join('&');

    const sep = autocomplete.url.includes('?') ? '&' : '?';
    return { ...autocomplete, url: extra ? `${autocomplete.url}${sep}${extra}` : autocomplete.url };
  });
</script>

<FormAutocomplete autocomplete={patchedAutocomplete} {...rest} />
```

**3. Register it** — match on the custom `dependencies` property:

```typescript theme={null}
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`.

<Note>
  **`FormAutocomplete` is a named export.**

  Import it with `import { FormAutocomplete } from '@flowdrop/flowdrop/form/autocomplete'`.
</Note>
