@vobs/runtime 1.5.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/bind.ts CHANGED
@@ -1,15 +1,15 @@
1
- // 动态绑定:将 Signal 绑定到 DOM 节点
2
-
1
+ // 动态绑定:将 Signal 绑定到 DOM 节点
2
+
3
3
  import { effect } from '@vobs/reactivity'
4
4
  import type { Signal } from '@vobs/reactivity'
5
- import { setAttribute, setProperty, setTextContent } from './ops'
5
+ import { registerSelectValueBinding, setAttribute, setProperty, setTextContent } from './ops'
6
6
 
7
7
  export type ValueSource<T> = Signal<T> | (() => T)
8
8
 
9
9
  function readSource<T>(source: ValueSource<T>): T {
10
10
  return typeof source === 'function' ? source() : source.value
11
11
  }
12
-
12
+
13
13
  export function bindText(
14
14
  node: Text,
15
15
  source: ValueSource<unknown>
@@ -19,10 +19,10 @@ export function bindText(
19
19
  setTextContent(node, value === null || value === undefined || typeof value === 'boolean' ? '' : String(value))
20
20
  })
21
21
  }
22
-
22
+
23
23
  export function bindAttribute(
24
- node: Element,
25
- key: string,
24
+ node: Element,
25
+ key: string,
26
26
  source: ValueSource<unknown>
27
27
  ): void {
28
28
  effect(() => {
@@ -41,6 +41,11 @@ export function bindProperty(
41
41
  key: string,
42
42
  source: ValueSource<unknown>
43
43
  ): void {
44
+ // Select initial-value auto resync: remember the bound reader so that inserting
45
+ // <option> children later re-applies the current value (see ops.insertBefore).
46
+ if (key === 'value' && node.nodeName === 'SELECT') {
47
+ registerSelectValueBinding(node, () => readSource(source))
48
+ }
44
49
  effect(() => {
45
50
  setProperty(node, key, readSource(source))
46
51
  })
@@ -145,10 +145,39 @@ describe('JSX 编译 × runtime 集成回归', () => {
145
145
  expect(input.getAttribute('spellcheck')).toBe('false')
146
146
  expect((el.querySelector('td') as HTMLTableCellElement).colSpan).toBe(2)
147
147
  })
148
+
149
+ it('⑥ select 绑定 value 时选项异步到达自动重同步(不再需要 ref 兜底)', async () => {
150
+ setRenderer(createDOMRenderer())
151
+ const items = state<string[]>([])
152
+ const el = runJsxWith(`
153
+ const current = { value: 'b' }
154
+ const el = <select value={current.value}>{items.value.map(o => <option value={o}>{o}</option>)}</select>
155
+ `, { items }) as HTMLSelectElement
156
+ expect(el.value).toBe('')
157
+ items.value = ['a', 'b', 'c']
158
+ await new Promise(resolve => setTimeout(resolve, 20))
159
+ expect(el.value).toBe('b')
160
+ })
161
+
162
+ it('⑥b select 选项经 optgroup 插入同样重同步', async () => {
163
+ setRenderer(createDOMRenderer())
164
+ interface Group { readonly label: string; readonly options: readonly string[] }
165
+ const groups = state<readonly Group[]>([])
166
+ const el = runJsxWith(`
167
+ const current = { value: 'b' }
168
+ const el = <select value={current.value}>{groups.value.map(group => (
169
+ <optgroup label={group.label}>{group.options.map(o => <option value={o}>{o}</option>)}</optgroup>
170
+ ))}</select>
171
+ `, { groups }) as HTMLSelectElement
172
+ expect(el.value).toBe('')
173
+ groups.value = [{ label: 'G', options: ['a', 'b'] }]
174
+ await new Promise(resolve => setTimeout(resolve, 20))
175
+ expect(el.value).toBe('b')
176
+ })
148
177
  })
149
178
 
150
179
  /** runJsx 变体:额外注入外部依赖(真实信号等),供编译产物闭包引用 */
151
- function runJsxWith(source: string, deps: Record<string, unknown>): unknown {
180
+ function runJsxWith(source: string, deps: Record<string, unknown> = {}): unknown {
152
181
  const compiled = compile(source, { filename: 'integration-deps.tsx' }) as unknown as string
153
182
  const imports = compiled.match(/import \{([^}]+)\} from "@vobs\/vobs";/)
154
183
  if (!imports) throw new Error('编译产物缺少 runtime 导入')
package/src/ops.ts CHANGED
@@ -64,6 +64,8 @@ export function insertBefore(
64
64
  }
65
65
  getRenderer().insertBefore(parent, child, isVobsFragment(anchor) ? anchor.start : anchor)
66
66
  markHmrInstanceMounted(child, parent)
67
+ const nodeName = (child as Element).nodeName
68
+ if (nodeName === 'OPTION' || nodeName === 'OPTGROUP') syncSelectValue(parent)
67
69
  if (getRuntimeDebugHooks()) {
68
70
  invokeRuntimeDebug('domMutation', {
69
71
  operation: 'insert',
@@ -73,6 +75,31 @@ export function insertBefore(
73
75
  }
74
76
  }
75
77
 
78
+ /**
79
+ * Select initial-value auto resync: `bindProperty(node, 'value', …)` registers the value
80
+ * reader for `<select>` elements; whenever an `<option>` is inserted into the select
81
+ * (directly or through an `<optgroup>`), the current bound value is re-applied. This
82
+ * removes the last reason for apps to keep `ref + queueMicrotask` value-sync workarounds
83
+ * for options that arrive after the value binding (e.g. asynchronously loaded lists).
84
+ */
85
+ const selectValueReaders = new WeakMap<object, () => unknown>()
86
+
87
+ export function registerSelectValueBinding(select: Element, read: () => unknown): void {
88
+ selectValueReaders.set(select, read)
89
+ }
90
+
91
+ function syncSelectValue(parent: Node): void {
92
+ let current: Node | null = parent
93
+ while (current !== null) {
94
+ if ((current as Element).nodeName === 'SELECT') {
95
+ const read = selectValueReaders.get(current)
96
+ if (read !== undefined) setProperty(current as Element, 'value', read())
97
+ return
98
+ }
99
+ current = current.parentNode
100
+ }
101
+ }
102
+
76
103
  export function removeChild(
77
104
  parent: Node,
78
105
  child: VobsNode