feat(nuxt): allow accessing NuxtLayout ref via layoutRef (#19465)

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

View File

@ -61,5 +61,22 @@ Please note the layout name is normalized to kebab-case, so if your layout file
</template> </template>
``` ```
## Accessing a layout's component ref
To get the ref of a layout component, access it through `ref.value.layoutRef`
````html
<template>
<NuxtLayout ref="layout" />
</template>
<script setup lang="ts">
const layout = ref()
function logFoo () {
layout.value.layoutRef.foo()
}
</script>
````
::ReadMore{link="/docs/guide/directory-structure/layouts"} ::ReadMore{link="/docs/guide/directory-structure/layouts"}
:: ::

View File

@ -1,5 +1,5 @@
import type { Ref, VNode } from 'vue' import type { Ref, VNode, VNodeRef } from 'vue'
import { Transition, computed, defineComponent, h, inject, nextTick, onMounted, unref } from 'vue' import { Transition, computed, defineComponent, h, inject, mergeProps, nextTick, onMounted, ref, unref } from 'vue'
import type { RouteLocationNormalizedLoaded } from 'vue-router' import type { RouteLocationNormalizedLoaded } from 'vue-router'
import { _wrapIf } from './utils' import { _wrapIf } from './utils'
import { useRoute } from '#app/composables/router' import { useRoute } from '#app/composables/router'
@ -16,6 +16,7 @@ const LayoutLoader = defineComponent({
inheritAttrs: false, inheritAttrs: false,
props: { props: {
name: String, name: String,
layoutRef: Object as () => VNodeRef,
...process.dev ? { hasTransition: Boolean } : {} ...process.dev ? { hasTransition: Boolean } : {}
}, },
async setup (props, context) { async setup (props, context) {
@ -35,13 +36,14 @@ const LayoutLoader = defineComponent({
return () => { return () => {
if (process.dev && process.client && props.hasTransition) { 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 vnode
} }
return h(LayoutComponent, context.attrs, context.slots) return h(LayoutComponent, mergeProps(context.attrs, { ref: props.layoutRef }), context.slots)
} }
} }
}) })
export default defineComponent({ export default defineComponent({
name: 'NuxtLayout', name: 'NuxtLayout',
inheritAttrs: false, inheritAttrs: false,
@ -57,6 +59,9 @@ export default defineComponent({
const route = injectedRoute === useRoute() ? useVueRouterRoute() : injectedRoute const route = injectedRoute === useRoute() ? useVueRouterRoute() : injectedRoute
const layout = computed(() => unref(props.name) ?? route.meta.layout as string ?? 'default') const layout = computed(() => unref(props.name) ?? route.meta.layout as string ?? 'default')
const layoutRef = ref()
context.expose({ layoutRef })
let vnode: VNode let vnode: VNode
let _layout: string | false let _layout: string | false
if (process.dev && process.client) { if (process.dev && process.client) {
@ -79,12 +84,17 @@ export default defineComponent({
// We avoid rendering layout transition if there is no layout to render // We avoid rendering layout transition if there is no layout to render
return _wrapIf(Transition, hasLayout && transitionProps, { return _wrapIf(Transition, hasLayout && transitionProps, {
default: () => _wrapIf(LayoutLoader, hasLayout && { default: () => {
key: layout.value, const layoutNode = _wrapIf(LayoutLoader, hasLayout && {
name: layout.value, key: layout.value,
...(process.dev ? { hasTransition: !!transitionProps } : {}), name: layout.value,
...context.attrs ...(process.dev ? { hasTransition: !!transitionProps } : {}),
}, context.slots).default() ...context.attrs,
layoutRef
}, context.slots).default()
return layoutNode
}
}).default() }).default()
} }
} }

View File

@ -315,6 +315,41 @@ describe('pages', () => {
await page.close() 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 () => { it('/client-only-explicit-import', async () => {
const html = await $fetch('/client-only-explicit-import') const html = await $fetch('/client-only-explicit-import')

View File

@ -2,5 +2,24 @@
<div> <div>
Custom Layout: Custom Layout:
<slot /> <slot />
<div class="count">
{{ count }}
</div>
<button class="add-count" @click="count++">
add count
</button>
</div> </div>
</template> </template>
<script setup lang="ts">
const count = ref(0)
function logHello () {
console.log('world')
}
defineExpose({
logHello
})
</script>

View File

@ -2,5 +2,24 @@
<div> <div>
Custom2 Layout: Custom2 Layout:
<slot /> <slot />
<div class="count">
{{ count }}
</div>
<button class="add-count" @click="count++">
add count
</button>
</div> </div>
</template> </template>
<script setup lang="ts">
const count = ref(0)
function logFoo () {
console.log('bar')
}
defineExpose({
logFoo
})
</script>

View File

@ -1,6 +1,8 @@
<template> <template>
<p>{{ someProp }}</p> <div>
<slot /> <p>{{ someProp }}</p>
<slot />
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>

View File

@ -0,0 +1,35 @@
<template>
<div>
<button class="swap-layout" @click="swapLayout">
swap layout
</button>
<button class="log-foo" @click="logFoo">
log foo
</button>
<button class="log-hello" @click="logHello">
log hello
</button>
<NuxtLayout ref="layout" />
</div>
</template>
<script setup lang="ts">
const layout = ref()
const currentLayout = useState('current-layout', () => 'custom')
definePageMeta({
layout: 'custom'
})
function logFoo () {
layout.value.layoutRef.logFoo()
}
function logHello () {
layout.value.layoutRef.logHello()
}
function swapLayout () {
currentLayout.value = currentLayout.value === 'custom2' ? 'custom' : 'custom2'
setPageLayout(currentLayout.value)
}
</script>