tempId: number
effect: IREffect[]
operation: OperationNode[]
- expressions: SimpleExpressionNode[]
returns: number[]
+ hasDeferredVShow: boolean
}
export interface RootIRNode {
effect: [],
operation: [],
returns: [],
- expressions: [],
tempId: 0,
+ hasDeferredVShow: false,
})
export function wrapTemplate(node: ElementNode, dirs: string[]): TemplateNode {
type SlotBlockIRNode,
type VaporDirectiveNode,
} from '../ir'
-import { findDir, resolveExpression } from '../utils'
+import {
+ findDir,
+ findProp,
+ isTransitionNode,
+ resolveExpression,
+} from '../utils'
+ import { markNonTemplate } from './transformText'
export const transformVSlot: NodeTransform = (node, context) => {
if (node.type !== NodeTypes.ELEMENT) return
) {
const { children } = node
const arg = dir && dir.arg
- const nonSlotTemplateChildren = children.filter(
- n =>
- isNonWhitespaceContent(node) &&
- !(n.type === NodeTypes.ELEMENT && n.props.some(isVSlot)),
- )
-
+ // whitespace: 'preserve'
+ const emptyTextNodes: TemplateChildNode[] = []
+ const nonSlotTemplateChildren = children.filter(n => {
+ if (isNonWhitespaceContent(n)) {
+ return !(n.type === NodeTypes.ELEMENT && n.props.some(isVSlot))
+ } else {
+ emptyTextNodes.push(n)
+ }
+ })
+ if (!nonSlotTemplateChildren.length) {
+ emptyTextNodes.forEach(n => {
+ markNonTemplate(n, context)
+ })
+ }
- const [block, onExit] = createSlotBlock(node, dir, context)
+ let slotKey
- if (isTransitionNode(node)) {
++ if (isTransitionNode(node) && nonSlotTemplateChildren.length) {
+ const keyProp = findProp(
+ nonSlotTemplateChildren[0] as ElementNode,
+ 'key',
+ ) as VaporDirectiveNode
+ if (keyProp) {
+ slotKey = keyProp.exp
+ }
+ }
+
+ const [block, onExit] = createSlotBlock(node, dir, context, slotKey)
const { slots } = context
* @internal
*/
export { initFeatureFlags } from './featureFlags'
+/**
+ * @internal
+ */
+export { performTransitionEnter, performTransitionLeave } from './renderer'
+/**
+ * @internal
+ */
+export { ensureVaporSlotFallback } from './helpers/renderSlot'
+ /**
+ * @internal
+ */
+ export { createInternalObject } from './internalObject'
mount()
expect(html()).toBe('<div><button>hi</button></div>')
})
+
+ test('switch dynamic component children', async () => {
+ const CompA = defineVaporComponent({
+ setup() {
+ return template('<div>A</div>')()
+ },
+ })
+ const CompB = defineVaporComponent({
+ setup() {
+ return template('<div>B</div>')()
+ },
+ })
+
+ const current = shallowRef(CompA)
+ const { html } = define({
+ setup() {
+ const t1 = template('<div></div>')
+ const n2 = t1() as any
+ setInsertionState(n2)
+ createDynamicComponent(() => current.value)
+ return n2
+ },
+ }).render()
+
+ expect(html()).toBe('<div><div>A</div><!--dynamic-component--></div>')
+
+ current.value = CompB
+ await nextTick()
+ expect(html()).toBe('<div><div>B</div><!--dynamic-component--></div>')
+ })
++
++ test('render fallback with insertionState', async () => {
++ const { html, mount } = define({
++ setup() {
++ const html = ref('hi')
++ const n1 = template('<div></div>', true)() as any
++ setInsertionState(n1)
++ const n0 = createComponentWithFallback(
++ resolveDynamicComponent('button') as any,
++ ) as any
++ renderEffect(() => setHtml(n0, html.value))
++ return n1
++ },
++ }).create()
++
++ mount()
++ expect(html()).toBe('<div><button>hi</button></div>')
++ })
})
insertionParent,
resetInsertionState,
} from './insertionState'
- import { isHydrating } from './dom/hydration'
+import { DYNAMIC_COMPONENT_ANCHOR_LABEL } from '@vue/shared'
+ import { isHydrating, locateHydrationNode } from './dom/hydration'
export function createDynamicComponent(
getter: () => any,
): VaporFragment {
const _insertionParent = insertionParent
const _insertionAnchor = insertionAnchor
- if (!isHydrating) resetInsertionState()
+ if (isHydrating) {
+ locateHydrationNode()
+ } else {
+ resetInsertionState()
+ }
- const frag = __DEV__
- ? new DynamicFragment('dynamic-component')
- : new DynamicFragment()
+ const frag =
+ isHydrating || __DEV__
+ ? new DynamicFragment(DYNAMIC_COMPONENT_ANCHOR_LABEL)
+ : new DynamicFragment()
+
renderEffect(() => {
const value = getter()
+ const appContext =
+ (currentInstance && currentInstance.appContext) || emptyContext
frag.update(
() =>
createComponentWithFallback(
type App,
type ComponentInternalInstance,
type ConcreteComponent,
+ type HydrationRenderer,
MoveType,
type Plugin,
+ type RendererElement,
type RendererInternals,
+ type RendererNode,
type ShallowRef,
type Slots,
+ type TransitionHooks,
type VNode,
type VaporInteropInterface,
+ createInternalObject,
createVNode,
currentInstance,
+ ensureHydrationRenderer,
ensureRenderer,
+ ensureVaporSlotFallback,
+ isEmitListener,
+ isVNode,
onScopeDispose,
renderSlot,
+ setTransitionHooks as setVNodeTransitionHooks,
+ shallowReactive,
shallowRef,
simpleSetCurrentInstance,
} from '@vue/runtime-dom'
// overwrite how the vdom instance handles props
vnode.vi = (instance: ComponentInternalInstance) => {
- instance.props = wrapper.props
+ // Ensure props are shallow reactive to align with VDOM behavior.
+ // This enables direct watching of props and prevents DEV warnings.
+ //
+ // Example:
+ // const props = defineProps(...)
+ // watch(props, () => { ... }) // props must be reactive for this to work
+ //
+ // Without reactivity, Vue will warn in DEV about non-reactive watch sources
+ instance.props = shallowReactive(wrapper.props)
- instance.attrs = wrapper.attrs
+
+ const attrs = (instance.attrs = createInternalObject())
+ for (const key in wrapper.attrs) {
+ if (!isEmitListener(instance.emitsOptions, key)) {
+ attrs[key] = wrapper.attrs[key]
+ }
+ }
+
instance.slots =
wrapper.slots === EMPTY_OBJ
? EMPTY_OBJ