diff --git a/src/components/graph/widgets/StringWidget.vue b/src/components/graph/widgets/StringWidget.vue
new file mode 100644
index 0000000000..2125d48480
--- /dev/null
+++ b/src/components/graph/widgets/StringWidget.vue
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/composables/widgets/useStringWidget.ts b/src/composables/widgets/useStringWidget.ts
index d9da814754..f5eb5f389f 100644
--- a/src/composables/widgets/useStringWidget.ts
+++ b/src/composables/widgets/useStringWidget.ts
@@ -91,6 +91,55 @@ function addMultilineWidget(
return widget
}
+function addSingleLineWidget(
+ node: LGraphNode,
+ name: string,
+ opts: { defaultVal: string; placeholder?: string }
+) {
+ const inputEl = document.createElement('input')
+ inputEl.className = 'comfy-text-input'
+ inputEl.type = 'text'
+ inputEl.value = opts.defaultVal
+ inputEl.placeholder = opts.placeholder || name
+
+ const widget = node.addDOMWidget(name, 'text', inputEl, {
+ getValue(): string {
+ return inputEl.value
+ },
+ setValue(v: string) {
+ inputEl.value = v
+ }
+ })
+
+ widget.inputEl = inputEl
+ widget.options.minNodeSize = [200, 40]
+
+ inputEl.addEventListener('input', () => {
+ widget.callback?.(widget.value)
+ })
+
+ // Allow middle mouse button panning
+ inputEl.addEventListener('pointerdown', (event: PointerEvent) => {
+ if (event.button === 1) {
+ app.canvas.processMouseDown(event)
+ }
+ })
+
+ inputEl.addEventListener('pointermove', (event: PointerEvent) => {
+ if ((event.buttons & 4) === 4) {
+ app.canvas.processMouseMove(event)
+ }
+ })
+
+ inputEl.addEventListener('pointerup', (event: PointerEvent) => {
+ if (event.button === 1) {
+ app.canvas.processMouseUp(event)
+ }
+ })
+
+ return widget
+}
+
export const useStringWidget = () => {
const widgetConstructor: ComfyWidgetConstructorV2 = (
node: LGraphNode,
@@ -108,7 +157,10 @@ export const useStringWidget = () => {
defaultVal,
placeholder: inputSpec.placeholder
})
- : node.addWidget('text', inputSpec.name, defaultVal, () => {}, {})
+ : addSingleLineWidget(node, inputSpec.name, {
+ defaultVal,
+ placeholder: inputSpec.placeholder
+ })
if (typeof inputSpec.dynamicPrompts === 'boolean') {
widget.dynamicPrompts = inputSpec.dynamicPrompts
diff --git a/src/composables/widgets/useStringWidgetVue.ts b/src/composables/widgets/useStringWidgetVue.ts
new file mode 100644
index 0000000000..bae4e15e28
--- /dev/null
+++ b/src/composables/widgets/useStringWidgetVue.ts
@@ -0,0 +1,65 @@
+import type { LGraphNode } from '@comfyorg/litegraph'
+import { ref } from 'vue'
+
+import StringWidget from '@/components/graph/widgets/StringWidget.vue'
+import {
+ type InputSpec,
+ isStringInputSpec
+} from '@/schemas/nodeDef/nodeDefSchemaV2'
+import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'
+import { type ComfyWidgetConstructorV2 } from '@/scripts/widgetTypes'
+
+const PADDING = 8
+
+export const useStringWidgetVue = (options: { defaultValue?: string } = {}) => {
+ const widgetConstructor: ComfyWidgetConstructorV2 = (
+ node: LGraphNode,
+ inputSpec: InputSpec
+ ) => {
+ if (!isStringInputSpec(inputSpec)) {
+ throw new Error(`Invalid input data: ${inputSpec}`)
+ }
+
+ // Initialize widget value
+ const widgetValue = ref(
+ inputSpec.default ?? options.defaultValue ?? ''
+ )
+
+ // Create the Vue-based widget instance
+ const widget = new ComponentWidgetImpl({
+ node,
+ name: inputSpec.name,
+ component: StringWidget,
+ 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: () => {
+ return inputSpec.multiline ? 80 + PADDING : 40 + PADDING
+ },
+
+ // Optional: whether to serialize this widget's value
+ serialize: true
+ }
+ })
+
+ // Add dynamic prompts support if specified
+ if (typeof inputSpec.dynamicPrompts === 'boolean') {
+ widget.dynamicPrompts = inputSpec.dynamicPrompts
+ }
+
+ // Register the widget with the node
+ addWidget(node, widget as any)
+
+ return widget
+ }
+
+ return widgetConstructor
+}
diff --git a/src/scripts/widgets.ts b/src/scripts/widgets.ts
index daef35e6e3..41a58c9355 100644
--- a/src/scripts/widgets.ts
+++ b/src/scripts/widgets.ts
@@ -11,6 +11,7 @@ import { useComboWidget } from '@/composables/widgets/useComboWidget'
import { useImageUploadWidget } from '@/composables/widgets/useImageUploadWidget'
import { useMarkdownWidget } from '@/composables/widgets/useMarkdownWidget'
import { useStringWidget } from '@/composables/widgets/useStringWidget'
+import { useStringWidgetVue } from '@/composables/widgets/useStringWidgetVue'
import { t } from '@/i18n'
import { transformInputSpecV1ToV2 } from '@/schemas/nodeDef/migration'
import type { InputSpec } from '@/schemas/nodeDefSchema'
@@ -282,7 +283,8 @@ export const ComfyWidgets: Record = {
useBadgedNumberInput({ mode: 'float' })
),
BOOLEAN: transformWidgetConstructorV2ToV1(useBooleanWidget()),
- STRING: transformWidgetConstructorV2ToV1(useStringWidget()),
+ STRING: transformWidgetConstructorV2ToV1(useStringWidgetVue()),
+ STRING_DOM: transformWidgetConstructorV2ToV1(useStringWidget()), // Fallback to DOM-based implementation
MARKDOWN: transformWidgetConstructorV2ToV1(useMarkdownWidget()),
COMBO: transformWidgetConstructorV2ToV1(useComboWidget()),
IMAGEUPLOAD: useImageUploadWidget(),
diff --git a/tests-ui/composables/useStringWidgetVue.test.ts b/tests-ui/composables/useStringWidgetVue.test.ts
new file mode 100644
index 0000000000..5a37a5a8d6
--- /dev/null
+++ b/tests-ui/composables/useStringWidgetVue.test.ts
@@ -0,0 +1,93 @@
+import { LGraphNode } from '@comfyorg/litegraph'
+import { describe, expect, it } from 'vitest'
+
+import { useStringWidgetVue } from '@/composables/widgets/useStringWidgetVue'
+import type { StringInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
+
+describe('useStringWidgetVue', () => {
+ it('creates widget constructor with correct default value', () => {
+ const constructor = useStringWidgetVue({ defaultValue: 'test' })
+ expect(constructor).toBeDefined()
+ expect(typeof constructor).toBe('function')
+ })
+
+ it('creates widget for single-line string input', () => {
+ const constructor = useStringWidgetVue()
+ const node = new LGraphNode('test')
+ const inputSpec: StringInputSpec = {
+ type: 'STRING',
+ name: 'test_input',
+ default: 'default_value',
+ placeholder: 'Enter text...'
+ }
+
+ const widget = constructor(node, inputSpec)
+
+ expect(widget).toBeDefined()
+ expect(widget.name).toBe('test_input')
+ expect(widget.value).toBe('default_value')
+ })
+
+ it('creates widget for multiline string input', () => {
+ const constructor = useStringWidgetVue()
+ const node = new LGraphNode('test')
+ const inputSpec: StringInputSpec = {
+ type: 'STRING',
+ name: 'multiline_input',
+ default: 'default\nvalue',
+ placeholder: 'Enter multiline text...',
+ multiline: true
+ }
+
+ const widget = constructor(node, inputSpec)
+
+ expect(widget).toBeDefined()
+ expect(widget.name).toBe('multiline_input')
+ expect(widget.value).toBe('default\nvalue')
+ })
+
+ it('handles placeholder fallback correctly', () => {
+ const constructor = useStringWidgetVue()
+ const node = new LGraphNode('test')
+ const inputSpec: StringInputSpec = {
+ type: 'STRING',
+ name: 'no_placeholder_input',
+ default: 'default_value'
+ }
+
+ const widget = constructor(node, inputSpec)
+
+ expect(widget).toBeDefined()
+ expect(widget.name).toBe('no_placeholder_input')
+ expect(widget.value).toBe('default_value')
+ })
+
+ it('supports dynamic prompts configuration', () => {
+ const constructor = useStringWidgetVue()
+ const node = new LGraphNode('test')
+ const inputSpec: StringInputSpec = {
+ type: 'STRING',
+ name: 'dynamic_input',
+ default: 'value',
+ dynamicPrompts: true
+ }
+
+ const widget = constructor(node, inputSpec)
+
+ expect(widget).toBeDefined()
+ expect(widget.dynamicPrompts).toBe(true)
+ })
+
+ it('throws error for invalid input spec', () => {
+ const constructor = useStringWidgetVue()
+ const node = new LGraphNode('test')
+ const invalidInputSpec = {
+ type: 'INT',
+ name: 'invalid_input'
+ } as any
+
+ expect(() => constructor(node, invalidInputSpec)).toThrow(
+ 'Invalid input data'
+ )
+ })
+})