Skip to content

Commit

Permalink
feat(nuxt): allow accessing NuxtLayout ref via layoutRef (#19465)
Browse files Browse the repository at this point in the history
  • Loading branch information
huang-julien committed Jun 10, 2023
1 parent 319935f commit 41d34ca
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 12 deletions.
17 changes: 17 additions & 0 deletions docs/3.api/2.components/3.nuxt-layout.md
Expand Up @@ -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"}
::
30 changes: 20 additions & 10 deletions 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'
Expand All @@ -16,6 +16,7 @@ const LayoutLoader = defineComponent({
inheritAttrs: false,
props: {
name: String,
layoutRef: Object as () => VNodeRef,
...process.dev ? { hasTransition: Boolean } : {}
},
async setup (props, context) {
Expand All @@ -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,
Expand All @@ -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) {
Expand All @@ -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()
}
}
Expand Down
35 changes: 35 additions & 0 deletions test/basic.test.ts
Expand Up @@ -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')

Expand Down
19 changes: 19 additions & 0 deletions test/fixtures/basic/layouts/custom.vue
Expand Up @@ -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 changes: 19 additions & 0 deletions test/fixtures/basic/layouts/custom2.vue
Expand Up @@ -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>
6 changes: 4 additions & 2 deletions test/fixtures/basic/layouts/with-props.vue
@@ -1,6 +1,8 @@
<template>
<p>{{ someProp }}</p>
<slot />
<div>
<p>{{ someProp }}</p>
<slot />
</div>
</template>

<script lang="ts" setup>
Expand Down
35 changes: 35 additions & 0 deletions test/fixtures/basic/pages/wrapper-expose/layout.vue
@@ -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>

0 comments on commit 41d34ca

Please sign in to comment.