diff --git a/docs/3.api/2.components/3.nuxt-layout.md b/docs/3.api/2.components/3.nuxt-layout.md
index f168f1048d..607d272408 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 5e468875e4..a44892639a 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 a65289f285..594206ca0c 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/fixtures/basic/layouts/custom.vue b/test/fixtures/basic/layouts/custom.vue
index e7938d8f69..7321cc97a7 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 35236542c8..9abe74108a 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 1d87ad7b54..5f2a714f7a 100644
--- a/test/fixtures/basic/layouts/with-props.vue
+++ b/test/fixtures/basic/layouts/with-props.vue
@@ -1,6 +1,8 @@
- {{ someProp }}
-
+