@vobs/dom 1.0.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/LICENSE +21 -0
- package/README.md +49 -0
- package/package.json +22 -0
- package/src/index.ts +34 -0
- package/src/renderer.test.ts +162 -0
- package/src/renderer.ts +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 vobs contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# @vobs/dom
|
|
2
|
+
|
|
3
|
+
Browser DOM adapter implementing the `VobsRenderer` interface, plus the runtime node and reactive binding primitives.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @vobs/dom
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { createDOMRenderer, setRenderer, createElement, createText, insertBefore, bindText } from '@vobs/dom'
|
|
15
|
+
import { state } from '@vobs/reactivity'
|
|
16
|
+
|
|
17
|
+
setRenderer(createDOMRenderer())
|
|
18
|
+
|
|
19
|
+
const count = state(0)
|
|
20
|
+
const span = createElement('span')
|
|
21
|
+
const text = createText('')
|
|
22
|
+
insertBefore(span, text, null)
|
|
23
|
+
bindText(text, () => `count: ${count.value}`)
|
|
24
|
+
insertBefore(document.body, span, null)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## API
|
|
28
|
+
|
|
29
|
+
| Signature | Description |
|
|
30
|
+
| --- | --- |
|
|
31
|
+
| `createDOMRenderer(): VobsRenderer<Node, Text, Element, Comment>` | Create the browser DOM renderer implementation. |
|
|
32
|
+
| `setRenderer(renderer)` / `getRenderer()` | Install or read the renderer used by the node factories. |
|
|
33
|
+
| `createElement(tag)` / `createText(content)` / `createComment(content)` | Create nodes through the active renderer. |
|
|
34
|
+
| `insertBefore(parent, child, anchor)` / `removeChild(parent, child)` | Insert or remove nodes. |
|
|
35
|
+
| `setTextContent(node, content)` / `setProperty(node, key, value)` / `setAttribute(node, key, value)` | Update node content, properties and attributes. |
|
|
36
|
+
| `setStaticProps(node, props)` | Apply a static props object to an element. |
|
|
37
|
+
| `addEventListener(node, event, handler)` / `removeEventListener(node, event, handler)` | Bind or unbind event handlers. |
|
|
38
|
+
| `bindText(node, source)` / `bindAttribute(node, key, source)` | Reactive bindings from a `ValueSource` (signal or getter). |
|
|
39
|
+
| `insertDynamic(parent, anchor, factory)` | Conditional block that creates and disposes nodes reactively. |
|
|
40
|
+
| `insertList(parent, anchor, source, renderItem, keyOf?)` | Keyed or indexed list rendering. |
|
|
41
|
+
| `createComponent(fn, props)` / `createFragment(factory)` / `createBlock(factory)` | Component, fragment and lazy block primitives. |
|
|
42
|
+
| `ref(initialValue?)` / `setRef(node, target)` | Ref objects for direct node access. |
|
|
43
|
+
| `insertErrorBoundary(parent, anchor, options)` / `insertAsyncBoundary(parent, anchor, options)` | Error and async boundaries without wrapper nodes. |
|
|
44
|
+
| `insertProfiler(parent, anchor, options)` | Profiler scope. |
|
|
45
|
+
| `disposeNodeOwner(node)` / `clear(container)` | Dispose a node's owner / remove all children. |
|
|
46
|
+
|
|
47
|
+
## Types
|
|
48
|
+
|
|
49
|
+
VobsRenderer, FragmentFactory, VobsFragment, VobsNode, ValueSource, Ref, RefTarget, NodeFactory, ErrorBoundaryFallback, ErrorBoundaryOptions, AsyncBoundaryFallback, AsyncBoundaryOptions, AsyncBoundaryProps, AsyncBoundaryView, ProfilerOptions, ProfilerProps, ProfilerRenderInfo
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"license": "MIT",
|
|
3
|
+
"files": [
|
|
4
|
+
"src",
|
|
5
|
+
"README.md",
|
|
6
|
+
"LICENSE"
|
|
7
|
+
],
|
|
8
|
+
"name": "@vobs/dom",
|
|
9
|
+
"version": "1.0.0",
|
|
10
|
+
"type": "module",
|
|
11
|
+
"main": "src/index.ts",
|
|
12
|
+
"types": "src/index.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": "./src/index.ts"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@vobs/runtime": "1.0.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@vobs/reactivity": "1.0.0"
|
|
21
|
+
}
|
|
22
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export { createDOMRenderer } from './renderer'
|
|
2
|
+
export type { VobsRenderer } from '@vobs/runtime'
|
|
3
|
+
export {
|
|
4
|
+
setRenderer,
|
|
5
|
+
getRenderer,
|
|
6
|
+
createText,
|
|
7
|
+
createElement,
|
|
8
|
+
createComment,
|
|
9
|
+
insertBefore,
|
|
10
|
+
removeChild,
|
|
11
|
+
setTextContent,
|
|
12
|
+
setProperty,
|
|
13
|
+
setAttribute,
|
|
14
|
+
setStaticProps,
|
|
15
|
+
addEventListener,
|
|
16
|
+
removeEventListener,
|
|
17
|
+
clear,
|
|
18
|
+
createComponent,
|
|
19
|
+
createBlock,
|
|
20
|
+
createFragment,
|
|
21
|
+
isVobsFragment,
|
|
22
|
+
disposeNodeOwner
|
|
23
|
+
} from '@vobs/runtime'
|
|
24
|
+
export type { FragmentFactory, VobsFragment, VobsNode } from '@vobs/runtime'
|
|
25
|
+
export { bindText, bindAttribute } from '@vobs/runtime'
|
|
26
|
+
export type { ValueSource } from '@vobs/runtime'
|
|
27
|
+
export { ref, setRef } from '@vobs/runtime'
|
|
28
|
+
export type { Ref, RefTarget } from '@vobs/runtime'
|
|
29
|
+
export { insertDynamic, insertList } from '@vobs/runtime'
|
|
30
|
+
export type { NodeFactory } from '@vobs/runtime'
|
|
31
|
+
export { insertErrorBoundary } from '@vobs/runtime'
|
|
32
|
+
export type { ErrorBoundaryFallback, ErrorBoundaryOptions } from '@vobs/runtime'
|
|
33
|
+
export { AsyncBoundary, insertAsyncBoundary, Profiler, insertProfiler } from '@vobs/runtime'
|
|
34
|
+
export type { AsyncBoundaryFallback, AsyncBoundaryOptions, AsyncBoundaryProps, AsyncBoundaryView, ProfilerOptions, ProfilerProps, ProfilerRenderInfo } from '@vobs/runtime'
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
createDOMRenderer,
|
|
4
|
+
setRenderer,
|
|
5
|
+
createText,
|
|
6
|
+
createElement,
|
|
7
|
+
insertBefore,
|
|
8
|
+
bindText,
|
|
9
|
+
insertDynamic,
|
|
10
|
+
insertList
|
|
11
|
+
} from './index'
|
|
12
|
+
import { onDispose, state } from '@vobs/reactivity'
|
|
13
|
+
|
|
14
|
+
describe('dom', () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
setRenderer(createDOMRenderer())
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('创建文本节点', () => {
|
|
20
|
+
const text = createText('hello')
|
|
21
|
+
|
|
22
|
+
expect(text.textContent).toBe('hello')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('创建元素并插入', () => {
|
|
26
|
+
const parent = createElement('div')
|
|
27
|
+
const child = createElement('span')
|
|
28
|
+
const text = createText('内容')
|
|
29
|
+
|
|
30
|
+
insertBefore(child, text, null)
|
|
31
|
+
insertBefore(parent, child, null)
|
|
32
|
+
|
|
33
|
+
expect(parent.innerHTML).toBe('<span>内容</span>')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('bindText 响应式更新', async () => {
|
|
37
|
+
const count = state(0)
|
|
38
|
+
const text = createText('')
|
|
39
|
+
|
|
40
|
+
bindText(text, count)
|
|
41
|
+
|
|
42
|
+
expect(text.textContent).toBe('0')
|
|
43
|
+
|
|
44
|
+
count.value = 5
|
|
45
|
+
await Promise.resolve()
|
|
46
|
+
|
|
47
|
+
expect(text.textContent).toBe('5')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('动态 getter 会追踪读取到的 state', async () => {
|
|
51
|
+
const count = state(0)
|
|
52
|
+
const text = createText('')
|
|
53
|
+
|
|
54
|
+
bindText(text, () => `count: ${count.value}`)
|
|
55
|
+
count.value = 2
|
|
56
|
+
await Promise.resolve()
|
|
57
|
+
|
|
58
|
+
expect(text.textContent).toBe('count: 2')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('条件块切换时创建和销毁节点', async () => {
|
|
62
|
+
const show = state(true)
|
|
63
|
+
const parent = createElement('div')
|
|
64
|
+
let disposed = 0
|
|
65
|
+
|
|
66
|
+
insertDynamic(parent, null, () => {
|
|
67
|
+
if (!show.value) return null
|
|
68
|
+
onDispose(() => { disposed++ })
|
|
69
|
+
const child = createElement('span')
|
|
70
|
+
insertBefore(child, createText('visible'), null)
|
|
71
|
+
return child
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
expect(parent.textContent).toContain('visible')
|
|
75
|
+
show.value = false
|
|
76
|
+
await Promise.resolve()
|
|
77
|
+
|
|
78
|
+
expect(parent.textContent).not.toContain('visible')
|
|
79
|
+
expect(disposed).toBe(1)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('keyed 列表复用节点、更新内容并保留顺序', async () => {
|
|
83
|
+
const items = state([
|
|
84
|
+
{ id: 1, name: 'A' },
|
|
85
|
+
{ id: 2, name: 'B' }
|
|
86
|
+
])
|
|
87
|
+
const parent = createElement('ul')
|
|
88
|
+
const created: Element[] = []
|
|
89
|
+
|
|
90
|
+
insertList(
|
|
91
|
+
parent,
|
|
92
|
+
null,
|
|
93
|
+
() => items.value,
|
|
94
|
+
item => {
|
|
95
|
+
const node = createElement('li')
|
|
96
|
+
created.push(node)
|
|
97
|
+
const text = createText('')
|
|
98
|
+
insertBefore(node, text, null)
|
|
99
|
+
bindText(text, () => item.name)
|
|
100
|
+
return node
|
|
101
|
+
},
|
|
102
|
+
item => item.id
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
const first = created[0]
|
|
106
|
+
items.value = [
|
|
107
|
+
{ id: 2, name: 'B updated' },
|
|
108
|
+
{ id: 1, name: 'A' }
|
|
109
|
+
]
|
|
110
|
+
await Promise.resolve()
|
|
111
|
+
|
|
112
|
+
expect(created).toHaveLength(2)
|
|
113
|
+
expect(parent.children[1]).toBe(first)
|
|
114
|
+
expect(parent.textContent).toContain('B updated')
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('keyed 列表在复用带 index 的渲染项时使用新索引', async () => {
|
|
118
|
+
const items = state([{ id: 'a' }, { id: 'b' }])
|
|
119
|
+
const parent = createElement('ul')
|
|
120
|
+
|
|
121
|
+
insertList(
|
|
122
|
+
parent,
|
|
123
|
+
null,
|
|
124
|
+
() => items.value,
|
|
125
|
+
(item, index) => {
|
|
126
|
+
const node = createElement('li')
|
|
127
|
+
const text = createText('')
|
|
128
|
+
insertBefore(node, text, null)
|
|
129
|
+
bindText(text, () => `${item.id}:${index}`)
|
|
130
|
+
return node
|
|
131
|
+
},
|
|
132
|
+
item => item.id
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
items.value = [items.value[1]!, items.value[0]!]
|
|
136
|
+
await Promise.resolve()
|
|
137
|
+
|
|
138
|
+
expect(parent.textContent).toBe('b:0a:1')
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('列表项删除时销毁其 Owner', async () => {
|
|
142
|
+
const items = state([{ id: 1 }])
|
|
143
|
+
const parent = createElement('ul')
|
|
144
|
+
let disposed = false
|
|
145
|
+
|
|
146
|
+
insertList(
|
|
147
|
+
parent,
|
|
148
|
+
null,
|
|
149
|
+
() => items.value,
|
|
150
|
+
() => {
|
|
151
|
+
onDispose(() => { disposed = true })
|
|
152
|
+
return createElement('li')
|
|
153
|
+
},
|
|
154
|
+
item => item.id
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
items.value = []
|
|
158
|
+
await Promise.resolve()
|
|
159
|
+
|
|
160
|
+
expect(disposed).toBe(true)
|
|
161
|
+
})
|
|
162
|
+
})
|
package/src/renderer.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// 浏览器 DOM 适配器
|
|
2
|
+
|
|
3
|
+
import type { VobsRenderer } from '@vobs/runtime'
|
|
4
|
+
|
|
5
|
+
export function createDOMRenderer(): VobsRenderer<Node, Text, Element, Comment> {
|
|
6
|
+
return {
|
|
7
|
+
createText(content: string): Text {
|
|
8
|
+
return document.createTextNode(content)
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
createElement(tag: string): Element {
|
|
12
|
+
return document.createElement(tag)
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
createComment(content: string): Comment {
|
|
16
|
+
return document.createComment(content)
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
insertBefore(parent: Node, child: Node, anchor: Node | null): void {
|
|
20
|
+
parent.insertBefore(child, anchor)
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
removeChild(parent: Node, child: Node): void {
|
|
24
|
+
parent.removeChild(child)
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
setTextContent(node: Text, content: string): void {
|
|
28
|
+
node.textContent = content
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
setProperty(node: Element, key: string, value: unknown): void {
|
|
32
|
+
Reflect.set(node, key, value)
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
setAttribute(node: Element, key: string, value: string): void {
|
|
36
|
+
node.setAttribute(key, value)
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
addEventListener(node: Element, event: string, handler: EventListener): void {
|
|
40
|
+
node.addEventListener(event, handler)
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
removeEventListener(node: Element, event: string, handler: EventListener): void {
|
|
44
|
+
node.removeEventListener(event, handler)
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
nextSibling(node: Node): Node | null {
|
|
48
|
+
return node.nextSibling
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
clear(container: Node): void {
|
|
52
|
+
container.textContent = ''
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|