mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-25 07:05:11 +00:00
feat(nuxt): usePreviewMode
composable (#21705)
This commit is contained in:
parent
f0442d0ddb
commit
98aa2c222f
81
docs/3.api/2.composables/use-preview-mode.md
Normal file
81
docs/3.api/2.composables/use-preview-mode.md
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
---
|
||||||
|
title: "usePreviewMode"
|
||||||
|
description: "Use usePreviewMode to check and control preview mode in Nuxt"
|
||||||
|
---
|
||||||
|
|
||||||
|
# `usePreviewMode`
|
||||||
|
|
||||||
|
You can use the built-in `usePreviewMode` composable to access and control preview state in Nuxt. If the composable detects preview mode it will automatically force any updates necessary for [`useAsyncData`](/docs/api/composables/use-async-data) and [`useFetch`](/docs/api/composables/use-fetch) to rerender preview content.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { enabled, state } = usePreviewMode()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
### Custom `enable` check
|
||||||
|
|
||||||
|
You can specify a custom way to enable preview mode. By default the `usePreviewMode` composable will enable preview mode if there is a `preview` param in url that is equal to `true` (for example, `http://localhost:3000?preview=true`). You can wrap the `usePreviewMode` into custom composable, to keep options consistent across usages and prevent any errors.
|
||||||
|
|
||||||
|
```js
|
||||||
|
export function useMyPreviewMode () {
|
||||||
|
return usePreviewMode({
|
||||||
|
shouldEnable: () => {
|
||||||
|
return !!route.query.customPreview
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}```
|
||||||
|
|
||||||
|
### Modify default state
|
||||||
|
|
||||||
|
`usePreviewMode` will try to store the value of a `token` param from url in state. You can modify this state and it will be available for all [`usePreviewMode`](/docs/api/composables/use-preview-mode) calls.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const data1 = ref('data1')
|
||||||
|
|
||||||
|
const { enabled, state } = usePreviewMode({
|
||||||
|
getState: (currentState) => {
|
||||||
|
return { data1, data2: 'data2' }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
::alert{icon=👉}
|
||||||
|
The `getState` function will append returned values to current state, so be careful not to accidentally overwrite important state.
|
||||||
|
::
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```vue [pages/some-page.vue]
|
||||||
|
<script setup>
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const { enabled, state } = usePreviewMode({
|
||||||
|
shouldEnable: () => {
|
||||||
|
return route.query.customPreview === 'true'
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { data } = await useFetch('/api/preview', {
|
||||||
|
query: {
|
||||||
|
apiKey: state.token
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
Some base content
|
||||||
|
|
||||||
|
<p v-if="enabled">
|
||||||
|
Only preview content: {{ state.token }}
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<button @click="enabled = false">
|
||||||
|
disable preview mode
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
@ -35,4 +35,5 @@ export type { NuxtAppManifest, NuxtAppManifestMeta } from './manifest'
|
|||||||
export type { ReloadNuxtAppOptions } from './chunk'
|
export type { ReloadNuxtAppOptions } from './chunk'
|
||||||
export { reloadNuxtApp } from './chunk'
|
export { reloadNuxtApp } from './chunk'
|
||||||
export { useRequestURL } from './url'
|
export { useRequestURL } from './url'
|
||||||
|
export { usePreviewMode } from './preview'
|
||||||
export { useId } from './id'
|
export { useId } from './id'
|
||||||
|
91
packages/nuxt/src/app/composables/preview.ts
Normal file
91
packages/nuxt/src/app/composables/preview.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import { toRef, watch } from 'vue'
|
||||||
|
|
||||||
|
import { useState } from './state'
|
||||||
|
import { refreshNuxtData } from './asyncData'
|
||||||
|
import { useRoute, useRouter } from './router'
|
||||||
|
|
||||||
|
interface Preview {
|
||||||
|
enabled: boolean
|
||||||
|
state: Record<any, unknown>
|
||||||
|
_initialized?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PreviewModeOptions<S> {
|
||||||
|
shouldEnable?: (state: Preview['state']) => boolean,
|
||||||
|
getState?: (state: Preview['state']) => S,
|
||||||
|
}
|
||||||
|
|
||||||
|
type EnteredState = Record<any, unknown> | null | undefined | void
|
||||||
|
|
||||||
|
let unregisterRefreshHook: (() => any) | undefined
|
||||||
|
|
||||||
|
/** @since 3.11.0 */
|
||||||
|
export function usePreviewMode<S extends EnteredState> (options: PreviewModeOptions<S> = {}) {
|
||||||
|
const preview = useState<Preview>('_preview-state', () => ({
|
||||||
|
enabled: false,
|
||||||
|
state: {}
|
||||||
|
}))
|
||||||
|
|
||||||
|
if (preview.value._initialized) {
|
||||||
|
return {
|
||||||
|
enabled: toRef(preview.value, 'enabled'),
|
||||||
|
state: preview.value.state as S extends void ? Preview['state'] : (NonNullable<S> & Preview['state']),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.client) {
|
||||||
|
preview.value._initialized = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!preview.value.enabled) {
|
||||||
|
const shouldEnable = options.shouldEnable ?? defaultShouldEnable
|
||||||
|
const result = shouldEnable(preview.value.state)
|
||||||
|
|
||||||
|
if (typeof result === 'boolean') { preview.value.enabled = result }
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => preview.value.enabled, (value) => {
|
||||||
|
if (value) {
|
||||||
|
const getState = options.getState ?? getDefaultState
|
||||||
|
const newState = getState(preview.value.state)
|
||||||
|
|
||||||
|
if (newState !== preview.value.state) {
|
||||||
|
Object.assign(preview.value.state, newState)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.client && !unregisterRefreshHook) {
|
||||||
|
refreshNuxtData()
|
||||||
|
|
||||||
|
unregisterRefreshHook = useRouter().afterEach((() => refreshNuxtData()))
|
||||||
|
}
|
||||||
|
} else if (unregisterRefreshHook) {
|
||||||
|
unregisterRefreshHook()
|
||||||
|
|
||||||
|
unregisterRefreshHook = undefined
|
||||||
|
}
|
||||||
|
}, { immediate: true, flush: 'sync' })
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabled: toRef(preview.value, 'enabled'),
|
||||||
|
state: preview.value.state as S extends void ? Preview['state'] : (NonNullable<S> & Preview['state']),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultShouldEnable () {
|
||||||
|
const route = useRoute()
|
||||||
|
const previewQueryName = 'preview'
|
||||||
|
|
||||||
|
return route.query[previewQueryName] === 'true'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultState (state: Preview['state']) {
|
||||||
|
if (state.token !== undefined) {
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
state.token = Array.isArray(route.query.token) ? route.query.token[0] : route.query.token
|
||||||
|
|
||||||
|
return state
|
||||||
|
}
|
@ -101,6 +101,10 @@ const granularAppPresets: InlinePreset[] = [
|
|||||||
imports: ['useRequestURL'],
|
imports: ['useRequestURL'],
|
||||||
from: '#app/composables/url'
|
from: '#app/composables/url'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
imports: ['usePreviewMode'],
|
||||||
|
from: '#app/composables/preview'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
imports: ['useId'],
|
imports: ['useId'],
|
||||||
from: '#app/composables/id'
|
from: '#app/composables/id'
|
||||||
|
@ -568,6 +568,61 @@ describe('nuxt composables', () => {
|
|||||||
expect(text).toContain('foobar')
|
expect(text).toContain('foobar')
|
||||||
await page.close()
|
await page.close()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('respects preview mode with a token', async () => {
|
||||||
|
const token = 'hehe'
|
||||||
|
const page = await createPage(`/preview?preview=true&token=${token}`)
|
||||||
|
|
||||||
|
const hasRerunFetchOnClient = await new Promise<boolean>((resolve) => {
|
||||||
|
page.on('console', (message) => {
|
||||||
|
setTimeout(() => resolve(false), 4000)
|
||||||
|
|
||||||
|
if (message.text() === 'true') { resolve(true) }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(hasRerunFetchOnClient).toBe(true)
|
||||||
|
|
||||||
|
expect(await page.locator('#fetched-on-client').textContent()).toContain('fetched on client')
|
||||||
|
expect(await page.locator('#preview-mode').textContent()).toContain('preview mode enabled')
|
||||||
|
|
||||||
|
await page.click('#use-fetch-check')
|
||||||
|
await page.waitForFunction(() => window.useNuxtApp?.()._route.fullPath.includes('/preview/with-use-fetch'))
|
||||||
|
|
||||||
|
expect(await page.locator('#token-check').textContent()).toContain(token)
|
||||||
|
expect(await page.locator('#correct-api-key-check').textContent()).toContain('true')
|
||||||
|
await page.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('respects preview mode with custom state', async () => {
|
||||||
|
const { page } = await renderPage('/preview/with-custom-state?preview=true')
|
||||||
|
|
||||||
|
expect(await page.locator('#data1').textContent()).toContain('data1 updated')
|
||||||
|
expect(await page.locator('#data2').textContent()).toContain('data2')
|
||||||
|
|
||||||
|
await page.click('#toggle-preview') // manually turns off preview mode
|
||||||
|
await page.click('#with-use-fetch')
|
||||||
|
await page.waitForFunction(() => window.useNuxtApp?.()._route.fullPath.includes('/preview/with-use-fetch'))
|
||||||
|
|
||||||
|
expect(await page.locator('#enabled').textContent()).toContain('false')
|
||||||
|
expect(await page.locator('#token-check').textContent()).toEqual('')
|
||||||
|
expect(await page.locator('#correct-api-key-check').textContent()).toContain('false')
|
||||||
|
await page.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('respects preview mode with custom enable', async () => {
|
||||||
|
const { page } = await renderPage('/preview/with-custom-enable?preview=true')
|
||||||
|
|
||||||
|
expect(await page.locator('#enabled').textContent()).toContain('false')
|
||||||
|
await page.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('respects preview mode with custom enable and customPreview', async () => {
|
||||||
|
const { page } = await renderPage('/preview/with-custom-enable?customPreview=true')
|
||||||
|
|
||||||
|
expect(await page.locator('#enabled').textContent()).toContain('true')
|
||||||
|
await page.close()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('rich payloads', () => {
|
describe('rich payloads', () => {
|
||||||
|
38
test/fixtures/basic/pages/preview/index.vue
vendored
Normal file
38
test/fixtures/basic/pages/preview/index.vue
vendored
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<script setup>
|
||||||
|
const { enabled: isPreview } = usePreviewMode()
|
||||||
|
|
||||||
|
const { data } = await useAsyncData(async () => {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 200))
|
||||||
|
|
||||||
|
const fetchedOnClient = process.client
|
||||||
|
|
||||||
|
console.log(fetchedOnClient)
|
||||||
|
|
||||||
|
return { fetchedOnClient }
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<NuxtLink
|
||||||
|
id="use-fetch-check"
|
||||||
|
href="/preview/with-use-fetch"
|
||||||
|
>
|
||||||
|
check useFetch
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
|
<p
|
||||||
|
v-if="data && data.fetchedOnClient"
|
||||||
|
id="fetched-on-client"
|
||||||
|
>
|
||||||
|
fetched on client
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p
|
||||||
|
v-if="isPreview"
|
||||||
|
id="preview-mode"
|
||||||
|
>
|
||||||
|
preview mode enabled
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
17
test/fixtures/basic/pages/preview/with-custom-enable.vue
vendored
Normal file
17
test/fixtures/basic/pages/preview/with-custom-enable.vue
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<script setup>
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const { enabled } = usePreviewMode({
|
||||||
|
shouldEnable: () => {
|
||||||
|
return !!route.query.customPreview
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<p id="enabled">
|
||||||
|
{{ enabled }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
39
test/fixtures/basic/pages/preview/with-custom-state.vue
vendored
Normal file
39
test/fixtures/basic/pages/preview/with-custom-state.vue
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<script setup>
|
||||||
|
const data1 = ref('data1')
|
||||||
|
|
||||||
|
const { enabled, state } = usePreviewMode({
|
||||||
|
getState: () => {
|
||||||
|
return { data1, data2: 'data2' }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
data1.value = 'data1 updated'
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<NuxtLink
|
||||||
|
id="with-use-fetch"
|
||||||
|
to="/preview/with-use-fetch"
|
||||||
|
>
|
||||||
|
fetch check
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
|
<p id="data1">
|
||||||
|
{{ state.data1 }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p id="data2">
|
||||||
|
{{ state.data2 }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
id="toggle-preview"
|
||||||
|
@click="enabled = !enabled"
|
||||||
|
>
|
||||||
|
toggle preview mode
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
25
test/fixtures/basic/pages/preview/with-use-fetch.vue
vendored
Normal file
25
test/fixtures/basic/pages/preview/with-use-fetch.vue
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<script setup>
|
||||||
|
const { enabled, state } = usePreviewMode()
|
||||||
|
|
||||||
|
const { data } = await useFetch('/api/preview', {
|
||||||
|
query: {
|
||||||
|
apiKey: state.token || undefined
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<p id="enabled">
|
||||||
|
{{ enabled }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p id="token-check">
|
||||||
|
{{ state.token }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p id="correct-api-key-check">
|
||||||
|
{{ data && data.hehe }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
8
test/fixtures/basic/server/api/preview.ts
vendored
Normal file
8
test/fixtures/basic/server/api/preview.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
const apiKeyName = 'apiKey'
|
||||||
|
const apiKey = 'hehe'
|
||||||
|
|
||||||
|
export default defineEventHandler((event) => {
|
||||||
|
return {
|
||||||
|
hehe: getQuery(event)[apiKeyName] === apiKey
|
||||||
|
}
|
||||||
|
})
|
@ -128,7 +128,8 @@ describe('composables', () => {
|
|||||||
'useLazyAsyncData',
|
'useLazyAsyncData',
|
||||||
'useRouter',
|
'useRouter',
|
||||||
'useSeoMeta',
|
'useSeoMeta',
|
||||||
'useServerSeoMeta'
|
'useServerSeoMeta',
|
||||||
|
'usePreviewMode'
|
||||||
]
|
]
|
||||||
expect(Object.keys(composables).sort()).toEqual([...new Set([...testedComposables, ...skippedComposables])].sort())
|
expect(Object.keys(composables).sort()).toEqual([...new Set([...testedComposables, ...skippedComposables])].sort())
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user