diff --git a/packages/nuxt/src/app/components/client-only.ts b/packages/nuxt/src/app/components/client-only.ts index ab56eb4ab2..c65eec1b55 100644 --- a/packages/nuxt/src/app/components/client-only.ts +++ b/packages/nuxt/src/app/components/client-only.ts @@ -1,12 +1,14 @@ -import { cloneVNode, createElementBlock, createStaticVNode, defineComponent, getCurrentInstance, h, onMounted, provide, ref } from 'vue' +import { cloneVNode, createElementBlock, defineComponent, getCurrentInstance, h, onMounted, provide, ref } from 'vue' import type { ComponentInternalInstance, ComponentOptions, InjectionKey } from 'vue' import { isPromise } from '@vue/shared' import { useNuxtApp } from '../nuxt' -import { getFragmentHTML } from './utils' import ServerPlaceholder from './server-placeholder' +import { elToStaticVNode } from './utils' export const clientOnlySymbol: InjectionKey = Symbol.for('nuxt:client-only') +const STATIC_DIV = '
' + export default defineComponent({ name: 'ClientOnly', inheritAttrs: false, @@ -54,16 +56,14 @@ export function createClientOnly (component: T) { return (res.children === null || typeof res.children === 'string') ? cloneVNode(res) : h(res) - } else { - const fragment = getFragmentHTML(ctx._.vnode.el ?? null) ?? ['
'] - return createStaticVNode(fragment.join(''), fragment.length) } + return elToStaticVNode(ctx._.vnode.el, STATIC_DIV) } } else if (clone.template) { // handle runtime-compiler template clone.template = ` - + ` } @@ -105,10 +105,8 @@ export function createClientOnly (component: T) { return (res.children === null || typeof res.children === 'string') ? cloneVNode(res) : h(res) - } else { - const fragment = getFragmentHTML(instance?.vnode.el ?? null) ?? ['
'] - return createStaticVNode(fragment.join(''), fragment.length) } + return elToStaticVNode(instance?.vnode.el, STATIC_DIV) } }) } else { @@ -117,8 +115,7 @@ export function createClientOnly (component: T) { if (mounted$.value) { return h(setupState(...args), ctx.attrs) } - const fragment = getFragmentHTML(instance?.vnode.el ?? null) ?? ['
'] - return createStaticVNode(fragment.join(''), fragment.length) + return elToStaticVNode(instance?.vnode.el, STATIC_DIV) } } return Object.assign(setupState, { mounted$ }) diff --git a/packages/nuxt/src/app/components/utils.ts b/packages/nuxt/src/app/components/utils.ts index 5fe9739e31..7a38c6fdfc 100644 --- a/packages/nuxt/src/app/components/utils.ts +++ b/packages/nuxt/src/app/components/utils.ts @@ -1,5 +1,5 @@ -import { h } from 'vue' -import type { Component, RendererNode } from 'vue' +import { createStaticVNode, h } from 'vue' +import type { Component, RendererNode, VNode } from 'vue' // eslint-disable-next-line import { isString, isPromise, isArray, isObject } from '@vue/shared' import type { RouteLocationNormalized } from 'vue-router' @@ -117,9 +117,9 @@ export function vforToArray (source: any): any[] { * Handles `` Fragment elements * @param element the element to retrieve the HTML * @param withoutSlots purge all slots from the HTML string retrieved - * @returns {string[]} An array of string which represent the content of each element. Use `.join('')` to retrieve a component vnode.el HTML + * @returns {string[]|undefined} An array of string which represent the content of each element. Use `.join('')` to retrieve a component vnode.el HTML */ -export function getFragmentHTML (element: RendererNode | null, withoutSlots = false): string[] | null { +export function getFragmentHTML (element: RendererNode | null, withoutSlots = false): string[] | undefined { if (element) { if (element.nodeName === '#comment' && element.nodeValue === '[') { return getFragmentChildren(element, [], withoutSlots) @@ -131,7 +131,6 @@ export function getFragmentHTML (element: RendererNode | null, withoutSlots = fa } return [element.outerHTML] } - return null } function getFragmentChildren (element: RendererNode | null, blocks: string[] = [], withoutSlots = false) { @@ -151,6 +150,20 @@ function getFragmentChildren (element: RendererNode | null, blocks: string[] = [ return blocks } +/** + * Return a static vnode from an element + * Default to a div if the element is not found and if a fallback is not provided + * @param el renderer node retrieved from the component internal instance + * @param staticNodeFallback fallback string to use if the element is not found. Must be a valid HTML string + */ +export function elToStaticVNode (el: RendererNode | null, staticNodeFallback?: string): VNode { + const fragment: string[] | undefined = el ? getFragmentHTML(el) : staticNodeFallback ? [staticNodeFallback] : undefined + if (fragment) { + return createStaticVNode(fragment.join(''), fragment.length) + } + return h('div') +} + function isStartFragment (element: RendererNode) { return element.nodeName === '#comment' && element.nodeValue === '[' }