toReadonly,
watch,
} from '@vue/reactivity'
-import { isArray, isObject, isString } from '@vue/shared'
+import { FOR_ANCHOR_LABEL, isArray, isObject, isString } from '@vue/shared'
import { createComment, createTextNode } from './dom/node'
- import { type Block, insert, remove as removeBlock } from './block'
-import {
- type Block,
- ForFragment,
- VaporFragment,
- insert,
- remove,
- remove as removeBlock,
-} from './block'
++import { type Block, insert, remove, remove as removeBlock } from './block'
import { warn } from '@vue/runtime-dom'
import { currentInstance, isVaporComponent } from './component'
import type { DynamicSlot } from './componentSlots'
import { renderEffect } from './renderEffect'
import { VaporVForFlags } from '../../shared/src/vaporFlags'
-import { isHydrating, locateHydrationNode } from './dom/hydration'
+import {
+ currentHydrationNode,
+ isHydrating,
+ locateHydrationNode,
+ locateVaporFragmentAnchor,
+ updateNextChildToHydrate,
+} from './dom/hydration'
- import { VaporFragment } from './fragment'
++import { ForFragment, VaporFragment } from './fragment'
import {
insertionAnchor,
insertionParent,
let parent: ParentNode | undefined | null
// useSelector only
let currentKey: any
- // TODO handle this in hydration
- const parentAnchor = __DEV__ ? createComment('for') : createTextNode()
+ let parentAnchor: Node
+ if (isHydrating) {
+ parentAnchor = locateVaporFragmentAnchor(
+ currentHydrationNode!,
+ FOR_ANCHOR_LABEL,
+ )!
+ if (__DEV__ && !parentAnchor) {
+ // this should not happen
+ throw new Error(`v-for fragment anchor node was not found.`)
+ }
+ } else {
+ parentAnchor = __DEV__ ? createComment('for') : createTextNode()
+ }
+
- const frag = new VaporFragment(oldBlocks)
+ const frag = new ForFragment(oldBlocks)
const instance = currentInstance!
const canUseFastRemove = !!(flags & VaporVForFlags.FAST_REMOVE)
const isComponent = !!(flags & VaporVForFlags.IS_COMPONENT)
--- /dev/null
- export class VaporFragment implements TransitionOptions {
+import { EffectScope, setActiveSub } from '@vue/reactivity'
+import { createComment, createTextNode } from './dom/node'
+import {
+ type Block,
+ type BlockFn,
+ type TransitionOptions,
+ type VaporTransitionHooks,
+ insert,
+ isValidBlock,
+ remove,
+} from './block'
+import type { TransitionHooks } from '@vue/runtime-dom'
+import {
+ currentHydrationNode,
+ isComment,
+ isHydrating,
+ locateHydrationNode,
+ locateVaporFragmentAnchor,
+ setCurrentHydrationNode,
+} from './dom/hydration'
+import {
+ applyTransitionHooks,
+ applyTransitionLeaveHooks,
+} from './components/Transition'
+import type { VaporComponentInstance } from './component'
+
- nodes: Block
++export class VaporFragment<T extends Block = Block>
++ implements TransitionOptions
++{
+ $key?: any
+ $transition?: VaporTransitionHooks | undefined
- constructor(nodes: Block) {
++ nodes: T
+ anchor?: Node
+ insert?: (
+ parent: ParentNode,
+ anchor: Node | null,
+ transitionHooks?: TransitionHooks,
+ ) => void
+ remove?: (parent?: ParentNode, transitionHooks?: TransitionHooks) => void
+ fallback?: BlockFn
+
+ target?: ParentNode | null
+ targetAnchor?: Node | null
+ getNodes?: () => Block
+ setRef?: (comp: VaporComponentInstance) => void
+
- if (this.fallback && !isValidBlock(this.nodes)) {
- parent && remove(this.nodes, parent)
- this.nodes =
- (this.scope || (this.scope = new EffectScope())).run(this.fallback) ||
- []
- parent && insert(this.nodes, parent, this.anchor)
++ constructor(nodes: T) {
+ this.nodes = nodes
+ }
+}
+
+export class DynamicFragment extends VaporFragment {
+ anchor!: Node
+ scope: EffectScope | undefined
+ current?: BlockFn
+ fallback?: BlockFn
+ /**
+ * slot only
+ * indicates forwarded slot
+ */
+ forwarded?: boolean
+
+ constructor(anchorLabel?: string) {
+ super([])
+ if (isHydrating) {
+ locateHydrationNode(anchorLabel === 'slot')
+ this.hydrate(anchorLabel!)
+ } else {
+ this.anchor =
+ __DEV__ && anchorLabel ? createComment(anchorLabel) : createTextNode()
+ }
+ }
+
+ update(render?: BlockFn, key: any = render): void {
+ if (key === this.current) {
+ return
+ }
+ this.current = key
+
+ const prevSub = setActiveSub()
+ const parent = this.anchor.parentNode
+ const transition = this.$transition
+ const renderBranch = () => {
+ if (render) {
+ this.scope = new EffectScope()
+ this.nodes = this.scope.run(render) || []
+ if (transition) {
+ this.$transition = applyTransitionHooks(this.nodes, transition)
+ }
+ if (parent) insert(this.nodes, parent, this.anchor)
+ } else {
+ this.scope = undefined
+ this.nodes = []
+ }
+ }
+
+ // teardown previous branch
+ if (this.scope) {
+ this.scope.stop()
+ const mode = transition && transition.mode
+ if (mode) {
+ applyTransitionLeaveHooks(this.nodes, transition, renderBranch)
+ parent && remove(this.nodes, parent)
+ if (mode === 'out-in') {
+ setActiveSub(prevSub)
+ return
+ }
+ } else {
+ parent && remove(this.nodes, parent)
+ }
+ }
+
+ renderBranch()
+
++ if (this.fallback) {
++ // set fallback for nested fragments
++ const hasNestedFragment = isFragment(this.nodes)
++ if (hasNestedFragment) {
++ setFragmentFallback(this.nodes as VaporFragment, this.fallback)
++ }
++
++ const invalidFragment = findInvalidFragment(this)
++ if (invalidFragment) {
++ parent && remove(this.nodes, parent)
++ const scope = this.scope || (this.scope = new EffectScope())
++ scope.run(() => {
++ // for nested fragments, render invalid fragment's fallback
++ if (hasNestedFragment) {
++ renderFragmentFallback(invalidFragment)
++ } else {
++ this.nodes = this.fallback!() || []
++ }
++ })
++ parent && insert(this.nodes, parent, this.anchor)
++ }
+ }
+
+ if (isHydrating) {
+ setCurrentHydrationNode(this.anchor.nextSibling)
+ }
+ setActiveSub(prevSub)
+ }
+
+ hydrate(label: string): void {
+ // for `v-if="false"` the node will be an empty comment, use it as the anchor.
+ // otherwise, find next sibling vapor fragment anchor
+ if (label === 'if' && isComment(currentHydrationNode!, '')) {
+ this.anchor = currentHydrationNode
+ } else {
+ let anchor = locateVaporFragmentAnchor(currentHydrationNode!, label)!
+ if (!anchor && (label === 'slot' || label === 'if')) {
+ // fallback to fragment end anchor for ssr vdom slot
+ anchor = locateVaporFragmentAnchor(currentHydrationNode!, ']')!
+ }
+ if (anchor) {
+ this.anchor = anchor
+ } else if (__DEV__) {
+ // this should not happen
+ throw new Error(`${label} fragment anchor node was not found.`)
+ }
+ }
+ }
+}
+
++export class ForFragment extends VaporFragment<Block[]> {
++ constructor(nodes: Block[]) {
++ super(nodes)
++ }
++}
++
+export function isFragment(val: NonNullable<unknown>): val is VaporFragment {
+ return val instanceof VaporFragment
+}
++
++export function setFragmentFallback(
++ fragment: VaporFragment,
++ fallback: BlockFn,
++): void {
++ // stop recursion if fragment has its own fallback
++ if (fragment.fallback) return
++
++ fragment.fallback = fallback
++ if (isFragment(fragment.nodes)) {
++ setFragmentFallback(fragment.nodes, fallback)
++ }
++}
++
++function renderFragmentFallback(fragment: VaporFragment): void {
++ if (fragment instanceof ForFragment) {
++ fragment.nodes[0] = [fragment.fallback!() || []] as Block[]
++ } else if (fragment instanceof DynamicFragment) {
++ fragment.update(fragment.fallback)
++ } else {
++ // vdom slots
++ }
++}
++
++function findInvalidFragment(fragment: VaporFragment): VaporFragment | null {
++ if (isValidBlock(fragment.nodes)) return null
++
++ return isFragment(fragment.nodes)
++ ? findInvalidFragment(fragment.nodes) || fragment
++ : fragment
++}
import { type RawProps, rawPropsProxyHandlers } from './componentProps'
import type { RawSlots, VaporSlot } from './componentSlots'
import { renderEffect } from './renderEffect'
-import { createTextNode } from './dom/node'
+import { __next, createTextNode } from './dom/node'
import { optimizePropertyLookup } from './dom/prop'
- import { DynamicFragment, VaporFragment, isFragment } from './fragment'
+import { setTransitionHooks as setVaporTransitionHooks } from './components/Transition'
+import {
+ currentHydrationNode,
+ isHydrating,
+ locateHydrationNode,
+ locateVaporFragmentAnchor,
+ setCurrentHydrationNode,
+ hydrateNode as vaporHydrateNode,
+} from './dom/hydration'
++import { VaporFragment, isFragment, setFragmentFallback } from './fragment'
export const interopKey: unique symbol = Symbol(`interop`)
component: ConcreteComponent,
rawProps?: LooseRawProps | null,
rawSlots?: LooseRawSlots | null,
+ scopeId?: string,
): VaporFragment {
-- const frag = new VaporFragment([])
++ const frag = new VaporFragment([] as Block[])
const vnode = createVNode(
component,
- rawProps && new Proxy(rawProps, rawPropsProxyHandlers),
+ rawProps && extend({}, new Proxy(rawProps, rawPropsProxyHandlers)),
)
const wrapper = new VaporComponentInstance(
{ props: component.props },