# Step-by-Step Guide: Converting Widgets to Vue Components in ComfyUI ## Overview This guide explains how to convert existing DOM widgets or create new widgets using Vue components in ComfyUI. The Vue widget system provides better reactivity, type safety, and maintainability compared to traditional DOM manipulation. ## Prerequisites - Understanding of Vue 3 Composition API - Basic knowledge of TypeScript - Familiarity with ComfyUI widget system ## Step 1: Create the Vue Component Create a new Vue component in `src/components/graph/widgets/`: ```vue ``` ## Step 2: Create the Widget Composables (Dual Pattern) The Vue widget system uses a **dual composable pattern** for separation of concerns: ### 2a. Create the Widget Constructor Composable Create the core widget constructor in `src/composables/widgets/`: ```typescript // src/composables/widgets/useYourWidget.ts import { ref } from 'vue' import type { LGraphNode } from '@comfyorg/litegraph' import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2' import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget' import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets' import YourWidget from '@/components/graph/widgets/YourWidget.vue' const PADDING = 8 export const useYourWidget = (options: { defaultValue?: string } = {}) => { const widgetConstructor: ComfyWidgetConstructorV2 = ( node: LGraphNode, inputSpec: InputSpec ) => { // Initialize widget value const widgetValue = ref(options.defaultValue ?? '') // Create the widget instance const widget = new ComponentWidgetImpl({ node, name: inputSpec.name, component: YourWidget, inputSpec, options: { // Required: getter for widget value getValue: () => widgetValue.value, // Required: setter for widget value setValue: (value: string) => { widgetValue.value = value }, // Optional: minimum height for the widget getMinHeight: () => options.minHeight ?? 40 + PADDING, // Optional: whether to serialize this widget's value serialize: true, // Optional: custom serialization serializeValue: (value: string) => { return { yourWidget: value } } } }) // Register the widget with the node addWidget(node, widget) return widget } return widgetConstructor } ``` ### 2b. Create the Node-Level Logic Composable (When Needed) **Only create this if your widget needs dynamic management** (showing/hiding widgets based on events, execution state, etc.). Most standard widgets only need the widget constructor composable. For widgets that need node-level operations (like showing/hiding widgets dynamically), create a separate composable in `src/composables/node/`: ```typescript // src/composables/node/useNodeYourWidget.ts import type { LGraphNode } from '@comfyorg/litegraph' import { useYourWidget } from '@/composables/widgets/useYourWidget' const YOUR_WIDGET_NAME = '$$node-your-widget' /** * Composable for handling node-level operations for YourWidget */ export function useNodeYourWidget() { const yourWidget = useYourWidget() const findYourWidget = (node: LGraphNode) => node.widgets?.find((w) => w.name === YOUR_WIDGET_NAME) const addYourWidget = (node: LGraphNode) => yourWidget(node, { name: YOUR_WIDGET_NAME, type: 'yourWidgetType' }) /** * Shows your widget for a node * @param node The graph node to show the widget for * @param value The value to set */ function showYourWidget(node: LGraphNode, value: string) { const widget = findYourWidget(node) ?? addYourWidget(node) widget.value = value node.setDirtyCanvas?.(true) } /** * Removes your widget from a node * @param node The graph node to remove the widget from */ function removeYourWidget(node: LGraphNode) { if (!node.widgets) return const widgetIdx = node.widgets.findIndex( (w) => w.name === YOUR_WIDGET_NAME ) if (widgetIdx > -1) { node.widgets[widgetIdx].onRemove?.() node.widgets.splice(widgetIdx, 1) } } return { showYourWidget, removeYourWidget } } ``` ## Step 3: Register the Widget Add your widget to the global widget registry in `src/scripts/widgets.ts`: ```typescript // src/scripts/widgets.ts import { useYourWidget } from '@/composables/widgets/useYourWidget' import { transformWidgetConstructorV2ToV1 } from '@/scripts/utils' export const ComfyWidgets: Record = { // ... existing widgets ... YOUR_WIDGET: transformWidgetConstructorV2ToV1(useYourWidget()), } ``` ## Step 4: Handle Widget-Specific Logic For widgets that need special handling (e.g., listening to execution events): ```typescript // In your composable or a separate composable import { useExecutionStore } from '@/stores/executionStore' import { watchEffect, onUnmounted } from 'vue' export const useYourWidgetLogic = (nodeId: string) => { const executionStore = useExecutionStore() // Watch for execution state changes const stopWatcher = watchEffect(() => { if (executionStore.isNodeExecuting(nodeId)) { // Handle execution start } }) // Cleanup onUnmounted(() => { stopWatcher() }) } ``` ## Step 5: Handle Complex Widget Types For widgets with complex data types or special requirements: ```typescript // Multi-value widget example const widget = new ComponentWidgetImpl({ node, name: inputSpec.name, component: MultiSelectWidget, inputSpec, options: { getValue: () => widgetValue.value, setValue: (value: string[]) => { widgetValue.value = Array.isArray(value) ? value : [] }, getMinHeight: () => 40 + PADDING, // Custom validation isValid: (value: string[]) => { return Array.isArray(value) && value.length > 0 } } }) ``` ## Step 6: Add Widget Props (Optional) Pass additional props to your Vue component: ```typescript const widget = new ComponentWidgetImpl({ node, name: inputSpec.name, component: YourWidget, inputSpec, props: { placeholder: 'Enter value...' }, options: { // ... options } }) ``` ## Step 7: Handle Widget Lifecycle For widgets that need cleanup or special lifecycle handling: ```typescript // In your widget component ``` ## Step 8: Test Your Widget 1. Create a test node that uses your widget: ```python class TestYourWidget: @classmethod def INPUT_TYPES(cls): return { "required": { "value": ("YOUR_WIDGET", {"default": "test"}) } } ``` 2. Write unit tests for your composable: ```typescript // tests-ui/composables/useYourWidget.test.ts import { describe, it, expect } from 'vitest' import { useYourWidget } from '@/composables/widgets/useYourWidget' describe('useYourWidget', () => { it('creates widget with correct default value', () => { const constructor = useYourWidget({ defaultValue: 'test' }) // ... test implementation }) }) ``` ## Common Patterns and Best Practices ### 1. Use PrimeVue Components (REQUIRED) **Always use PrimeVue components** for UI elements to maintain consistency across the application. ComfyUI includes PrimeVue 4.2.5 with 147 available components. **Reference Documentation**: - See `primevue-components.md` in the project root directory for a complete list of all available PrimeVue components with descriptions and documentation links - Alternative location: `vue-widget-conversion/primevue-components.md` (if working in a conversion branch) - This reference includes all 147 components organized by category (Form, Button, Data, Panel, etc.) with enhanced descriptions **Important**: When deciding how to create a widget, always consult the PrimeVue components reference first to find the most appropriate component for your use case. Common widget components include: ```vue