2025-01-05 14:35:59 +00:00
|
|
|
import { cloneVNode, createElementBlock, defineComponent, getCurrentInstance, h, onMounted, provide, ref } from 'vue'
|
2024-01-30 09:10:13 +00:00
|
|
|
import type { ComponentInternalInstance, ComponentOptions, InjectionKey } from 'vue'
|
2024-08-12 08:37:43 +00:00
|
|
|
import { isPromise } from '@vue/shared'
|
2024-02-09 20:33:35 +00:00
|
|
|
import { useNuxtApp } from '../nuxt'
|
2024-08-16 10:21:58 +00:00
|
|
|
import ServerPlaceholder from './server-placeholder'
|
2025-01-05 14:35:59 +00:00
|
|
|
import { elToStaticVNode } from './utils'
|
2021-11-15 11:57:38 +00:00
|
|
|
|
2024-01-30 09:10:13 +00:00
|
|
|
export const clientOnlySymbol: InjectionKey<boolean> = Symbol.for('nuxt:client-only')
|
|
|
|
|
2025-01-05 14:35:59 +00:00
|
|
|
const STATIC_DIV = '<div></div>'
|
|
|
|
|
2021-11-15 11:57:38 +00:00
|
|
|
export default defineComponent({
|
|
|
|
name: 'ClientOnly',
|
2022-11-14 10:27:57 +00:00
|
|
|
inheritAttrs: false,
|
2024-04-05 18:08:32 +00:00
|
|
|
|
2021-11-15 11:57:38 +00:00
|
|
|
props: ['fallback', 'placeholder', 'placeholderTag', 'fallbackTag'],
|
2025-01-20 06:47:51 +00:00
|
|
|
setup (props, { slots, attrs }) {
|
2021-11-15 11:57:38 +00:00
|
|
|
const mounted = ref(false)
|
|
|
|
onMounted(() => { mounted.value = true })
|
2024-02-09 20:33:35 +00:00
|
|
|
// Bail out of checking for pages/layouts as they might be included under `<ClientOnly>` 🤷♂️
|
|
|
|
if (import.meta.dev) {
|
|
|
|
const nuxtApp = useNuxtApp()
|
|
|
|
nuxtApp._isNuxtPageUsed = true
|
|
|
|
nuxtApp._isNuxtLayoutUsed = true
|
|
|
|
}
|
2024-01-30 09:10:13 +00:00
|
|
|
provide(clientOnlySymbol, true)
|
2025-01-20 06:47:51 +00:00
|
|
|
return () => {
|
2021-11-15 11:57:38 +00:00
|
|
|
if (mounted.value) { return slots.default?.() }
|
|
|
|
const slot = slots.fallback || slots.placeholder
|
2025-01-20 06:47:51 +00:00
|
|
|
if (slot) { return h(slot) }
|
2021-11-15 11:57:38 +00:00
|
|
|
const fallbackStr = props.fallback || props.placeholder || ''
|
|
|
|
const fallbackTag = props.fallbackTag || props.placeholderTag || 'span'
|
2022-11-14 10:27:57 +00:00
|
|
|
return createElementBlock(fallbackTag, attrs, fallbackStr)
|
2021-11-15 11:57:38 +00:00
|
|
|
}
|
2024-04-05 18:08:32 +00:00
|
|
|
},
|
2021-11-15 11:57:38 +00:00
|
|
|
})
|
2022-04-19 19:13:55 +00:00
|
|
|
|
2022-09-07 08:31:11 +00:00
|
|
|
const cache = new WeakMap()
|
|
|
|
|
2024-03-09 06:48:15 +00:00
|
|
|
/* @__NO_SIDE_EFFECTS__ */
|
2023-04-20 21:47:56 +00:00
|
|
|
export function createClientOnly<T extends ComponentOptions> (component: T) {
|
2024-08-16 10:21:58 +00:00
|
|
|
if (import.meta.server) {
|
|
|
|
return ServerPlaceholder
|
|
|
|
}
|
2022-09-07 08:31:11 +00:00
|
|
|
if (cache.has(component)) {
|
|
|
|
return cache.get(component)
|
|
|
|
}
|
|
|
|
|
2022-09-06 07:40:25 +00:00
|
|
|
const clone = { ...component }
|
|
|
|
|
|
|
|
if (clone.render) {
|
2024-08-12 08:37:43 +00:00
|
|
|
// override the component render (non script setup component) or dev mode
|
2024-01-02 21:04:58 +00:00
|
|
|
clone.render = (ctx: any, cache: any, $props: any, $setup: any, $data: any, $options: any) => {
|
2024-08-16 10:21:58 +00:00
|
|
|
if ($setup.mounted$ ?? ctx.mounted$) {
|
2024-01-02 21:04:58 +00:00
|
|
|
const res = component.render?.bind(ctx)(ctx, cache, $props, $setup, $data, $options)
|
2022-10-03 14:14:55 +00:00
|
|
|
return (res.children === null || typeof res.children === 'string')
|
2024-01-02 21:04:58 +00:00
|
|
|
? cloneVNode(res)
|
2022-10-03 14:14:55 +00:00
|
|
|
: h(res)
|
|
|
|
}
|
2025-01-05 14:35:59 +00:00
|
|
|
return elToStaticVNode(ctx._.vnode.el, STATIC_DIV)
|
2022-08-02 15:05:02 +00:00
|
|
|
}
|
2025-02-16 13:31:49 +00:00
|
|
|
} else {
|
2022-08-02 15:05:02 +00:00
|
|
|
// handle runtime-compiler template
|
2025-02-16 13:31:49 +00:00
|
|
|
clone.template &&= `
|
2022-09-06 07:40:25 +00:00
|
|
|
<template v-if="mounted$">${component.template}</template>
|
2025-01-05 14:35:59 +00:00
|
|
|
<template v-else>${STATIC_DIV}</template>
|
2022-08-02 15:05:02 +00:00
|
|
|
`
|
|
|
|
}
|
|
|
|
|
2022-09-06 07:40:25 +00:00
|
|
|
clone.setup = (props, ctx) => {
|
2024-08-12 08:37:43 +00:00
|
|
|
const nuxtApp = useNuxtApp()
|
2024-08-16 10:21:58 +00:00
|
|
|
const mounted$ = ref(nuxtApp.isHydrating === false)
|
2023-10-16 13:09:54 +00:00
|
|
|
const instance = getCurrentInstance()!
|
|
|
|
|
2024-08-16 10:21:58 +00:00
|
|
|
if (nuxtApp.isHydrating) {
|
2024-08-12 08:37:43 +00:00
|
|
|
const attrs = { ...instance.attrs }
|
|
|
|
// remove existing directives during hydration
|
|
|
|
const directives = extractDirectives(instance)
|
|
|
|
// prevent attrs inheritance since a staticVNode is rendered before hydration
|
|
|
|
for (const key in attrs) {
|
|
|
|
delete instance.attrs[key]
|
|
|
|
}
|
2024-01-23 10:22:45 +00:00
|
|
|
|
2024-08-12 08:37:43 +00:00
|
|
|
onMounted(() => {
|
|
|
|
Object.assign(instance.attrs, attrs)
|
|
|
|
instance.vnode.dirs = directives
|
|
|
|
})
|
2024-01-23 10:22:45 +00:00
|
|
|
}
|
2023-10-16 13:09:54 +00:00
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
mounted$.value = true
|
|
|
|
})
|
2024-08-12 08:37:43 +00:00
|
|
|
const setupState = component.setup?.(props, ctx) || {}
|
2022-09-06 07:40:25 +00:00
|
|
|
|
2024-08-12 08:37:43 +00:00
|
|
|
if (isPromise(setupState)) {
|
|
|
|
return Promise.resolve(setupState).then((setupState) => {
|
2024-01-02 21:04:58 +00:00
|
|
|
if (typeof setupState !== 'function') {
|
2025-01-14 17:36:18 +00:00
|
|
|
setupState ||= {}
|
2024-01-02 21:04:58 +00:00
|
|
|
setupState.mounted$ = mounted$
|
|
|
|
return setupState
|
|
|
|
}
|
|
|
|
return (...args: any[]) => {
|
2024-08-16 10:21:58 +00:00
|
|
|
if (mounted$.value || !nuxtApp.isHydrating) {
|
2024-01-02 21:04:58 +00:00
|
|
|
const res = setupState(...args)
|
|
|
|
return (res.children === null || typeof res.children === 'string')
|
|
|
|
? cloneVNode(res)
|
|
|
|
: h(res)
|
|
|
|
}
|
2025-01-05 14:35:59 +00:00
|
|
|
return elToStaticVNode(instance?.vnode.el, STATIC_DIV)
|
2024-01-02 21:04:58 +00:00
|
|
|
}
|
2022-09-06 07:40:25 +00:00
|
|
|
})
|
2024-08-12 08:37:43 +00:00
|
|
|
} else {
|
|
|
|
if (typeof setupState === 'function') {
|
|
|
|
return (...args: any[]) => {
|
|
|
|
if (mounted$.value) {
|
|
|
|
return h(setupState(...args), ctx.attrs)
|
|
|
|
}
|
2025-01-05 14:35:59 +00:00
|
|
|
return elToStaticVNode(instance?.vnode.el, STATIC_DIV)
|
2024-08-12 08:37:43 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return Object.assign(setupState, { mounted$ })
|
|
|
|
}
|
2022-09-06 07:40:25 +00:00
|
|
|
}
|
|
|
|
|
2022-09-07 08:31:11 +00:00
|
|
|
cache.set(component, clone)
|
|
|
|
|
2022-09-06 07:40:25 +00:00
|
|
|
return clone
|
2022-04-19 19:13:55 +00:00
|
|
|
}
|
2023-10-16 13:09:54 +00:00
|
|
|
|
|
|
|
function extractDirectives (instance: ComponentInternalInstance | null) {
|
|
|
|
if (!instance || !instance.vnode.dirs) { return null }
|
|
|
|
const directives = instance.vnode.dirs
|
|
|
|
instance.vnode.dirs = null
|
|
|
|
return directives
|
|
|
|
}
|