"import { createSlot as _createSlot } from 'vue';
export function render(_ctx) {
- const n0 = _createSlot("default", null, null, undefined, true)
+ const n0 = _createSlot("default", null, null, true)
return n0
}"
`;
nameExpr,
genRawProps(oper.props, context) || 'null',
fallbackArg,
- noSlotted && 'undefined', // instance
noSlotted && 'true', // noSlotted
),
)
transition: TransitionHooks,
): void
- vdomMount: (component: ConcreteComponent, props?: any, slots?: any) => any
+ vdomMount: (
+ component: ConcreteComponent,
+ parentComponent: any,
+ props?: any,
+ slots?: any,
+ ) => any
vdomUnmount: UnmountComponentFn
vdomSlot: (
slots: any,
} from '@vue/runtime-dom'
import {
createComponent,
+ createSlot,
createTextNode,
createVaporApp,
+ defineVaporComponent,
renderEffect,
+ withVaporCtx,
} from '../src'
import { makeRender } from './_utils'
import { setElementText } from '../src/dom/prop'
expect(host.innerHTML).toBe('')
})
+ it('should work with slots', () => {
+ const Parent = defineVaporComponent({
+ setup() {
+ provide('test', 'hello')
+ return createSlot('default', null)
+ },
+ })
+
+ const Child = defineVaporComponent({
+ setup() {
+ const test = inject('test')
+ return createTextNode(toDisplayString(test))
+ },
+ })
+
+ const { host } = define({
+ setup() {
+ return createComponent(Parent, null, {
+ default: withVaporCtx(() => createComponent(Child)),
+ })
+ },
+ }).render()
+
+ expect(host.innerHTML).toBe('hello<!--slot-->')
+ })
+
describe('hasInjectionContext', () => {
it('should be false outside of setup', () => {
expect(hasInjectionContext()).toBe(false)
setDynamicProps,
setText,
template,
+ withVaporCtx,
} from '../src'
import { nextTick, reactive, ref, watchEffect } from '@vue/runtime-dom'
import { makeRender } from './_utils'
inheritAttrs: false,
setup(_: any, { attrs }: any) {
const n0 = createComponent(Wrapper, null, {
- default: () => {
+ default: withVaporCtx(() => {
const n0 = template('<div>')() as HTMLDivElement
renderEffect(() => setDynamicProps(n0, [attrs]))
return n0
- },
+ }),
})
return n0
},
setProp,
setStyle,
template,
+ withVaporCtx,
} from '../src'
import { makeRender } from './_utils'
import { stringifyStyle } from '@vue/shared'
() => 'button',
null,
{
- default: () => {
+ default: withVaporCtx(() => {
const n0 = createSlot('default', null)
return n0
- },
+ }),
},
true,
)
const { host } = define(() => {
return createComponent(Comp, null, {
- header: () => template('header')(),
+ header: withVaporCtx(() => template('header')()),
})
}).render()
)
define(() =>
createComponent(Comp, null, {
- default: _props => ((props = _props), []),
+ default: withVaporCtx((_props: any) => ((props = _props), [])),
}),
).render()
)
define(() =>
createComponent(Comp, null, {
- default: _props => ((props = _props), []),
+ default: withVaporCtx((_props: any) => ((props = _props), [])),
}),
).render()
$: [
() => ({
name: 'header',
- fn: (props: any) => {
+ fn: withVaporCtx((props: any) => {
const el = template('<h1></h1>')()
renderEffect(() => {
setElementText(el, props.title)
})
return el
- },
+ }),
}),
],
})
const { host } = define(() => {
return createComponent(Comp, null, {
- header: () => template('header')(),
- footer: () => template('footer')(),
+ header: withVaporCtx(() => template('header')()),
+ footer: withVaporCtx(() => template('footer')()),
})
}).render()
$: [
() =>
flag1.value
- ? { name: 'one', fn: () => template('one content')() }
- : { name: 'two', fn: () => template('two content')() },
+ ? {
+ name: 'one',
+ fn: withVaporCtx(() => template('one content')()),
+ }
+ : {
+ name: 'two',
+ fn: withVaporCtx(() => template('two content')()),
+ },
],
})
}).render()
Child,
{},
{
- one: () => template('one content')(),
- two: () => template('two content')(),
+ one: withVaporCtx(() => template('one content')()),
+ two: withVaporCtx(() => template('two content')()),
},
)
}).render()
const { html } = define({
setup() {
return createComponent(Child, null, {
- default: () => {
+ default: withVaporCtx(() => {
return createIf(
() => toggle.value,
() => {
return document.createTextNode('content')
},
)
- },
+ }),
})
},
}).render()
const { html } = define({
setup() {
return createComponent(Child, null, {
- default: () => {
+ default: withVaporCtx(() => {
return createIf(
() => toggle.value,
() => {
return document.createTextNode('content')
},
)
- },
+ }),
})
},
}).render()
(toggle.value
? {
name: val.value,
- fn: () => {
+ fn: withVaporCtx(() => {
return template('<h1></h1>')()
- },
+ }),
}
: void 0) as DynamicSlot,
],
const { html } = define({
setup() {
return createComponent(Child, null, {
- default: () => {
+ default: withVaporCtx(() => {
return template('<!--comment-->')()
- },
+ }),
})
},
}).render()
const { html } = define({
setup() {
return createComponent(Child, null, {
- default: () => {
+ default: withVaporCtx(() => {
return createIf(
() => toggle.value,
() => {
return document.createTextNode('content')
},
)
- },
+ }),
})
},
}).render()
const { html } = define({
setup() {
return createComponent(Child, null, {
- default: () => {
+ default: withVaporCtx(() => {
return createIf(
() => outerShow.value,
() => {
)
},
)
- },
+ }),
})
},
}).render()
const { html } = define({
setup() {
return createComponent(Child, null, {
- default: () => {
+ default: withVaporCtx(() => {
const n2 = createFor(
() => items.value,
for_item0 => {
},
)
return n2
- },
+ }),
})
},
}).render()
const { html } = define({
setup() {
return createComponent(Child, null, {
- default: () => {
+ default: withVaporCtx(() => {
const n2 = createFor(
() => items.value,
for_item0 => {
},
)
return n2
- },
+ }),
})
},
}).render()
Parent,
null,
{
- foo: () => {
+ foo: withVaporCtx(() => {
const n0 = template(' ')() as any
renderEffect(() => setText(n0, foo.value))
return n0
- },
+ }),
},
true,
)
Parent,
null,
{
- foo: () => {
+ foo: withVaporCtx(() => {
const n0 = template(' ')() as any
renderEffect(() => setText(n0, foo.value))
return n0
- },
- default: () => {
+ }),
+ default: withVaporCtx(() => {
const n3 = template(' ')() as any
renderEffect(() => setText(n3, foo.value))
return n3
- },
+ }),
},
true,
)
const { html } = define({
setup() {
return createComponent(Parent, null, {
- default: () => template('<!-- <div>App</div> -->')(),
+ default: withVaporCtx(() => template('<!-- <div>App</div> -->')()),
})
},
}).render()
const { html } = define({
setup() {
return createComponent(Parent, null, {
- default: () => template('<!-- <div>App</div> -->')(),
+ default: withVaporCtx(() => template('<!-- <div>App</div> -->')()),
})
},
}).render()
expect(html()).toBe('child fallback<!--for--><!--slot--><!--slot-->')
})
+ test('consecutive slots with insertion state', async () => {
+ const { component: Child } = define({
+ setup() {
+ const n2 = template('<div><div>baz</div></div>', true)() as any
+ setInsertionState(n2, 0)
+ createSlot('default', null)
+ setInsertionState(n2, 0)
+ createSlot('foo', null)
+ return n2
+ },
+ })
+
+ const { html } = define({
+ setup() {
+ return createComponent(Child, null, {
+ default: withVaporCtx(() => template('default')()),
+ foo: withVaporCtx(() => template('foo')()),
+ })
+ },
+ }).render()
+
+ expect(html()).toBe(
+ `<div>` +
+ `default<!--slot-->` +
+ `foo<!--slot-->` +
+ `<div>baz</div>` +
+ `</div>`,
+ )
+ })
+
describe('vdom interop', () => {
const createVaporSlot = (fallbackText = 'fallback') => {
return defineVaporComponent({
expect(root.innerHTML).toBe('<span>bar</span>')
})
})
-
- test('consecutive slots with insertion state', async () => {
- const { component: Child } = define({
- setup() {
- const n2 = template('<div><div>baz</div></div>', true)() as any
- setInsertionState(n2, 0)
- createSlot('default', null)
- setInsertionState(n2, 0)
- createSlot('foo', null)
- return n2
- },
- })
-
- const { html } = define({
- setup() {
- return createComponent(Child, null, {
- default: () => template('default')(),
- foo: () => template('foo')(),
- })
- },
- }).render()
-
- expect(html()).toBe(
- `<div>` +
- `default<!--slot-->` +
- `foo<!--slot-->` +
- `<div>baz</div>` +
- `</div>`,
- )
- })
})
})
setValue,
template,
txt,
+ withVaporCtx,
} from '../src'
declare var __VUE_HMR_RUNTIME__: HMRRuntime
const App = {
setup() {
return createPlainElement('my-parent', null, {
- default: () =>
+ default: withVaporCtx(() =>
createPlainElement('my-child', null, {
- default: () => template('<span>default</span>')(),
+ default: withVaporCtx(() => template('<span>default</span>')()),
}),
+ ),
})
},
}
VaporTeleport,
{ to: () => target },
{
- default: () => createSlot('default'),
+ default: withVaporCtx(() => createSlot('default')),
},
)
},
const App = {
setup() {
return createPlainElement('my-el-teleport-parent', null, {
- default: () =>
+ default: withVaporCtx(() =>
createPlainElement('my-el-teleport-child', null, {
- default: () => template('<span>default</span>')(),
+ default: withVaporCtx(() => template('<span>default</span>')()),
}),
+ ),
})
},
}
VaporTeleport,
{ to: () => target1 },
{
- default: () => createSlot('header'),
+ default: withVaporCtx(() => createSlot('header')),
},
),
createComponent(
VaporTeleport,
{ to: () => target2 },
{
- default: () => createSlot('body'),
+ default: withVaporCtx(() => createSlot('body')),
},
),
]
const App = {
setup() {
return createPlainElement('my-el-two-teleport-child', null, {
- default: () => [
+ default: withVaporCtx(() => [
template('<div slot="header">header</div>')(),
template('<span slot="body">body</span>')(),
- ],
+ ]),
})
},
}
// with disabled: true
{ to: () => target1, disabled: () => true },
{
- default: () => createSlot('header'),
+ default: withVaporCtx(() => createSlot('header')),
},
),
createComponent(
VaporTeleport,
{ to: () => target2 },
{
- default: () => createSlot('body'),
+ default: withVaporCtx(() => createSlot('body')),
},
),
]
const App = {
setup() {
return createPlainElement('my-el-two-teleport-child-0', null, {
- default: () => [
+ default: withVaporCtx(() => [
template('<div slot="header">header</div>')(),
template('<span slot="body">body</span>')(),
- ],
+ ]),
})
},
}
const ChildWrapper = {
setup() {
return createPlainElement('my-el-child-shadow-false', null, {
- default: () => template('child')(),
+ default: withVaporCtx(() => template('child')()),
})
},
}
'my-el-parent-shadow-false',
{ isShown: () => props.isShown },
{
- default: () => createSlot('default'),
+ default: withVaporCtx(() => createSlot('default')),
},
)
},
ParentWrapper,
{ isShown: () => isShown.value },
{
- default: () => createComponent(ChildWrapper),
+ default: withVaporCtx(() => createComponent(ChildWrapper)),
},
)
},
insert,
renderEffect,
template,
+ withVaporCtx,
} from '../../src'
import { compile, makeRender, runtimeDom, runtimeVapor } from '../_utils'
import {
render() {
const setRef = createTemplateRefSetter()
const n0 = createComponent(Child, null, {
- default: () => {
+ default: withVaporCtx(() => {
n = document.createElement('div')
setRef(n, 'foo')
return n
- },
+ }),
})
return n0
},
setup() {
const setRef = createTemplateRefSetter()
const n0 = createComponent(Child, null, {
- default: () => {
+ default: withVaporCtx(() => {
n = document.createElement('div')
setRef(n, r)
return n
- },
+ }),
})
return n0
},
r = useTemplateRef('foo')
const setRef = createTemplateRefSetter()
const n0 = createComponent(Child, null, {
- default: () => {
+ default: withVaporCtx(() => {
n = document.createElement('div')
setRef(n, 'foo')
return n
- },
+ }),
})
return n0
},
}
export function setComponentScopeId(instance: VaporComponentInstance): void {
- const parent = instance.parent
- if (!parent) return
+ const { parent, scopeId } = instance
+ if (!parent || !scopeId) return
+
// prevent setting scopeId on multi-root fragments
if (isArray(instance.block) && instance.block.length > 1) return
const scopeIds: string[] = []
-
- const scopeId = parent.type.__scopeId
- if (scopeId) {
+ const parentScopeId = parent && parent.type.__scopeId
+ // if parent scopeId is different from scopeId, this means scopeId
+ // is inherited from slot owner, so we need to set it to the component
+ // scopeIds. the `parentScopeId-s` is handled in createSlot
+ if (parentScopeId !== scopeId) {
scopeIds.push(scopeId)
+ } else {
+ if (parentScopeId) scopeIds.push(parentScopeId)
}
// inherit scopeId from vdom parent
type StaticSlots,
type VaporSlot,
dynamicSlotsProxyHandlers,
+ getParentInstance,
getSlot,
+ setCurrentSlotConsumer,
} from './componentSlots'
import { hmrReload, hmrRerender } from './hmr'
import {
resetInsertionState()
}
+ const parentInstance = getParentInstance()
+
if (
isSingleRoot &&
component.inheritAttrs !== false &&
- isVaporComponent(currentInstance) &&
- currentInstance.hasFallthrough
+ isVaporComponent(parentInstance) &&
+ parentInstance.hasFallthrough
) {
// check if we are the single root of the parent
// if yes, inject parent attrs as dynamic props source
- const attrs = currentInstance.attrs
+ const attrs = parentInstance.attrs
if (rawProps) {
;((rawProps as RawProps).$ || ((rawProps as RawProps).$ = [])).push(
() => attrs,
}
// keep-alive
- if (
- currentInstance &&
- currentInstance.vapor &&
- isKeepAlive(currentInstance)
- ) {
- const cached = (currentInstance as KeepAliveInstance).getCachedComponent(
+ if (parentInstance && parentInstance.vapor && isKeepAlive(parentInstance)) {
+ const cached = (parentInstance as KeepAliveInstance).getCachedComponent(
component,
)
// @ts-expect-error
// vdom interop enabled and component is not an explicit vapor component
if (appContext.vapor && !component.__vapor) {
+ const prevSlotConsumer = setCurrentSlotConsumer(null)
const frag = appContext.vapor.vdomMount(
component as any,
+ parentInstance as any,
rawProps,
rawSlots,
)
-
+ setCurrentSlotConsumer(prevSlotConsumer)
if (!isHydrating) {
if (_insertionParent) insert(frag, _insertionParent, _insertionAnchor)
} else {
rawSlots as RawSlots,
appContext,
once,
+ parentInstance,
)
+ // set currentSlotConsumer to null to avoid affecting the child components
+ const prevSlotConsumer = setCurrentSlotConsumer(null)
+
// HMR
if (__DEV__ && component.__hmrId) {
registerHMR(instance)
setupComponent(instance, component)
}
+ // restore currentSlotConsumer to previous value after setupFn is called
+ setCurrentSlotConsumer(prevSlotConsumer)
onScopeDispose(() => unmountComponent(instance), true)
if (_insertionParent || isHydrating) {
if (isHydrating && _insertionAnchor !== undefined) {
advanceHydrationNode(_insertionParent!)
}
+
return instance
}
slots: StaticSlots
+ scopeId?: string | null
+
// to hold vnode props / slots in vdom interop mode
rawPropsRef?: ShallowRef<any>
rawSlotsRef?: ShallowRef<any>
rawSlots?: RawSlots | null,
appContext?: GenericAppContext,
once?: boolean,
+ parent: GenericComponentInstance | null = currentInstance,
) {
this.vapor = true
this.uid = nextUid()
this.type = comp
- this.parent = currentInstance
- this.root = currentInstance ? currentInstance.root : this
+ this.parent = parent
+ this.root = parent ? parent.root : this
- if (currentInstance) {
- this.appContext = currentInstance.appContext
- this.provides = currentInstance.provides
- this.ids = currentInstance.ids
+ if (parent) {
+ this.appContext = parent.appContext
+ this.provides = parent.provides
+ this.ids = parent.ids
} else {
this.appContext = appContext || emptyContext
this.provides = Object.create(this.appContext.provides)
: rawSlots
: EMPTY_OBJ
+ this.scopeId = currentInstance && currentInstance.type.__scopeId
+
// apply custom element special handling
if (comp.ce) {
comp.ce(this)
/**
* Current slot scopeIds for vdom interop
- * @internal
*/
export let currentSlotScopeIds: string[] | null = null
-/**
- * @internal
- */
-export function setCurrentSlotScopeIds(
- scopeIds: string[] | null,
-): string[] | null {
- const prev = currentSlotScopeIds
- currentSlotScopeIds = scopeIds
- return prev
+function setCurrentSlotScopeIds(scopeIds: string[] | null): string[] | null {
+ try {
+ return currentSlotScopeIds
+ } finally {
+ currentSlotScopeIds = scopeIds
+ }
}
export type RawSlots = Record<string, VaporSlot> & {
}
}
+export let currentSlotConsumer: GenericComponentInstance | null = null
+
+export function setCurrentSlotConsumer(
+ consumer: GenericComponentInstance | null,
+): GenericComponentInstance | null {
+ try {
+ return currentSlotConsumer
+ } finally {
+ currentSlotConsumer = consumer
+ }
+}
+
/**
- * Wraps a slot function to execute in the parent component's context.
- *
- * This ensures that:
- * 1. Reactive effects created inside the slot (e.g., `renderEffect`) bind to the
- * parent's instance, so the parent's lifecycle hooks fire when the slot's
- * reactive dependencies change.
- * 2. Elements created in the slot inherit the parent's scopeId for proper style
- * scoping in scoped CSS.
- *
- * **Rationale**: Slots are defined in the parent's template, so the parent should
- * own the rendering context and be aware of updates.
- *
+ * use currentSlotConsumer as parent, the currentSlotConsumer will be reset to null
+ * before setupFn call to avoid affecting children and restore to previous value
+ * after setupFn is called
+ */
+export function getParentInstance(): GenericComponentInstance | null {
+ return currentSlotConsumer || currentInstance
+}
+
+/**
+ * Wrap a slot function to memoize currentInstance
+ * 1. ensure correct currentInstance in forwarded slots
+ * 2. elements created in the slot inherit the slot owner's scopeId
*/
export function withVaporCtx(fn: Function): BlockFn {
- const instance = currentInstance as VaporComponentInstance
+ const owner = currentInstance
return (...args: any[]) => {
- const prev = setCurrentInstance(instance)
+ const prev = setCurrentInstance(owner)
+ const prevConsumer = setCurrentSlotConsumer(prev[0])
try {
return fn(...args)
} finally {
setCurrentInstance(...prev)
+ setCurrentSlotConsumer(prevConsumer)
}
}
}
// Calculate slotScopeIds once (for vdom interop)
const slotScopeIds: string[] = []
if (!noSlotted) {
- const scopeId = instance!.type.__scopeId
+ const scopeId = instance.type.__scopeId
if (scopeId) {
slotScopeIds.push(`${scopeId}-s`)
}
fragment.fallback = fallback
// Create and cache bound version of the slot to make it stable
// so that we avoid unnecessary updates if it resolves to the same slot
-
fragment.update(
slot._bound ||
(slot._bound = () => {
function createVDOMComponent(
internals: RendererInternals,
component: ConcreteComponent,
+ parentComponent: VaporComponentInstance | null,
rawProps?: LooseRawProps | null,
rawSlots?: LooseRawSlots | null,
): VaporFragment {
- const parentInstance = currentInstance as VaporComponentInstance
const frag = new VaporFragment([])
const vnode = (frag.vnode = createVNode(
component,
{ props: component.props },
rawProps as RawProps,
rawSlots as RawSlots,
+ parentComponent ? parentComponent.appContext : undefined,
+ undefined,
+ parentComponent,
)
// overwrite how the vdom instance handles props
if (vnode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
vdomDeactivate(
vnode,
- findParentKeepAlive(parentInstance)!.getStorageContainer(),
+ findParentKeepAlive(parentComponent!)!.getStorageContainer(),
internals,
- parentInstance as any,
+ parentComponent as any,
null,
)
return
}
frag.hydrate = () => {
- hydrateVNode(vnode, parentInstance as any)
+ hydrateVNode(vnode, parentComponent as any)
onScopeDispose(unmount, true)
isMounted = true
frag.nodes = vnode.el as any
}
- vnode.scopeId = parentInstance && parentInstance.type.__scopeId!
+ vnode.scopeId = (currentInstance && currentInstance.type.__scopeId) || null
vnode.slotScopeIds = currentSlotScopeIds
frag.insert = (parentNode, anchor, transition) => {
parentNode,
anchor,
internals,
- parentInstance as any,
+ parentComponent as any,
null,
undefined,
false,
)
} else {
const prev = currentInstance
- simpleSetCurrentInstance(parentInstance)
+ simpleSetCurrentInstance(parentComponent)
if (!isMounted) {
if (transition) setVNodeTransitionHooks(vnode, transition)
internals.mt(
vnode,
parentNode,
anchor,
- parentInstance as any,
+ parentComponent as any,
null,
undefined,
false,
parentNode,
anchor,
MoveType.REORDER,
- parentInstance as any,
+ parentComponent as any,
)
}
simpleSetCurrentInstance(prev)