Skip to content

Commit

Permalink
feat: replace ClientOnly component with clientOnly function #82 (#110)
Browse files Browse the repository at this point in the history
* feat: replace `ClientOnly` component with `clientOnly` function with TS support #67, #82

BREAKING CHANGE: `ClientOnly` component is removed, please use the new `clientOnly` function to create components that only load and render on client side

* chore: some tests to be removed before merge

* minor

* simplify slots handling?

* always require argument to be a function returning a promise?

* fix: call alternate fallback slot function

* refactor slots definition

* swallow error when loading component and expose it as prop for fallback slot

* fix slots typing

* cleanup examples

* expose attrs to fallback slots so styles can be applied if required

* minor refactor

* let's change the docs URL after vike-react also makes the migration to a clientOnly() function

* small tweak of example - move timer to the slow loading component example

* more `clientOnly` under helpers

---------

Co-authored-by: Romuald Brillout <[email protected]>
  • Loading branch information
pdanpdan and brillout authored Jun 14, 2024
1 parent 3ab7600 commit d0b7a29
Show file tree
Hide file tree
Showing 8 changed files with 169 additions and 62 deletions.
49 changes: 49 additions & 0 deletions examples/full/components/Toggler.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<template>
<button type="button" @click="state.status = state.status !== true">
<template v-if="state.status !== null">
<slot name="prefix">State is </slot>
<slot :status="state.status">{{ state.status ? 'On' : 'Off' }}</slot>
</template>
<slot v-else name="fallback">No state</slot>
</button>
</template>

<script setup lang="ts">
import { reactive, watch } from 'vue'
const props = withDefaults(
defineProps<{
status?: boolean | null
}>(),
{
status: false,
},
)
const emit = defineEmits<{
toggle: [value: boolean | null]
}>()
defineSlots<{
default: { status: boolean }
prefix: {}
fallback: {}
}>()
const state = reactive({ status: false as boolean | null })
watch(
() => props.status,
(val) => {
state.status = val
},
{ immediate: true },
)
watch(
() => state.status,
(val) => {
emit('toggle', val)
},
)
</script>
76 changes: 62 additions & 14 deletions examples/full/pages/client-only/+Page.vue
Original file line number Diff line number Diff line change
@@ -1,24 +1,61 @@
<template>
<h1>ClientOnly</h1>
<h2>Basic Usage</h2>
<pre><code>{{ `<ClientOnly :load="() => import('../../components/Counter.vue')">
<pre><code>{{ `const ClientOnlyCounter = clientOnly(() => import('../../components/Counter.vue'))

<ClientOnlyCounter>
<template #fallback>
<p>Loading...</p>
</template>
</ClientOnly>
</ClientOnlyCounter>
` }}</code></pre>
<p>Time until load: {{ timeLeft / 1000 }}s</p>

<h2>Demo</h2>
<ClientOnly :load="load">

<h3>Basic example</h3>
<ClientOnlyCounter>
<template #fallback>
<p>Loading...</p>
<p style="min-height: 21px">Fast loading counter...</p>
</template>
</ClientOnlyCounter>

<h3>Slow loading component</h3>
<SlowClientOnlyToggler :status="null" @toggle="onToggle" style="color: green; min-height: 32px">
<!-- if the component uses the #fallback slot you can use #client-only-fallback -->
<template #client-only-fallback="{ attrs }">
<p :style="attrs.style">Slow loading toggler...Time until load: {{ timeLeft / 1000 }}s</p>
</template>

<template #fallback>Buton is in limbo</template>

<template #prefix>Button is </template>

<template #="{ status }">{{ status ? 'pressed' : 'depressed :)' }}</template>
</SlowClientOnlyToggler>

<h3>Handling errors when loading</h3>
<ErrorClientOnlyToggler>
<!-- handling errors using the #fallback / #client-only-fallback slot -->
<template #client-only-fallback="{ error }">
<p v-if="!error">Trying to load toggler...</p>
<p v-else style="color: red">{{ error.message }}</p>
</template>
</ClientOnly>
</ErrorClientOnlyToggler>

<h3>Nothing rendered on server</h3>
<ClientOnlyCounter />

<h3>Nothing rendered on server when component uses #fallback slot</h3>
<FastClientOnlyToggler :status="null">
<template #client-only-fallback></template>

<template #fallback>Buton is in limbo</template>
</FastClientOnlyToggler>
</template>

<script lang="ts" setup>
import { ref, watchEffect } from 'vue'
import ClientOnly from 'vike-vue/ClientOnly'
import { clientOnly } from 'vike-vue/clientOnly'
const delay = 3000
Expand All @@ -34,13 +71,24 @@ watchEffect(() => {
}
})
const load = () =>
new Promise((resolve) =>
setTimeout(async () => {
const Counter = await import('../../components/Counter.vue')
resolve(Counter)
}, delay),
)
const onToggle = <T>(val: T) => {
console.log('Toggled value:', val)
}
const ClientOnlyCounter = clientOnly(() => import('../../components/Counter.vue'))
const SlowClientOnlyToggler = clientOnly(async () => {
await new Promise((resolve) => {
setTimeout(resolve, delay)
})
return import('../../components/Toggler.vue')
})
const FastClientOnlyToggler = clientOnly(() => import('../../components/Toggler.vue'))
const ErrorClientOnlyToggler = clientOnly(() => {
throw new Error('The Toggler does not like to be loaded')
})
</script>

<style scoped>
Expand Down
2 changes: 1 addition & 1 deletion examples/full/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Full-fledged example of using `vike-vue`, showcasing:
- [Layout](https://vike.dev/Layout)
- Fetching data with [`data()`](https://vike.dev/data)
- [Nested Layout](https://vike.dev/Layout#nested-layouts)
- [`<ClientOnly>`](https://vike.dev/ClientOnly)
- [`clientOnly`](https://vike.dev/ClientOnly)
- [Toggling SSR](https://vike.dev/ssr) on a per-page basis.
- [Markdown](https://vike.dev/markdown)
- [Route Function](https://vike.dev/route-function)
Expand Down
6 changes: 3 additions & 3 deletions packages/vike-vue/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,11 @@ All hooks are [cumulative](https://vike.dev/meta#api), so you can add your own h
* [`usePageContext()`](https://vike.dev/usePageContext): Access the [`pageContext` object](https://vike.dev/pageContext)
from any component.

## Components
## Utilities

`vike-vue` introduces the following new components:
`vike-vue` introduces the following new utility functions:

* [`ClientOnly`](https://vike.dev/ClientOnly): Wrapper to render and load a component only on the client-side.
* [`clientOnly`](https://vike.dev/ClientOnly): Creates a wrapper component to load and render a component only on the client-side.

## Teleports

Expand Down
11 changes: 4 additions & 7 deletions packages/vike-vue/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,7 @@
"./renderer/onRenderClient": "./dist/renderer/onRenderClient.js",
"./usePageContext": "./dist/hooks/usePageContext.js",
"./useData": "./dist/hooks/useData.js",
"./ClientOnly": {
"default": "./dist/components/ClientOnly.js",
"types": "./dist/components/ClientOnly.vue.d.ts"
},
"./clientOnly": "./dist/helpers/clientOnly.js",
"./types": {
"default": "./dist/types/index.js",
"types": "./dist/types/index.d.ts"
Expand Down Expand Up @@ -60,14 +57,14 @@
"useData": [
"./dist/hooks/useData.d.ts"
],
"clientOnly": [
"./dist/helpers/clientOnly.d.ts"
],
"renderer/onRenderHtml": [
"./dist/renderer/onRenderHtml.d.ts"
],
"renderer/onRenderClient": [
"./dist/renderer/onRenderClient.d.ts"
],
"ClientOnly": [
"./dist/components/ClientOnly.vue.d.ts"
]
}
},
Expand Down
36 changes: 0 additions & 36 deletions packages/vike-vue/src/components/ClientOnly.vue

This file was deleted.

49 changes: 49 additions & 0 deletions packages/vike-vue/src/helpers/clientOnly.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
export { clientOnly }

import { h, nextTick, shallowRef, defineComponent, onBeforeMount } from 'vue'
import type { Component, SlotsType } from 'vue'

function clientOnly<ComponentLoaded extends Component>(
load: () => Promise<ComponentLoaded | { default: ComponentLoaded }>,
) {
const componentWrapper = defineComponent({
inheritAttrs: false,

setup(_, { attrs, slots }) {
const componentLoaded = shallowRef<ComponentLoaded | null>(null)
const error = shallowRef<unknown>(null)

onBeforeMount(async () => {
try {
const ret = await load()
componentLoaded.value = 'default' in ret ? ret.default : ret
} catch (e) {
console.error('Error while loading clientOnly() component:', e)
// wait for nextTick to avoid hydration errors
nextTick(() => {
error.value = e
})
}
})

return () => {
if (componentLoaded.value !== null) {
return h(componentLoaded.value, attrs, slots)
}
if (slots['client-only-fallback']) {
return slots['client-only-fallback']({ error: error.value, attrs })
}
// if the user doesn't want clientOnly() to use <template #fallback> then he should define a (empty) <template #client-only-fallback>
if (slots['fallback']) {
return slots['fallback']({ error: error.value, attrs })
}
}
},

slots: {} as SlotsType<{
fallback: { error: unknown; attrs: Record<string, any> }
'client-only-fallback': { error: unknown; attrs: Record<string, any> }
}>,
})
return componentWrapper as typeof componentWrapper & ComponentLoaded
}
2 changes: 1 addition & 1 deletion packages/vike-vue/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ export default defineConfig({
['+config']: resolve(__dirname, './src/+config.ts'),
['renderer/onRenderClient']: resolve(__dirname, './src/renderer/onRenderClient.ts'),
['renderer/onRenderHtml']: resolve(__dirname, './src/renderer/onRenderHtml.ts'),
['helpers/clientOnly']: resolve(__dirname, './src/helpers/clientOnly.ts'),
['types/index']: resolve(__dirname, './src/types/index.ts'),
['hooks/usePageContext']: resolve(__dirname, './src/hooks/usePageContext.ts'),
['hooks/useData']: resolve(__dirname, './src/hooks/useData.ts'),
['components/ClientOnly']: resolve(__dirname, './src/components/ClientOnly.vue'),
},
formats: ['es'],
},
Expand Down

0 comments on commit d0b7a29

Please sign in to comment.