diff --git a/docs/3.api/2.components/3.nuxt-layout.md b/docs/3.api/2.components/3.nuxt-layout.md index f168f1048d91..607d272408a3 100644 --- a/docs/3.api/2.components/3.nuxt-layout.md +++ b/docs/3.api/2.components/3.nuxt-layout.md @@ -61,5 +61,22 @@ Please note the layout name is normalized to kebab-case, so if your layout file ``` +## Accessing a layout's component ref + +To get the ref of a layout component, access it through `ref.value.layoutRef` + +````html + + + +```` + ::ReadMore{link="/docs/guide/directory-structure/layouts"} :: diff --git a/packages/nuxt/src/app/components/layout.ts b/packages/nuxt/src/app/components/layout.ts index 5e468875e4ae..a44892639aa7 100644 --- a/packages/nuxt/src/app/components/layout.ts +++ b/packages/nuxt/src/app/components/layout.ts @@ -1,5 +1,5 @@ -import type { Ref, VNode } from 'vue' -import { Transition, computed, defineComponent, h, inject, nextTick, onMounted, unref } from 'vue' +import type { Ref, VNode, VNodeRef } from 'vue' +import { Transition, computed, defineComponent, h, inject, mergeProps, nextTick, onMounted, ref, unref } from 'vue' import type { RouteLocationNormalizedLoaded } from 'vue-router' import { _wrapIf } from './utils' import { useRoute } from '#app/composables/router' @@ -16,6 +16,7 @@ const LayoutLoader = defineComponent({ inheritAttrs: false, props: { name: String, + layoutRef: Object as () => VNodeRef, ...process.dev ? { hasTransition: Boolean } : {} }, async setup (props, context) { @@ -35,13 +36,14 @@ const LayoutLoader = defineComponent({ return () => { if (process.dev && process.client && props.hasTransition) { - vnode = h(LayoutComponent, context.attrs, context.slots) + vnode = h(LayoutComponent, mergeProps(context.attrs, { ref: props.layoutRef }), context.slots) return vnode } - return h(LayoutComponent, context.attrs, context.slots) + return h(LayoutComponent, mergeProps(context.attrs, { ref: props.layoutRef }), context.slots) } } }) + export default defineComponent({ name: 'NuxtLayout', inheritAttrs: false, @@ -57,6 +59,9 @@ export default defineComponent({ const route = injectedRoute === useRoute() ? useVueRouterRoute() : injectedRoute const layout = computed(() => unref(props.name) ?? route.meta.layout as string ?? 'default') + const layoutRef = ref() + context.expose({ layoutRef }) + let vnode: VNode let _layout: string | false if (process.dev && process.client) { @@ -79,12 +84,17 @@ export default defineComponent({ // We avoid rendering layout transition if there is no layout to render return _wrapIf(Transition, hasLayout && transitionProps, { - default: () => _wrapIf(LayoutLoader, hasLayout && { - key: layout.value, - name: layout.value, - ...(process.dev ? { hasTransition: !!transitionProps } : {}), - ...context.attrs - }, context.slots).default() + default: () => { + const layoutNode = _wrapIf(LayoutLoader, hasLayout && { + key: layout.value, + name: layout.value, + ...(process.dev ? { hasTransition: !!transitionProps } : {}), + ...context.attrs, + layoutRef + }, context.slots).default() + + return layoutNode + } }).default() } } diff --git a/test/basic.test.ts b/test/basic.test.ts index 01a320ada1b2..bbb40d164c19 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -315,6 +315,41 @@ describe('pages', () => { await page.close() }) + it('/wrapper-expose/layout', async () => { + await expectNoClientErrors('/wrapper-expose/layout') + + let lastLog: string|undefined + const page = await createPage('/wrapper-expose/layout') + page.on('console', (log) => { + lastLog = log.text() + }) + page.on('pageerror', (log) => { + lastLog = log.message + }) + await page.waitForLoadState('networkidle') + await page.locator('.log-foo').first().click() + expect(lastLog).toContain('.logFoo is not a function') + await page.locator('.log-hello').first().click() + expect(lastLog).toContain('world') + await page.locator('.add-count').first().click() + expect(await page.locator('.count').first().innerText()).toContain('1') + + // change layout + await page.locator('.swap-layout').click() + await page.waitForTimeout(25) + expect(await page.locator('.count').first().innerText()).toContain('0') + await page.locator('.log-foo').first().click() + expect(lastLog).toContain('bar') + await page.locator('.log-hello').first().click() + expect(lastLog).toContain('.logHello is not a function') + await page.locator('.add-count').first().click() + expect(await page.locator('.count').first().innerText()).toContain('1') + // change layout + await page.locator('.swap-layout').click() + await page.waitForTimeout(25) + expect(await page.locator('.count').first().innerText()).toContain('0') + }) + it('/client-only-explicit-import', async () => { const html = await $fetch('/client-only-explicit-import') diff --git a/test/bundle.test.ts b/test/bundle.test.ts index 427b1aa80e26..f60169103887 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/layouts/custom.vue b/test/fixtures/basic/layouts/custom.vue index e7938d8f69d6..7321cc97a740 100644 --- a/test/fixtures/basic/layouts/custom.vue +++ b/test/fixtures/basic/layouts/custom.vue @@ -2,5 +2,24 @@
Custom Layout: + +
+ {{ count }} +
+
+ + diff --git a/test/fixtures/basic/layouts/custom2.vue b/test/fixtures/basic/layouts/custom2.vue index 35236542c813..9abe74108ac9 100644 --- a/test/fixtures/basic/layouts/custom2.vue +++ b/test/fixtures/basic/layouts/custom2.vue @@ -2,5 +2,24 @@
Custom2 Layout: + +
+ {{ count }} +
+
+ + diff --git a/test/fixtures/basic/layouts/with-props.vue b/test/fixtures/basic/layouts/with-props.vue index 1d87ad7b544f..5f2a714f7a63 100644 --- a/test/fixtures/basic/layouts/with-props.vue +++ b/test/fixtures/basic/layouts/with-props.vue @@ -1,6 +1,8 @@