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>
|
</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"}
|
||||||
::
|
::
|
||||||
|
@ -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: () => {
|
||||||
|
const layoutNode = _wrapIf(LayoutLoader, hasLayout && {
|
||||||
key: layout.value,
|
key: layout.value,
|
||||||
name: layout.value,
|
name: layout.value,
|
||||||
...(process.dev ? { hasTransition: !!transitionProps } : {}),
|
...(process.dev ? { hasTransition: !!transitionProps } : {}),
|
||||||
...context.attrs
|
...context.attrs,
|
||||||
|
layoutRef
|
||||||
}, context.slots).default()
|
}, context.slots).default()
|
||||||
|
|
||||||
|
return layoutNode
|
||||||
|
}
|
||||||
}).default()
|
}).default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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')
|
||||||
|
|
||||||
|
19
test/fixtures/basic/layouts/custom.vue
vendored
19
test/fixtures/basic/layouts/custom.vue
vendored
@ -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>
|
||||||
|
19
test/fixtures/basic/layouts/custom2.vue
vendored
19
test/fixtures/basic/layouts/custom2.vue
vendored
@ -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>
|
||||||
|
2
test/fixtures/basic/layouts/with-props.vue
vendored
2
test/fixtures/basic/layouts/with-props.vue
vendored
@ -1,6 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<div>
|
||||||
<p>{{ someProp }}</p>
|
<p>{{ someProp }}</p>
|
||||||
<slot />
|
<slot />
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<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