--- /dev/null
+import { compile } from '../src'
+
+describe('transition', () => {
+ test('basic', () => {
+ expect(compile(`<transition><div>foo</div></transition>`).code)
+ .toMatchInlineSnapshot(`
+ "const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\")
+
+ return function ssrRender(_ctx, _push, _parent, _attrs) {
+ _push(\`<div\${_ssrRenderAttrs(_attrs)}>foo</div>\`)
+ }"
+ `)
+ })
+
+ test('with appear', () => {
+ expect(compile(`<transition appear><div>foo</div></transition>`).code)
+ .toMatchInlineSnapshot(`
+ "const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\")
+
+ return function ssrRender(_ctx, _push, _parent, _attrs) {
+ _push(\`<template><div\${_ssrRenderAttrs(_attrs)}>foo</div></template>\`)
+ }"
+ `)
+ })
+})
} from './ssrTransformTransitionGroup'
import { isSymbol, isObject, isArray } from '@vue/shared'
import { buildSSRProps } from './ssrTransformElement'
+import {
+ ssrProcessTransition,
+ ssrTransformTransition
+} from './ssrTransformTransition'
// We need to construct the slot functions in the 1st pass to ensure proper
// scope tracking, but the children of each slot cannot be processed until
if (isSymbol(component)) {
if (component === SUSPENSE) {
return ssrTransformSuspense(node, context)
- }
- if (component === TRANSITION_GROUP) {
+ } else if (component === TRANSITION_GROUP) {
return ssrTransformTransitionGroup(node, context)
+ } else if (component === TRANSITION) {
+ return ssrTransformTransition(node, context)
}
return // other built-in components: fallthrough
}
if ((parent as WIPSlotEntry).type === WIP_SLOT) {
context.pushStringPart(``)
}
- // #5351: filter out comment children inside transition
if (component === TRANSITION) {
- node.children = node.children.filter(c => c.type !== NodeTypes.COMMENT)
+ return ssrProcessTransition(node, context)
}
processChildren(node, context)
}
--- /dev/null
+import {
+ ComponentNode,
+ findProp,
+ NodeTypes,
+ TransformContext
+} from '@vue/compiler-dom'
+import { processChildren, SSRTransformContext } from '../ssrCodegenTransform'
+
+const wipMap = new WeakMap<ComponentNode, Boolean>()
+
+export function ssrTransformTransition(
+ node: ComponentNode,
+ context: TransformContext
+) {
+ return () => {
+ const appear = findProp(node, 'appear', false, true)
+ wipMap.set(node, !!appear)
+ }
+}
+
+export function ssrProcessTransition(
+ node: ComponentNode,
+ context: SSRTransformContext
+) {
+ // #5351: filter out comment children inside transition
+ node.children = node.children.filter(c => c.type !== NodeTypes.COMMENT)
+
+ const appear = wipMap.get(node)
+ if (appear) {
+ context.pushStringPart(`<template>`)
+ processChildren(node, context, false, true)
+ context.pushStringPart(`</template>`)
+ } else {
+ processChildren(node, context, false, true)
+ }
+}
createVNode,
withDirectives,
vModelCheckbox,
- renderSlot
+ renderSlot,
+ Transition,
+ createCommentVNode,
+ vShow
} from '@vue/runtime-dom'
import { renderToString, SSRContext } from '@vue/server-renderer'
-import { PatchFlags } from '../../shared/src'
+import { PatchFlags } from '@vue/shared'
+import { vShowOldKey } from '../../runtime-dom/src/directives/vShow'
function mountWithHydration(html: string, render: () => any) {
const container = document.createElement('div')
expect(`mismatch`).not.toHaveBeenWarned()
})
+ test('transition appear', () => {
+ const { vnode, container } = mountWithHydration(
+ `<template><div>foo</div></template>`,
+ () =>
+ h(
+ Transition,
+ { appear: true },
+ {
+ default: () => h('div', 'foo')
+ }
+ )
+ )
+ expect(container.firstChild).toMatchInlineSnapshot(`
+ <div
+ class="v-enter-from v-enter-active"
+ >
+ foo
+ </div>
+ `)
+ expect(vnode.el).toBe(container.firstChild)
+ expect(`mismatch`).not.toHaveBeenWarned()
+ })
+
+ test('transition appear with v-if', () => {
+ const show = false
+ const { vnode, container } = mountWithHydration(
+ `<template><!----></template>`,
+ () =>
+ h(
+ Transition,
+ { appear: true },
+ {
+ default: () => (show ? h('div', 'foo') : createCommentVNode(''))
+ }
+ )
+ )
+ expect(container.firstChild).toMatchInlineSnapshot('<!---->')
+ expect(vnode.el).toBe(container.firstChild)
+ expect(`mismatch`).not.toHaveBeenWarned()
+ })
+
+ test('transition appear with v-show', () => {
+ const show = false
+ const { vnode, container } = mountWithHydration(
+ `<template><div style="display: none;">foo</div></template>`,
+ () =>
+ h(
+ Transition,
+ { appear: true },
+ {
+ default: () =>
+ withDirectives(createVNode('div', null, 'foo'), [[vShow, show]])
+ }
+ )
+ )
+ expect(container.firstChild).toMatchInlineSnapshot(`
+ <div
+ class="v-enter-from v-enter-active"
+ style="display: none;"
+ >
+ foo
+ </div>
+ `)
+ expect((container.firstChild as any)[vShowOldKey]).toBe('')
+ expect(vnode.el).toBe(container.firstChild)
+ expect(`mismatch`).not.toHaveBeenWarned()
+ })
+
describe('mismatch handling', () => {
test('text node', () => {
const { container } = mountWithHydration(`foo`, () => 'bar')
import { invokeDirectiveHook } from './directives'
import { warn } from './warning'
import { PatchFlags, ShapeFlags, isReservedProp, isOn } from '@vue/shared'
-import { RendererInternals } from './renderer'
+import { needTransition, RendererInternals } from './renderer'
import { setRef } from './rendererTemplateRef'
import {
SuspenseImpl,
break
case Comment:
if (domType !== DOMNodeTypes.COMMENT || isFragmentStart) {
- nextNode = onMismatch()
+ if ((node as Element).tagName.toLowerCase() === 'template') {
+ const content = (vnode.el! as HTMLTemplateElement).content
+ .firstChild!
+
+ // replace <template> node with inner children
+ replaceNode(content, node, parentComponent)
+ vnode.el = node = content
+ nextNode = nextSibling(node)
+ } else {
+ nextNode = onMismatch()
+ }
} else {
nextNode = nextSibling(node)
}
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
if (
- domType !== DOMNodeTypes.ELEMENT ||
- (vnode.type as string).toLowerCase() !==
- (node as Element).tagName.toLowerCase()
+ (domType !== DOMNodeTypes.ELEMENT ||
+ (vnode.type as string).toLowerCase() !==
+ (node as Element).tagName.toLowerCase()) &&
+ !isTemplateNode(node as Element)
) {
nextNode = onMismatch()
} else {
// on its sub-tree.
vnode.slotScopeIds = slotScopeIds
const container = parentNode(node)!
- mountComponent(
- vnode,
- container,
- null,
- parentComponent,
- parentSuspense,
- isSVGContainer(container),
- optimized
- )
// Locate the next node.
if (isFragmentStart) {
nextNode = nextSibling(node)
}
+ mountComponent(
+ vnode,
+ container,
+ null,
+ parentComponent,
+ parentSuspense,
+ isSVGContainer(container),
+ optimized
+ )
+
// #3787
// if component is async, it may get moved / unmounted before its
// inner component is loaded, so we need to give it a placeholder
optimized: boolean
) => {
optimized = optimized || !!vnode.dynamicChildren
- const { type, props, patchFlag, shapeFlag, dirs } = vnode
+ const { type, props, patchFlag, shapeFlag, dirs, transition } = vnode
// #4006 for form elements with non-string v-model value bindings
// e.g. <option :value="obj">, <input type="checkbox" :true-value="1">
const forcePatchValue = (type === 'input' && dirs) || type === 'option'
if ((vnodeHooks = props && props.onVnodeBeforeMount)) {
invokeVNodeHook(vnodeHooks, parentComponent, vnode)
}
+
+ // handle appear transition
+ let needCallTransitionHooks = false
+ if (isTemplateNode(el)) {
+ needCallTransitionHooks =
+ needTransition(parentSuspense, transition) &&
+ parentComponent &&
+ parentComponent.vnode.props &&
+ parentComponent.vnode.props.appear
+
+ const content = (el as HTMLTemplateElement).content
+ .firstChild as Element
+
+ if (needCallTransitionHooks) {
+ transition!.beforeEnter(content)
+ }
+
+ // replace <template> node with inner children
+ replaceNode(content, el, parentComponent)
+ vnode.el = el = content
+ }
+
if (dirs) {
invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount')
}
- if ((vnodeHooks = props && props.onVnodeMounted) || dirs) {
+
+ if (
+ (vnodeHooks = props && props.onVnodeMounted) ||
+ dirs ||
+ needCallTransitionHooks
+ ) {
queueEffectWithSuspense(() => {
vnodeHooks && invokeVNodeHook(vnodeHooks, parentComponent, vnode)
+ needCallTransitionHooks && transition!.enter(el)
dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
}, parentSuspense)
}
return node
}
+ const replaceNode = (
+ newNode: Node,
+ oldNode: Node,
+ parentComponent: ComponentInternalInstance | null
+ ): void => {
+ // replace node
+ const parentNode = oldNode.parentNode
+ if (parentNode) {
+ parentNode.replaceChild(newNode, oldNode)
+ }
+
+ // update vnode
+ let parent = parentComponent
+ while (parent) {
+ if (parent.vnode.el === oldNode) {
+ parent.vnode.el = newNode
+ parent.subTree.el = newNode
+ }
+ parent = parent.parent
+ }
+ }
+
+ const isTemplateNode = (node: Element): boolean => {
+ return (
+ node.nodeType === DOMNodeTypes.ELEMENT &&
+ node.tagName.toLowerCase() === 'template'
+ )
+ }
+
return [hydrate, hydrateNode] as const
}
import { isAsyncWrapper } from './apiAsyncComponent'
import { isCompatEnabled } from './compat/compatConfig'
import { DeprecationTypes } from './compat/compatConfig'
+import { TransitionHooks } from './components/BaseTransition'
export interface Renderer<HostElement = RendererElement> {
render: RootRenderFunction<HostElement>
}
// #1583 For inside suspense + suspense not resolved case, enter hook should call when suspense resolved
// #1689 For inside suspense + suspense resolved case, just call it
- const needCallTransitionHooks =
- (!parentSuspense || (parentSuspense && !parentSuspense.pendingBranch)) &&
- transition &&
- !transition.persisted
+ const needCallTransitionHooks = needTransition(parentSuspense, transition)
if (needCallTransitionHooks) {
transition!.beforeEnter(el)
}
effect.allowRecurse = update.allowRecurse = allowed
}
+export function needTransition(
+ parentSuspense: SuspenseBoundary | null,
+ transition: TransitionHooks | null
+) {
+ return (
+ (!parentSuspense || (parentSuspense && !parentSuspense.pendingBranch)) &&
+ transition &&
+ !transition.persisted
+ )
+}
+
/**
* #1156
* When a component is HMR-enabled, we need to make sure that all static nodes