- Update documentation to reflect new widget filename `lora-manager-widgets.js` - Remove `LoraManagerDemoNode` import and registration from `__init__.py` - Translate development guide from Chinese to English for broader accessibility - Clean up obsolete demo references to align with actual widget implementation
11 KiB
DOMWidget Development Guide
This document provides a comprehensive guide for developing custom DOMWidgets in ComfyUI using Vanilla JavaScript. DOMWidgets allow you to embed standard HTML elements (div, video, canvas, input, etc.) into ComfyUI nodes while benefitting from the frontend's automatic layout and zoom management.
1. Core Concepts
In ComfyUI, a DOMWidget extends the default LiteGraph Canvas rendering logic. It maintains an HTML layer on top of the Canvas, making complex interactions and media displays significantly easier to implement than pure Canvas drawing.
Key APIs
app.registerExtension: The entry point for registering extensions.getCustomWidgets: A hook for defining new widget types associated with specific input types.node.addDOMWidget: The core method to add HTML elements to a node.
2. Basic Structure
A standard custom DOMWidget extension typically follows this structure:
import { app } from "../../scripts/app.js";
app.registerExtension({
name: "My.Custom.Extension",
async getCustomWidgets() {
return {
// Define a new widget type named "MY_WIDGET_TYPE"
MY_WIDGET_TYPE(node, inputName, inputData, app) {
// 1. Create the HTML element
const container = document.createElement("div");
container.innerHTML = "Hello <b>DOMWidget</b>!";
// 2. Setup styles (Optional but recommended)
container.style.color = "white";
container.style.backgroundColor = "#222";
container.style.padding = "5px";
// 3. Add the DOMWidget and return the result
const widget = node.addDOMWidget(inputName, "MY_WIDGET_TYPE", container, {
// Configuration options
getValue() {
return container.innerText;
},
setValue(v) {
container.innerText = v;
}
});
// 4. Return in the standard format
return { widget };
}
};
}
});
3. The addDOMWidget API
node.addDOMWidget(name, type, element, options)
Parameters
name: The internal name of the widget (usually matches the input name).type: The type identifier for the widget.element: The actual HTMLElement to embed.options: (Object) Configuration for lifecycle, sizing, and persistence.
Common options Fields
| Field | Type | Description |
|---|---|---|
getValue |
Function |
Defines how to retrieve the widget's value for serialization. |
setValue |
Function |
Defines how to restore the widget's state from workflow data. |
getMinHeight |
Function |
Returns the minimum height in pixels. |
getHeight |
Function |
Returns the preferred height (supports numbers or percentage strings like "50%"). |
onResize |
Function |
Callback triggered when the widget is resized. |
hideOnZoom |
Boolean |
Whether to hide the DOM element when zoomed out to improve performance (default: true). |
selectOn |
string[] |
Events on the element that should trigger node selection (default: ['focus', 'click']). |
4. Size Control
Custom DOMWidgets must actively inform the parent Node of their size requirements to ensure the Node layout is calculated correctly and connection wires remain aligned.
4.1 Core Mechanism
Whether in Canvas Mode or Vue Mode, the underlying logic model (LGraphNode) calls the widget's computeLayoutSize method to determine dimensions. This logic is used to calculate the Node's total size and the position of input/output slots.
4.2 Controlling Height
It is recommended to use the options parameter to define height behavior.
Method 1: Using options (Recommended)
const widget = node.addDOMWidget("MyWidget", "custom", element, {
// Specify minimum height in pixels
getMinHeight: () => 150,
// Or specify preferred height (pixels or percentage string)
// getHeight: () => "50%",
});
Method 2: Using CSS Variables
You can also set specific CSS variables on the root element:
element.style.setProperty("--comfy-widget-min-height", "150px");
// or --comfy-widget-height
4.3 Controlling Width
By default, a DOMWidget's width automatically stretches to fit the Node's width (which is determined by the Title or other Input Slots).
If you must force the Node to be wider to accommodate your widget, you need to override the widget instance's computeLayoutSize method:
const widget = node.addDOMWidget("WideWidget", "custom", element);
// Override the default layout calculation
widget.computeLayoutSize = (targetNode) => {
return {
minHeight: 150, // Must return height
minWidth: 300 // Force the Node to be at least 300px wide
};
};
4.4 Dynamic Resizing
If your widget's content changes dynamically (e.g., expanding sections, loading images), you must manually trigger a node resize:
// Execute after internal size changes:
node.setSize(node.computeSize());
node.setDirtyCanvas(true, true); // Force redraw
5. State Persistence (Serialization)
5.1 Default Behavior
DOMWidgets have serialization enabled by default (serialize property is true).
- Saving: ComfyUI attempts to read the widget's value to save into the Workflow file.
- Loading: ComfyUI reads the value from the Workflow file and assigns it to the widget.
5.2 Custom Serialization
To make persistence work effectively (saving internal DOM state and restoring it), you must implement getValue and setValue in the options:
getValue: Returns the state to be saved (Number, String, or Object).setValue: Receives the restored value and updates the DOM element.
Example:
const inputEl = document.createElement("input");
const widget = node.addDOMWidget("MyInput", "custom", inputEl, {
// 1. Called during Save
getValue: () => {
return inputEl.value;
},
// 2. Called during Load or Copy/Paste
setValue: (value) => {
inputEl.value = value || "";
}
});
// Optional: Listen for changes to update widget.value immediately
inputEl.addEventListener("change", () => {
widget.value = inputEl.value; // Triggers callbacks
});
5.3 The Restoration Mechanism (configure)
configure(data): When a Workflow is loaded,LGraphNodecalls itsconfigure(data)method.setValueChain: Duringconfigure, the Node iterates over the savedwidgets_valuesarray and assigns each value (widget.value = savedValue). For DOMWidgets, this assignment triggers thesetValuecallback defined in your options.
Therefore, options.setValue is the critical hook for restoring widget state.
5.4 Disabling Serialization
If your widget is purely for display (e.g., a real-time monitor or generated chart) and doesn't need to save state, disable serialization to reduce workflow file size.
Note: You cannot set this via options. You must modify the widget instance directly.
const widget = node.addDOMWidget("DisplayOnly", "custom", element);
widget.serialize = false; // Explicitly disable
6. Lifecycle & Events
6.1 onResize
When the Node size changes (e.g., user drags the corner), the widget can receive a notification via options:
const widget = node.addDOMWidget("ResizingWidget", "custom", element, {
onResize: (w) => {
// 'w' is the widget instance
// Adjust internal DOM layout here if necessary
console.log("Widget resized");
}
});
6.2 Construction & Mounting
- Construction: Occurs immediately when
addDOMWidgetis called. - Mounting:
- Canvas Mode: Appended to
.dom-widget-containerviaDomWidget.vue. - Vue Mode: Appended inside the Node component via
WidgetDOM.vue. - Caution: When
addDOMWidgetreturns, the element may not be in thedocument.bodyyet. If you need to access layout properties likegetBoundingClientRect, usesetTimeoutor wait for the firstonResize.
- Canvas Mode: Appended to
6.3 Cleanup
If you create external references (like setInterval or global event listeners), ensure you clean them up using node.onRemoved:
node.onRemoved = function() {
clearInterval(myInterval);
// Call original onRemoved if it existed
};
7. Styling & Best Practices
7.1 Styling
Since DOMWidgets are placed in absolute positioned containers or managed by Vue, ensure your container handles sizing gracefully:
container.style.width = "100%";
container.style.boxSizing = "border-box";
7.2 Path References
When importing app, adjust the path based on your extension's folder depth. Typically:
import { app } from "../../scripts/app.js";
7.3 Security
If setting innerHTML dynamically, ensure the content is sanitized or trusted to prevent XSS attacks.
8. Complete Example: Text Counter
This example implements a simple widget that displays the character count of another text widget in the same node.
import { app } from "../../scripts/app.js";
app.registerExtension({
name: "Comfy.TextCounter",
getCustomWidgets() {
return {
TEXT_COUNTER(node, inputName) {
const el = document.createElement("div");
Object.assign(el.style, {
background: "#222",
border: "1px solid #444",
padding: "8px",
borderRadius: "4px",
fontSize: "12px",
color: "#eee"
});
const label = document.createElement("span");
label.innerText = "Characters: 0";
el.appendChild(label);
const widget = node.addDOMWidget(inputName, "TEXT_COUNTER", el, {
getValue() { return ""; }, // Nothing to save
setValue(v) { }, // Nothing to restore
getMinHeight() { return 40; }
});
// Disable serialization for this display-only widget
widget.serialize = false;
// Custom method to update UI
widget.updateCount = (text) => {
label.innerText = `Characters: ${text.length}`;
};
return { widget };
}
};
},
nodeCreated(node) {
// Logic to link widgets after the node is initialized
if (node.comfyClass === "MyTextNode") {
const counterWidget = node.widgets.find(w => w.type === "TEXT_COUNTER");
const textWidget = node.widgets.find(w => w.name === "text");
if (counterWidget && textWidget) {
// Hook into the text widget's callback
const oldCallback = textWidget.callback;
textWidget.callback = function(v) {
if (oldCallback) oldCallback.apply(this, arguments);
counterWidget.updateCount(v);
};
}
}
}
});