From 41d34ca67d8e09fec8bae94ed0213c9e1bae6c19 Mon Sep 17 00:00:00 2001 From: Julien Huang Date: Sun, 11 Jun 2023 00:17:14 +0200 Subject: [PATCH] feat(nuxt): allow accessing `NuxtLayout` ref via `layoutRef` (#19465) --- docs/3.api/2.components/3.nuxt-layout.md | 17 +++++++++ packages/nuxt/src/app/components/layout.ts | 30 ++++++++++------ test/basic.test.ts | 35 +++++++++++++++++++ test/fixtures/basic/layouts/custom.vue | 19 ++++++++++ test/fixtures/basic/layouts/custom2.vue | 19 ++++++++++ test/fixtures/basic/layouts/with-props.vue | 6 ++-- .../basic/pages/wrapper-expose/layout.vue | 35 +++++++++++++++++++ 7 files changed, 149 insertions(+), 12 deletions(-) create mode 100644 test/fixtures/basic/pages/wrapper-expose/layout.vue 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 @@