Skip to content

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

// 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:

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:

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

Frontend — register the field component:

// 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

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).

Registration API

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

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)

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:

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

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

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:

# 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:

// 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:

// 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