# Custom Editor Components

> Register custom field types and node components in the FlowDrop workflow editor from an external Drupal module.

## Overview

FlowDrop exposes two runtime registries that external modules can use to extend the workflow editor without modifying core code:

| Registry | Purpose | Access |
|----------|---------|--------|
| `fieldComponentRegistry` | Custom field renderers for node configuration forms | `window.FlowDrop.fieldComponentRegistry` |
| `nodeComponentRegistry` | Custom node types on the editor canvas | `await window.FlowDrop.getEditorModule()` |

Both registries are reactive — the editor UI updates automatically when new components are registered.

## Custom Field Types

Use the field component registry to add a custom renderer for configuration fields. When a node's parameter schema matches your `matcher` function, the editor renders your Svelte component instead of the built-in one.

### How Field Resolution Works

When rendering a configuration form, FlowDrop iterates registered field components in **priority order** (highest first). The first matcher that returns `true` wins. If no custom matcher matches, a built-in field is used based on the schema's `type` and `format`.

### Registration API

```javascript
// fieldComponentRegistry is available directly on window.FlowDrop
const { fieldComponentRegistry } = window.FlowDrop;

fieldComponentRegistry.register("my-color-picker", {
  matcher: (schema) => schema.format === "color",
  component: MyColorPickerComponent, // Svelte 5 component
  priority: 100
});
```

#### Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `type` | `string` | Unique identifier for this field type |
| `registration.matcher` | `(schema: FieldSchema) => boolean` | Returns `true` if this component should handle the given schema |
| `registration.component` | `Component` | Svelte component to render |
| `registration.priority` | `number` | Higher values are checked first (built-in fields use low priorities) |

#### FieldSchema Properties Available to Matchers

Your matcher function receives the JSON Schema for the field. Common properties to match on:

| Property | Example | Description |
|----------|---------|-------------|
| `type` | `"string"`, `"number"` | JSON Schema data type |
| `format` | `"color"`, `"date"`, `"uri"` | Format hint (primary extension point) |
| `enum` | `["a", "b"]` | Enumerated values |
| `oneOf` | `[{const, title}]` | Labeled options |
| `multiple` | `true` | Multi-select flag |

### Field Component Props

Your Svelte component receives these props:

```typescript
interface FieldComponentProps {
  id: string;              // Field identifier
  value: unknown;          // Current field value
  placeholder?: string;    // Placeholder text
  required?: boolean;      // Whether field is required
  ariaDescribedBy?: string; // ARIA description ID
  onChange: (value: unknown) => void; // Call this when value changes
}
```

### Example: Color Picker Field

**Backend — define the schema format in your node processor:**

```php
public function getParameterSchema(): array {
  return [
    "type" => "object",
    "properties" => [
      "background_color" => [
        "type" => "string",
        "description" => "Background color",
        "default" => "#ffffff",
        "format" => "color",
      ],
    ],
  ];
}
```

**Frontend — register the field component:**

```javascript
// my_module/js/color-field.js
(function () {
  document.addEventListener("flowdrop:editor:init", async function () {
    const { fieldComponentRegistry } = window.FlowDrop;

    // Dynamically import your Svelte component (built separately)
    const { default: ColorPicker } = await import("./ColorPicker.svelte.js");

    fieldComponentRegistry.register("color-picker", {
      matcher: (schema) => schema.format === "color",
      component: ColorPicker,
      priority: 100
    });
  });
})();
```

### Unregistering

```javascript
fieldComponentRegistry.unregister("color-picker"); // Returns true if removed
```

### Built-in Field Matchers

These matchers are already registered at low priority. Your custom matchers with higher priority will take precedence:

| Matcher | Matches |
|---------|---------|
| `hiddenFieldMatcher` | `format: "hidden"` |
| `checkboxGroupMatcher` | `enum` + `multiple: true` |
| `enumSelectMatcher` | `enum` fields |
| `selectOptionsMatcher` | `oneOf` with `const`/`title` pattern |
| `textareaMatcher` | `format: "multiline"` |
| `rangeMatcher` | `format: "range"` |
| `textFieldMatcher` | `type: "string"` |
| `numberFieldMatcher` | `type: "number"` or `"integer"` |
| `toggleMatcher` | `type: "boolean"` |
| `arrayMatcher` | `type: "array"` |
| `autocompleteMatcher` | `format: "autocomplete"` |

## Custom Node Types

Use the plugin system to register custom node components that appear on the editor canvas. These are the visual node representations — the backend processing is handled by `FlowDropNodeProcessor` plugins (see [Defining a Node](quick-reference/define-node.md)).

### Registration API

The plugin system is on the **editor module**, which is lazy-loaded:

```javascript
document.addEventListener("flowdrop:editor:init", async function () {
  const editorModule = await window.FlowDrop.getEditorModule();

  editorModule.registerFlowDropPlugin({
    namespace: "mymodule",
    name: "My Custom Nodes",
    version: "1.0.0",
    nodes: [
      {
        type: "fancy-display",
        displayName: "Fancy Display",
        component: FancyDisplayNode,
        icon: "mdi:sparkles",
        category: "custom"
      }
    ]
  });
});
```

Node types are automatically namespaced — the example above registers `mymodule:fancy-display`.

### Plugin Builder (Fluent API)

```javascript
const editorModule = await window.FlowDrop.getEditorModule();

editorModule.createPlugin("mymodule", "My Custom Nodes")
  .version("1.0.0")
  .node("fancy-display", "Fancy Display", FancyDisplayNode, {
    icon: "mdi:sparkles",
    category: "custom"
  })
  .node("status-card", "Status Card", StatusCardNode, {
    icon: "mdi:card-text",
    category: "visual"
  })
  .register();
```

### Single Node Registration

For registering a single node without a plugin wrapper:

```javascript
const editorModule = await window.FlowDrop.getEditorModule();

editorModule.registerCustomNode(
  "mymodule:special",
  "Special Node",
  SpecialNodeComponent,
  {
    icon: "mdi:star",
    description: "A special-purpose node",
    category: "custom"
  }
);
```

### Plugin Configuration Reference

```typescript
interface FlowDropPluginConfig {
  namespace: string;           // Lowercase alphanumeric + hyphens
  name: string;                // Display name
  version?: string;            // Semver string
  description?: string;        // What the plugin provides
  nodes: PluginNodeDefinition[];
}

interface PluginNodeDefinition {
  type: string;                // Prefixed with namespace automatically
  displayName: string;         // Shown in UI
  description?: string;
  component: Component;        // Svelte component
  icon?: string;               // Iconify format (e.g., "mdi:star")
  category?: "visual" | "functional" | "layout" | "custom";
  statusPosition?: "top-left" | "top-right" | "bottom-left" | "bottom-right";
  statusSize?: "sm" | "md" | "lg";
}
```

### Plugin Management

```javascript
const editorModule = await window.FlowDrop.getEditorModule();

// List registered plugins
editorModule.getRegisteredPlugins();         // ["mymodule", "other-plugin"]

// Count nodes in a plugin
editorModule.getPluginNodeCount("mymodule"); // 2

// Unregister all nodes from a plugin
editorModule.unregisterFlowDropPlugin("mymodule"); // ["mymodule:fancy-display", ...]

// Validate a namespace
editorModule.isValidNamespace("my-plugin");  // true
editorModule.isValidNamespace("My Plugin!"); // false
```

## Drupal Integration

### Module Setup

To register custom components from an external Drupal module:

**1. Define a JS library** in your module's `*.libraries.yml`:

```yaml
# my_module.libraries.yml
custom_editor_components:
  js:
    js/custom-editor-components.js: { attributes: { type: module } }
  dependencies:
    - flowdrop_ui_components/flowdrop
    - flowdrop_ui_components/editor
```

**2. Attach the library** where the editor is rendered, or use a `hook_page_attachments` to load it on editor pages.

**3. Register components** in your JS file:

```javascript
// my_module/js/custom-editor-components.js
(function () {
  "use strict";

  document.addEventListener("flowdrop:editor:init", async function (event) {
    const { app, workflow } = event.detail;

    // Register custom field types
    const { fieldComponentRegistry } = window.FlowDrop;
    fieldComponentRegistry.register("my-field", {
      matcher: (schema) => schema.format === "my-custom-format",
      component: MyFieldComponent,
      priority: 100
    });

    // Register custom node types
    const editorModule = await window.FlowDrop.getEditorModule();
    editorModule.registerFlowDropPlugin({
      namespace: "my-module",
      name: "My Module",
      nodes: [
        {
          type: "custom-node",
          displayName: "Custom Node",
          component: MyNodeComponent,
          icon: "mdi:puzzle"
        }
      ]
    });
  });
})();
```

### Timing

The `flowdrop:editor:init` event fires after the editor is mounted and `initializeAllFieldTypes()` has completed. This is the recommended point for registering custom components.

If you need to register field types **before** the editor mounts (e.g., for standalone `SchemaForm` usage), you can register immediately after the FlowDrop library loads:

```javascript
// For standalone SchemaForm (no editor)
if (window.FlowDrop) {
  window.FlowDrop.fieldComponentRegistry.register("my-field", { ... });
}
```

### Events Reference

| Event | Detail | Fires When |
|-------|--------|------------|
| `flowdrop:editor:init` | `{ editorId, app, workflow }` | Editor mounted successfully |
| `flowdrop:editor:save` | `{ editorId, success, result\|error }` | Save completes or fails |

## Important Notes

- **Svelte required**: Custom components must be Svelte 5 components. If your module doesn't use Svelte, you'll need to either build Svelte components separately or wrap vanilla JS/Web Components using Svelte's interop.
- **Singleton registries**: `fieldComponentRegistry` and `nodeComponentRegistry` are singletons. Registrations persist for the page lifetime.
- **Priority conflicts**: If two matchers at the same priority both match, the last one registered wins. Use distinctive priority values to avoid ambiguity.
- **Namespace validation**: Plugin namespaces must be lowercase alphanumeric with optional hyphens (validated by `isValidNamespace()`).

## References

- [Defining a Node](quick-reference/define-node.md) — backend node processor plugins
- [FlowDrop Node Processor](flowdrop-node-processor.md) — full plugin system documentation
- [Supported JSON Schema Properties](flowdrop-node-processor.md#supported-json-schema-properties) — schema properties including `format`
- [flowdrop_ui_components](../modules/flowdrop_ui_components.md) — UI module overview
