diff --git a/docs/3.api/2.components/2.nuxt-page.md b/docs/3.api/2.components/2.nuxt-page.md index 18c5d5b776..952d256a88 100644 --- a/docs/3.api/2.components/2.nuxt-page.md +++ b/docs/3.api/2.components/2.nuxt-page.md @@ -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 + + + +```` + ## 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. diff --git a/packages/nuxt/src/pages/runtime/page.ts b/packages/nuxt/src/pages/runtime/page.ts index d906c008f8..c8e320acce 100644 --- a/packages/nuxt/src/pages/runtime/page.ts +++ b/packages/nuxt/src/pages/runtime/page.ts @@ -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 }) } } }) diff --git a/test/basic.test.ts b/test/basic.test.ts index 01a320ada1..a65289f285 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -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', diff --git a/test/bundle.test.ts b/test/bundle.test.ts index 427b1aa80e..f601691038 100644 --- a/test/bundle.test.ts +++ b/test/bundle.test.ts @@ -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"') diff --git a/test/fixtures/basic/pages/wrapper-expose/page.vue b/test/fixtures/basic/pages/wrapper-expose/page.vue new file mode 100644 index 0000000000..10b564e0c6 --- /dev/null +++ b/test/fixtures/basic/pages/wrapper-expose/page.vue @@ -0,0 +1,24 @@ + + + diff --git a/test/fixtures/basic/pages/wrapper-expose/page/another.vue b/test/fixtures/basic/pages/wrapper-expose/page/another.vue new file mode 100644 index 0000000000..b8b7d185e4 --- /dev/null +++ b/test/fixtures/basic/pages/wrapper-expose/page/another.vue @@ -0,0 +1,21 @@ + + + diff --git a/test/fixtures/basic/pages/wrapper-expose/page/index.vue b/test/fixtures/basic/pages/wrapper-expose/page/index.vue new file mode 100644 index 0000000000..b42c8e9d39 --- /dev/null +++ b/test/fixtures/basic/pages/wrapper-expose/page/index.vue @@ -0,0 +1,19 @@ + + +