@vobs/runtime 1.5.0 → 1.6.0
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/dist/async-boundary.cjs +55 -0
- package/dist/async-boundary.cjs.map +1 -1
- package/dist/async-boundary.js +55 -0
- package/dist/async-boundary.js.map +1 -1
- package/dist/bind.cjs +8 -0
- package/dist/bind.cjs.map +1 -1
- package/dist/bind.js +8 -0
- package/dist/bind.js.map +1 -1
- package/dist/boundary.cjs +55 -0
- package/dist/boundary.cjs.map +1 -1
- package/dist/boundary.js +55 -0
- package/dist/boundary.js.map +1 -1
- package/dist/dynamic.cjs +55 -0
- package/dist/dynamic.cjs.map +1 -1
- package/dist/dynamic.js +55 -0
- package/dist/dynamic.js.map +1 -1
- package/dist/error-boundary.cjs +55 -0
- package/dist/error-boundary.cjs.map +1 -1
- package/dist/error-boundary.js +55 -0
- package/dist/error-boundary.js.map +1 -1
- package/dist/fragment.cjs.map +1 -1
- package/dist/fragment.js.map +1 -1
- package/dist/index.cjs +22 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -1
- package/dist/ops.cjs +21 -0
- package/dist/ops.cjs.map +1 -1
- package/dist/ops.d.cts +2 -1
- package/dist/ops.d.ts +2 -1
- package/dist/ops.js +20 -0
- package/dist/ops.js.map +1 -1
- package/dist/profiler.cjs +55 -0
- package/dist/profiler.cjs.map +1 -1
- package/dist/profiler.js +55 -0
- package/dist/profiler.js.map +1 -1
- package/package.json +2 -2
- package/src/bind.ts +12 -7
- package/src/jsx-integration.test.ts +30 -1
- package/src/ops.ts +27 -0
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
|