Files
ComfyUI-Lora-Manager/docs/dom-widgets/value-persistence-best-practices.md
Will Miao 822ac046e0 docs: update DOM widget value persistence best practices guide
- Restructure document to clearly separate simple vs complex widget patterns
- Add detailed explanation of ComfyUI's built-in callback mechanism
- Provide complete implementation examples for both patterns
- Remove outdated sync chain diagrams and replace with practical guidance
- Emphasize using DOM element as source of truth for simple widgets
- Document proper use of internal state with widget.callback for complex widgets
2026-01-27 22:51:09 +08:00

5.7 KiB

DOM Widget Value Persistence - Best Practices

Overview

DOM widgets require different persistence patterns depending on their complexity. This document covers two patterns:

  1. Simple Text Widgets: DOM element as source of truth (e.g., textarea, input)
  2. Complex Widgets: Internal value with widget.callback (e.g., LoraPoolWidget, RandomizerWidget)

Understanding ComfyUI's Built-in Callback Mechanism

When widget.value is set (e.g., during workflow load), ComfyUI's domWidget.ts triggers this flow:

// From ComfyUI_frontend/src/scripts/domWidget.ts:146-149
set value(v: V) {
  this.options.setValue?.(v)      // 1. Update internal state
  this.callback?.(this.value)     // 2. Notify listeners for UI updates
}

This means:

  • setValue() handles storing the value
  • widget.callback() is automatically called to notify the UI
  • You don't need custom callback mechanisms like onSetValue

Pattern 1: Simple Text Input Widgets

For widgets where the value IS the DOM element's text content (textarea, input fields).

When to Use

  • Single text input/textarea widgets
  • Value is a simple string
  • No complex state management needed

Implementation

main.ts:

const widget = node.addDOMWidget(name, type, container, {
  getValue() {
    return widget.inputEl?.value ?? ''
  },
  setValue(v: string) {
    if (widget.inputEl) {
      widget.inputEl.value = v ?? ''
    }
  }
})

Vue Component:

onMounted(() => {
  if (textareaRef.value) {
    props.widget.inputEl = textareaRef.value
  }
})

onUnmounted(() => {
  if (props.widget.inputEl === textareaRef.value) {
    props.widget.inputEl = undefined
  }
})

Why This Works

  • Single source of truth: the DOM element
  • getValue() reads directly from DOM
  • setValue() writes directly to DOM
  • No sync issues between multiple state variables

Pattern 2: Complex Widgets

For widgets with structured data (JSON configs, arrays, objects) where the value cannot be stored in a DOM element.

When to Use

  • Value is a complex object/array (e.g., { loras: [...], settings: {...} })
  • Multiple UI elements contribute to the value
  • Vue reactive state manages the UI

Implementation

main.ts:

let internalValue: MyConfig | undefined

const widget = node.addDOMWidget(name, type, container, {
  getValue() {
    return internalValue
  },
  setValue(v: MyConfig) {
    internalValue = v
    // NO custom onSetValue needed - widget.callback is called automatically
  },
  serialize: true  // Ensure value is saved with workflow
})

Vue Component:

const config = ref<MyConfig>(getDefaultConfig())

onMounted(() => {
  // Set up callback for UI updates when widget.value changes externally
  // (e.g., workflow load, undo/redo)
  props.widget.callback = (newValue: MyConfig) => {
    if (newValue) {
      config.value = newValue
    }
  }

  // Restore initial value if workflow was already loaded
  if (props.widget.value) {
    config.value = props.widget.value
  }
})

// When UI changes, update widget value
function onConfigChange(newConfig: MyConfig) {
  config.value = newConfig
  props.widget.value = newConfig  // This also triggers callback
}

Why This Works

  1. Clear separation: internalValue stores the data, Vue ref manages the UI
  2. Built-in callback: ComfyUI calls widget.callback() automatically after setValue()
  3. Bidirectional sync:
    • External → UI: setValue() updates internalValue, callback() updates Vue ref
    • UI → External: User interaction updates Vue ref, which updates widget.value

Common Mistakes

Creating custom callback mechanisms

// Wrong - unnecessary complexity
setValue(v: MyConfig) {
  internalValue = v
  widget.onSetValue?.(v)  // Don't add this - use widget.callback instead
}

Use the built-in widget.callback instead.

Using v-model for simple text inputs in DOM widgets

<!-- Wrong - creates sync issues -->
<textarea v-model="textValue" />

<!-- Right for simple text widgets -->
<textarea ref="textareaRef" @input="onInput" />

Watching props.widget.value

// Wrong - creates race conditions
watch(() => props.widget.value, (newValue) => {
  config.value = newValue
})

Use widget.callback instead - it's called at the right time in the lifecycle.

Multiple sources of truth

// Wrong - who is the source of truth?
let internalValue = ''        // State 1
const textValue = ref('')     // State 2
const domElement = textarea   // State 3
props.widget.value            // State 4

Choose ONE source of truth:

  • Simple widgets: DOM element
  • Complex widgets: internalValue (with Vue ref as derived UI state)

Adding serializeValue for simple widgets

// Wrong - getValue/setValue handle serialization
props.widget.serializeValue = async () => textValue.value

Decision Guide

Widget Type Source of Truth Use widget.callback Example
Simple text input DOM element (inputEl) Optional AutocompleteTextWidget
Complex config internalValue Yes, for UI sync LoraPoolWidget
Vue component widget Vue ref + internalValue Yes RandomizerWidget

Testing Checklist

  • Load workflow - value restores correctly
  • Switch workflow - value persists
  • Reload page - value persists
  • UI interaction - value updates
  • Undo/redo - value syncs with UI
  • No console errors

References

  • ComfyUI DOMWidget implementation: ComfyUI_frontend/src/scripts/domWidget.ts
  • Simple text widget example: ComfyUI_frontend/src/renderer/extensions/vueNodes/widgets/composables/useStringWidget.ts