Skip to content

Commit a6790ed

Browse files
committed
feat(runtime-vapor): support custom directives on vapor components
1 parent f37b6af commit a6790ed

File tree

3 files changed

+102
-10
lines changed

3 files changed

+102
-10
lines changed

packages/runtime-vapor/__tests__/directives/customDirective.spec.ts

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import { effectScope, ref } from '@vue/reactivity'
2-
import { type VaporDirective, withVaporDirectives } from '../../src'
2+
import {
3+
type VaporDirective,
4+
createComponent,
5+
defineVaporComponent,
6+
withVaporDirectives,
7+
} from '../../src'
38
import { nextTick, watchEffect } from '@vue/runtime-dom'
9+
import type { Mock } from 'vitest'
410

511
describe('custom directive', () => {
612
it('should work', async () => {
@@ -36,4 +42,68 @@ describe('custom directive', () => {
3642
// should be stopped and not update
3743
expect(el.textContent).toBe('2')
3844
})
45+
46+
it('should work on single root component', async () => {
47+
const teardown = vi.fn()
48+
const dir: VaporDirective = vi.fn((el, source) => {
49+
watchEffect(() => {
50+
el.textContent = source()
51+
})
52+
return teardown
53+
})
54+
const scope = effectScope()
55+
const n = ref(1)
56+
const source = () => n.value
57+
58+
// Child component with single root
59+
const Child = defineVaporComponent({
60+
render() {
61+
const el = document.createElement('div')
62+
return el
63+
},
64+
})
65+
66+
const root = document.createElement('div')
67+
68+
scope.run(() => {
69+
const instance = createComponent(Child)
70+
withVaporDirectives(instance, [[dir, source]])
71+
root.appendChild(instance.block as Node)
72+
})
73+
74+
// Should resolve to the div element inside Child
75+
expect(dir).toHaveBeenCalled()
76+
const el = (dir as unknown as Mock).mock.calls[0][0]
77+
expect(el).toBeInstanceOf(HTMLDivElement)
78+
expect(el.textContent).toBe('1')
79+
80+
n.value = 2
81+
await nextTick()
82+
expect(el.textContent).toBe('2')
83+
84+
scope.stop()
85+
expect(teardown).toHaveBeenCalled()
86+
})
87+
88+
it('should warn on multi-root component', () => {
89+
const dir: VaporDirective = vi.fn()
90+
const scope = effectScope()
91+
92+
// Child component with multiple roots
93+
const Child = defineVaporComponent({
94+
render() {
95+
return [document.createElement('div'), document.createElement('span')]
96+
},
97+
})
98+
99+
scope.run(() => {
100+
const instance = createComponent(Child)
101+
withVaporDirectives(instance, [[dir]])
102+
})
103+
104+
expect(dir).not.toHaveBeenCalled()
105+
expect(
106+
'Runtime directive used on component with non-element root node',
107+
).toHaveBeenWarned()
108+
})
39109
})

packages/runtime-vapor/src/component.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ import {
9999
isLastInsertion,
100100
resetInsertionState,
101101
} from './insertionState'
102-
import { DynamicFragment } from './fragment'
102+
import { DynamicFragment, isFragment } from './fragment'
103103
import type { VaporElement } from './apiDefineVaporCustomElement'
104104

105105
export { currentInstance } from '@vue/runtime-dom'
@@ -415,7 +415,7 @@ export function applyFallthroughProps(
415415
block: Block,
416416
attrs: Record<string, any>,
417417
): void {
418-
const el = getRootElement(block)
418+
const el = getRootElement(block, false)
419419
if (el) {
420420
isApplyingFallthroughProps = true
421421
setDynamicProps(el, [attrs])
@@ -820,16 +820,24 @@ export function getExposed(
820820
}
821821
}
822822

823-
function getRootElement(block: Block): Element | undefined {
823+
export function getRootElement(
824+
block: Block,
825+
recurse: boolean = true,
826+
): Element | undefined {
824827
if (block instanceof Element) {
825828
return block
826829
}
827830

828-
if (block instanceof DynamicFragment) {
831+
if (recurse && isVaporComponent(block)) {
832+
return getRootElement(block.block, recurse)
833+
}
834+
835+
if (isFragment(block)) {
829836
const { nodes } = block
830837
if (nodes instanceof Element && (nodes as any).$root) {
831838
return nodes
832839
}
840+
return getRootElement(nodes, recurse)
833841
}
834842

835843
// The root node contains comments. It is necessary to filter out
@@ -843,7 +851,7 @@ function getRootElement(block: Block): Element | undefined {
843851
hasComment = true
844852
continue
845853
}
846-
const thisRoot = getRootElement(b)
854+
const thisRoot = getRootElement(b, recurse)
847855
// only return root if there is exactly one eligible root in the array
848856
if (!thisRoot || singleRoot) {
849857
return

packages/runtime-vapor/src/directives/custom.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1-
import { type DirectiveModifiers, onScopeDispose } from '@vue/runtime-dom'
2-
import type { VaporComponentInstance } from '../component'
1+
import { type DirectiveModifiers, onScopeDispose, warn } from '@vue/runtime-dom'
2+
import {
3+
type VaporComponentInstance,
4+
getRootElement,
5+
isVaporComponent,
6+
} from '../component'
37

48
// !! vapor directive is different from vdom directives
59
export type VaporDirective = (
@@ -25,10 +29,20 @@ export function withVaporDirectives(
2529
node: Element | VaporComponentInstance,
2630
dirs: VaporDirectiveArguments,
2731
): void {
28-
// TODO handle custom directive on component
32+
const element = isVaporComponent(node) ? getRootElement(node.block) : node
33+
if (!element) {
34+
if (__DEV__) {
35+
warn(
36+
`Runtime directive used on component with non-element root node. ` +
37+
`The directives will not function as intended.`,
38+
)
39+
}
40+
return
41+
}
42+
2943
for (const [dir, value, argument, modifiers] of dirs) {
3044
if (dir) {
31-
const ret = dir(node, value, argument, modifiers)
45+
const ret = dir(element, value, argument, modifiers)
3246
if (ret) onScopeDispose(ret)
3347
}
3448
}

0 commit comments

Comments
 (0)