feat(nuxt): allow accessing `NuxtPage` ref via `pageRef` (#19403)

This commit is contained in:
Julien Huang 2023-06-11 00:13:33 +02:00 committed by GitHub
parent c6a62268c3
commit 319935fc95
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 113 additions and 7 deletions

View File

@ -46,6 +46,23 @@ definePageMeta({
:button-link[Open on StackBlitz]{href="https://stackblitz.com/github/nuxt/nuxt/tree/main/examples/routing/pages?file=app.vue" blank}
## Accessing a page's component ref
To get the ref of a page component, access it through `ref.value.pageRef`
````html
<template>
<NuxtPage ref="page" />
</template>
<script setup lang="ts">
const page = ref()
function logFoo () {
page.value.pageRef.foo()
}
</script>
````
## Custom Props
In addition, `NuxtPage` also accepts custom props that you may need to pass further down the hierarchy. These custom props are accessible via `attrs` in the Nuxt app.

View File

@ -1,4 +1,4 @@
import { Suspense, Transition, computed, defineComponent, h, nextTick, onMounted, provide, reactive } from 'vue'
import { Suspense, Transition, computed, defineComponent, h, nextTick, onMounted, provide, reactive, ref } from 'vue'
import type { KeepAliveProps, TransitionProps, VNode } from 'vue'
import { RouterView } from '#vue-router'
import { defu } from 'defu'
@ -34,8 +34,12 @@ export default defineComponent({
default: null
}
},
setup (props, { attrs }) {
setup (props, { attrs, expose }) {
const nuxtApp = useNuxtApp()
const pageRef = ref()
expose({ pageRef })
return () => {
return h(RouterView, { name: props.name, route: props.route, ...attrs }, {
default: (routeProps: RouterViewSlotProps) => {
@ -57,7 +61,7 @@ export default defineComponent({
suspensible: true,
onPending: () => nuxtApp.callHook('page:start', routeProps.Component),
onResolve: () => { nextTick(() => nuxtApp.callHook('page:finish', routeProps.Component).finally(done)) }
}, { default: () => h(RouteProvider, { key, routeProps, pageKey: key, hasTransition } as {}) })
}, { default: () => h(RouteProvider, { key, routeProps, pageKey: key, hasTransition, pageRef } as {}) })
)).default()
}
})
@ -81,7 +85,7 @@ const RouteProvider = defineComponent({
name: 'RouteProvider',
// TODO: Type props
// eslint-disable-next-line vue/require-prop-types
props: ['routeProps', 'pageKey', 'hasTransition'],
props: ['routeProps', 'pageKey', 'hasTransition', 'pageRef'],
setup (props) {
// Prevent reactivity when the page will be rerendered in a different suspense fork
// eslint-disable-next-line vue/no-setup-props-destructure
@ -111,11 +115,11 @@ const RouteProvider = defineComponent({
return () => {
if (process.dev && process.client) {
vnode = h(props.routeProps.Component)
vnode = h(props.routeProps.Component, { ref: props.pageRef })
return vnode
}
return h(props.routeProps.Component)
return h(props.routeProps.Component, { ref: props.pageRef })
}
}
})

View File

@ -326,6 +326,27 @@ describe('pages', () => {
await expectNoClientErrors('/client-only-explicit-import')
})
it('/wrapper-expose/page', async () => {
await expectNoClientErrors('/wrapper-expose/page')
let lastLog: string|undefined
const page = await createPage('/wrapper-expose/page')
page.on('console', (log) => {
lastLog = log.text()
})
page.on('pageerror', (log) => {
lastLog = log.message
})
await page.waitForLoadState('networkidle')
await page.locator('#log-foo').click()
expect(lastLog === 'bar').toBeTruthy()
// change page
await page.locator('#to-hello').click()
await page.locator('#log-foo').click()
expect(lastLog?.includes('.foo is not a function')).toBeTruthy()
await page.locator('#log-hello').click()
expect(lastLog === 'world').toBeTruthy()
})
it('client-fallback', async () => {
const classes = [
'clientfallback-non-stateful-setup',

View File

@ -35,7 +35,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
it('default server bundle size', async () => {
stats.server = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
expect(roundToKilobytes(stats.server.totalBytes)).toMatchInlineSnapshot('"61.9k"')
expect(roundToKilobytes(stats.server.totalBytes)).toMatchInlineSnapshot('"61.8k"')
const modules = await analyzeSizes('node_modules/**/*', serverDir)
expect(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"2286k"')

View File

@ -0,0 +1,24 @@
<template>
<div>
<NuxtPage ref="page" />
<button id="log-hello" @click="logHello">
hello
</button>
<button id="log-foo" @click="logFoo">
foo
</button>
</div>
</template>
<script setup lang="ts">
const page = ref()
function logFoo () {
page.value.pageRef.foo()
}
function logHello () {
page.value.pageRef.hello()
}
</script>

View File

@ -0,0 +1,21 @@
<template>
<div>
<p>
hello
</p>
<NuxtLink id="to-foo" to="/wrapper-expose/page">
to foo
</NuxtLink>
</div>
</template>
<script setup lang="ts">
function hello () {
console.log('world')
}
defineExpose({
hello
})
</script>

View File

@ -0,0 +1,19 @@
<template>
<div>
<p>foo</p>
<NuxtLink id="to-hello" to="/wrapper-expose/page/another">
to hello
</NuxtLink>
</div>
</template>
<script setup lang="ts">
function foo () {
console.log('bar')
}
defineExpose({
foo
})
</script>