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:
fieldComponentRegistryandnodeComponentRegistryare 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 — backend node processor plugins
- FlowDrop Node Processor — full plugin system documentation
- Supported JSON Schema Properties — schema properties including
format - flowdrop_ui_components — UI module overview