From 9032226724ea35c5f8391ae7efb6b48488f492c8 Mon Sep 17 00:00:00 2001 From: Will Miao Date: Mon, 26 Jan 2026 23:22:37 +0800 Subject: [PATCH] fix(autocomplete): fix value persistence in DOM text widgets Remove multiple sources of truth and async sync chains that caused values to be lost during load/switch workflow or reload page. Changes: - Remove internalValue state variable from main.ts - Update getValue/setValue to read/write DOM directly via widget.inputEl - Remove textValue reactive ref and v-model from Vue component - Remove serializeValue, onSetValue, and watch callbacks - Register textarea reference on mount, clean up on unmount - Simplify AutocompleteTextWidgetInterface Follows ComfyUI built-in addMultilineWidget pattern: - Single source of truth (DOM element value only) - Direct sync (no intermediate variables or async chains) Also adds documentation: - docs/dom-widgets/value-persistence-best-practices.md - docs/dom-widgets/README.md - Update docs/dom_widget_dev_guide.md with reference --- docs/dom-widgets/README.md | 28 ++++ .../value-persistence-best-practices.md | 144 ++++++++++++++++++ docs/dom_widget_dev_guide.md | 2 + .../src/components/AutocompleteTextWidget.vue | 59 +++---- vue-widgets/src/main.ts | 9 +- .../vue-widgets/lora-manager-widgets.js | 135 ++++++++-------- .../vue-widgets/lora-manager-widgets.js.map | 2 +- 7 files changed, 260 insertions(+), 119 deletions(-) create mode 100644 docs/dom-widgets/README.md create mode 100644 docs/dom-widgets/value-persistence-best-practices.md diff --git a/docs/dom-widgets/README.md b/docs/dom-widgets/README.md new file mode 100644 index 00000000..a24e730b --- /dev/null +++ b/docs/dom-widgets/README.md @@ -0,0 +1,28 @@ +# DOM Widgets Documentation + +Documentation for custom DOM widget development in ComfyUI LoRA Manager. + +## Files + +- **[Value Persistence Best Practices](value-persistence-best-practices.md)** - Essential guide for implementing text input DOM widgets that persist values correctly + +## Key Lessons + +### Common Anti-Patterns + +❌ **Don't**: Create internal state variables +❌ **Don't**: Use v-model for text inputs +❌ **Don't**: Add serializeValue, onSetValue callbacks +❌ **Don't**: Watch props.widget.value + +### Best Practices + +✅ **Do**: Use DOM element as single source of truth +✅ **Do**: Store DOM reference on widget.inputEl +✅ **Do**: Direct getValue/setValue to DOM +✅ **Do**: Clean up reference on unmount + +## Related Documentation + +- [DOM Widget Development Guide](../dom_widget_dev_guide.md) - Comprehensive guide for building DOM widgets +- [ComfyUI Built-in Example](../../../../code/ComfyUI_frontend/src/renderer/extensions/vueNodes/widgets/composables/useStringWidget.ts) - Reference implementation diff --git a/docs/dom-widgets/value-persistence-best-practices.md b/docs/dom-widgets/value-persistence-best-practices.md new file mode 100644 index 00000000..95e4a11e --- /dev/null +++ b/docs/dom-widgets/value-persistence-best-practices.md @@ -0,0 +1,144 @@ +# DOM Widget Value Persistence - Best Practices + +## Problem + +DOM widgets with text inputs failed to persist values after: +- Loading workflows +- Switching workflows +- Reloading pages + +## Root Cause + +**Multiple sources of truth** causing sync issues: +- Internal state variable (`internalValue` in main.ts) +- Vue reactive ref (`textValue` in component) +- DOM element value (actual textarea) +- ComfyUI widget value (`props.widget.value`) + +**Broken sync chains:** +``` +getValue() → internalValue (not actual DOM value) +setValue(v) → internalValue → onSetValue() → textValue.value (async chain) +serializeValue() → textValue.value (different from getValue) +watch() → another sync layer +``` + +## Solution + +Follow ComfyUI built-in `addMultilineWidget` pattern: + +### ✅ Do + +1. **Single source of truth**: Use the DOM element directly + ```typescript + // main.ts + const widget = node.addDOMWidget(name, type, container, { + getValue() { + return widget.inputEl?.value ?? '' + }, + setValue(v: string) { + if (widget.inputEl) { + widget.inputEl.value = v ?? '' + } + } + }) + ``` + +2. **Register DOM reference** when component mounts + ```typescript + // Vue component + onMounted(() => { + if (textareaRef.value) { + props.widget.inputEl = textareaRef.value + } + }) + ``` + +3. **Clean up reference** on unmount + ```typescript + onUnmounted(() => { + if (props.widget.inputEl === textareaRef.value) { + props.widget.inputEl = undefined + } + }) + ``` + +4. **Simplify interface** - only expose what's needed + ```typescript + export interface MyWidgetInterface { + inputEl?: HTMLTextAreaElement + callback?: (v: string) => void + } + ``` + +### ❌ Don't + +1. **Don't create internal state variables** + ```typescript + // Wrong + let internalValue = '' + getValue() { return internalValue } + ``` + +2. **Don't use v-model** for text inputs in DOM widgets + ```html + +