mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-22 05:35:13 +00:00
feat(nuxt): allow accessing NuxtLayout
ref via layoutRef
(#19465)
This commit is contained in:
parent
319935fc95
commit
41d34ca67d
@ -61,5 +61,22 @@ Please note the layout name is normalized to kebab-case, so if your layout file
|
||||
</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"}
|
||||
::
|
||||
|
@ -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 && {
|
||||
default: () => {
|
||||
const layoutNode = _wrapIf(LayoutLoader, hasLayout && {
|
||||
key: layout.value,
|
||||
name: layout.value,
|
||||
...(process.dev ? { hasTransition: !!transitionProps } : {}),
|
||||
...context.attrs
|
||||
...context.attrs,
|
||||
layoutRef
|
||||
}, context.slots).default()
|
||||
|
||||
return layoutNode
|
||||
}
|
||||
}).default()
|
||||
}
|
||||
}
|
||||
|
@ -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')
|
||||
|
||||
|
19
test/fixtures/basic/layouts/custom.vue
vendored
19
test/fixtures/basic/layouts/custom.vue
vendored
@ -2,5 +2,24 @@
|
||||
<div>
|
||||
Custom Layout:
|
||||
<slot />
|
||||
|
||||
<div class="count">
|
||||
{{ count }}
|
||||
</div>
|
||||
<button class="add-count" @click="count++">
|
||||
add count
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const count = ref(0)
|
||||
|
||||
function logHello () {
|
||||
console.log('world')
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
logHello
|
||||
})
|
||||
</script>
|
||||
|
19
test/fixtures/basic/layouts/custom2.vue
vendored
19
test/fixtures/basic/layouts/custom2.vue
vendored
@ -2,5 +2,24 @@
|
||||
<div>
|
||||
Custom2 Layout:
|
||||
<slot />
|
||||
|
||||
<div class="count">
|
||||
{{ count }}
|
||||
</div>
|
||||
<button class="add-count" @click="count++">
|
||||
add count
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const count = ref(0)
|
||||
|
||||
function logFoo () {
|
||||
console.log('bar')
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
logFoo
|
||||
})
|
||||
</script>
|
||||
|
2
test/fixtures/basic/layouts/with-props.vue
vendored
2
test/fixtures/basic/layouts/with-props.vue
vendored
@ -1,6 +1,8 @@
|
||||
<template>
|
||||
<div>
|
||||
<p>{{ someProp }}</p>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
35
test/fixtures/basic/pages/wrapper-expose/layout.vue
vendored
Normal file
35
test/fixtures/basic/pages/wrapper-expose/layout.vue
vendored
Normal 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>
|
Loading…
Reference in New Issue
Block a user