inertia
inertia copied to clipboard
Issue with multiple slots in persistence layout.
I have used a persistence layout where two slots (scoped slots) are present. Now I can't use that multiple scoped slots in my pages to push content in a layout. Any workaround on this?
Here is something that I was trying to do.
<template>
<main>
<slot name="header"/>
<slot/>
</main>
</template>
<script>
export default {
name: "Layout",
}
</script>
and my page is something like this:
<template>
<div>
page content
</div>
</template>
<script>
export default {
name: "Page",
layout: (h, page) => h(Layout, [page])
}
</script>
How can I pass the content of the header slot? I can do the following, as mentioned in https://github.com/inertiajs/inertia-vue/pull/87#issuecomment-536436890
export default {
layout: (h, page) => {
return h(Layout, {
scopedSlots: {
header() {
return h('span', 'Header')
}
}}, [page])
},
}
But I have not just a simple "Header" text in span. So I guess it's not that easier to write the whole content of the header in that way.
Any suggestion what should I do?
Use slot in Header from your page component:
export default {
name: "Page",
layout: (h, page) => {
return h(Layout, [
page,
h('span', {
slot: 'header'
}, 'Header')
])
}
}
Thanks for the reply. Yes, I can do that and I have mentioned above as well. The problem is, there will not just a span in the header, rather it will have few more dom. And writing dom in a functional way is not feasible as it will be different in pages and tedious as well. I tried JSX as well but didn't work. Can I pass a component in that h(...) ??
export default {
name: "Page",
layout: (h, page) => {
return h(Layout, [
page,
h('div', {
slot: 'header'
}, [
h('div', 'Other component'),
h('div', 'Other component')
])
])
}
}
It is recommended to read the "Render Functions" part of the Vue.js documentation: https://vuejs.org/v2/guide/render-function.html
Thank you @ycs77 for the suggestion. As per your suggestion, I did something similar to this and now I am stuck with another problem:
export default {
name: "Page",
layout: (h, page) => {
return h(Layout, [
page,
h(InertiaLink, {
props: {
href: `/users/${this.userId}`,
},
domProps: {
innerHTML: "Update User",
},
slot: "actions",
}),
])
}
}
In the above code, everything working perfectly except dynamic user id this.userId. Of course, it will not work because layout function will be called later hence this scope will be different.
Are there any ways to pass component scope (or data) to the layout? or any other workaround?
I have the same problem and I can't solve it.
But I send a new issue #172
@puncoz There is a solution for now (Reference for https://github.com/inertiajs/inertia/issues/172#issuecomment-640269331):
export default {
name: "Page",
layout: (h, page) => {
return h(Layout, [
page,
h(InertiaLink, {
props: {
href: `/users/${page.context.props.userId}`, // Change this line
},
domProps: {
innerHTML: "Update User",
},
slot: "actions",
}),
])
}
}
Sorry for not replying to this earlier (like I did on solution referred to above).
While the above solution indeed works, is only a partial & programmatic workaround to this issue by intercepting Vue's render cycle. While it would allow you to use multiple slots, it's still far from perfect, and it's not exactly what I would call the 'great developer experience' that most of us are used to from working with Vue.
I did some source diving and looked into how to solve this earlier. Essentially, the way that persistent layouts work in Inertia is like this:
- The Inertia component gets created, and it's
render-method gets hit by Vue itself. - Instead of rendering it's own template, the "Inertia component" passes the "page"-component options (basically an object representation of the component) in to the renderer, which returns a VNode instance. To keep things simple, you can think of this as a DOM-ready, rendered instance of the component.
- Inertia checks whether the "page"-component options (the same ones used to create the rendered VNode just now) contains a
layout-property/function, which it then calls as if it was the Vuerendermethod, passing in the already-rendered VNode/rendered instance as a 'child'
While this might seem somewhat odd, it is actually quite cool, as this allow us to:
- Prevent having to override our page component's
rendermethod - Intercept the component rendering from within the component itself 🤯
Unfortunately, this also means that while Vue at this point hasn't finished rendering yet, but ** the "Page"-component is already done rendering** (which gets injected into the Layout), we cannot use template slots, unless defined programmatically like described previously.
If you think of it using Vue terms, it also makes sense, since you can (mostly) only pass slots into a child component, but you cannot pass child-slots into a parent.. (In this case, the Layout is the parent component, and the Page is the child component)
Anyway, bad news aside, this got me onto another idea, which is to combine both approaches. Here's what I ended up doing:
- I created a "Persistent" Vue (layout) component, in which I mostly don't use markup, except for things that I want to, well, persist across pages.
- In my
app.js, I modified theresolveComponentmethod to hijack Inertia before it hits Vue's rendering process, and always inject the persistent layout on the "page"-component's options (as theresolveComponentmethod is simply how Inertia resolves the pages / the Vue component options that get used in the renderer described earlier) - In my "page"-components themselves, I use Layouts in the same way how Inertia's docs mostly describe using them.
Complexity aside, this allows me to keep persistent things (such as notifications etc. and the user's browser-interaction-state of it) in the "Persistent"-layout, and communicate to/from it using Vuex. The important thing to do here (IMO) is to keep the html markup/styling on the Layout component as much as possible, and to keep the "Persistent"-layout as markup-free as possible, as to provide an "invisible" layer of sorts. As far as solutions go, I think this is a pretty decent one.
Long story short, or in case that wasn't clear / copy and paste-able enough, here's an example of how this would all get wired up:
app.js
import PersistentLayout from "./Layouts/Persistent";
import Store from "./VueXStore";
new Vue({
store: Store,
render: h => h(InertiaApp, {
props: {
initialPage: JSON.parse(app.dataset.page),
resolveComponent: name => {
const componentOptions = require(`./Pages/${name}`).default;
componentOptions.layout = (h, page) => h(PersistentLayout, [page]);
return componentOptions;
},
},
}),
}).$mount(app)
VueXStore.js
import Vue from 'vue';
import Vuex, { Store } from 'vuex';
Vue.use(Vuex);
export default new Store({
state: {
count: 1
},
mutations: {
increment (state) {
// mutate state
state.count++
}
},
actions: {
increment (context) {
context.commit('increment')
}
}
})
Layouts/Persistent.vue
<template>
<div>
<persistent-component />
<slot />
Example VueX Counter: {{ count }}
</div>
</template>
<script>
import { mapState } from "vuex";
import PersistentComponent from "../Components/PersistentComponent";
export default {
components: {
PersistentComponent
},
computed: {
...mapState([
'count',
])
}
}
</script>
Layouts/Primary.vue
<template>
<div>
<div class="this-is-my-template">
<slot name="header" />
</div>
<slot />
</div>
</template>
Pages/MyPage.vue
<template>
<layout title="Home">
My Page Content
<button @click="increment">Increment Persistent Counter</button>
<div slot="header">
My Header Content
</div>
</layout>
</template>
<script>
import { mapActions } from "vuex";
import Layout from "../../Layouts/Primary";
export default {
components: {
Layout
},
methods: {
...mapActions([
'increment'
])
}
}
</script>
Hope it helps!
@claudiodekker Thanks for the lengthy and helpful work around!
@claudiodekker - Question for you....Have you used this approach to persistent layouts in conjunction with code splitting? Today I implemented code splitting in my application but my persistent layout is no longer being rendered.
Was curious if you've seen this before
@jamesburrow23 Hmm.. No, I haven't. Sorry.
To be honest, I've also since migrated to a different approach, at which I threw out VueX altogether. My main reasoning for this is that Inertia was designed to have it's state managed on the back-end, and I wanted to prevent having 'two' sources of truth.
What I've decided to do instead, is to use Vue's built-in provide and inject. It's not reactive, but it solves the same issue:
- https://vuejs.org/v2/api/#provide-inject
- https://vuejs.org/v2/guide/components-edge-cases.html#Dependency-Injection
The most appropriate answer to this question is probably also the one provided by @ycs77, as that's how Vue's render method is supposed to be used (according to their forums and documentation)
@claudiodekker thanks for the reply! I've used provide and inject in various places but unfortunately, I've got this component that needs to be persistent so I'll keep hacking at it 👍
I'll check out that other comment, thanks!
For anyone who stumbles across this issue, I was able to work around the lack of slots by using this package: https://github.com/LinusBorg/portal-vue
I have portal-target components in my persistent layout where i'd normally use a conventional vue named slot. Then in my inertia pages I can push my content into those portal targets using <portal to="target-name">
With this setup my code splitting works again since my inertia page component is invoking PersistentLayout, obviously you lose some of the vue goodness like scopedProps but most of your state should be in a vuex store anyways 😄
here is a simple example:

Any progess on this issue?
@jamesburrow23 So, in vue3 we have teleport but the same problem exists when using code splitting aka dynamic import the layout is not rendered, is there any solutions?
Teleport workaround is not useful on slots with default content. It's a Vue 3 issue too.
I'm also curious about this for Vue 3. I'd love to use Inertia's persistent layouts but I haven't figured out any reliable way to either a) pass data from the child component to the parent layout, or b) use scoped slots with persistent layouts. Portal-vue hasn't added Vue3 support, Vue's teleport doesn't seem like the correct solution, and I can't seem to use Vue's render function to achieve this either.
Unfortunately I'm pretty new to both Inertia and Vue, so I'm not getting anywhere. I've read and watched as many resources as I could but still haven't found an answer yet. Has anyone found a good solution?
Any updates on this? 😅
@reinink @claudiodekker any update? I just painfully ran into this today when trying to modify a new Breeze install.
Would love an update on this as <Teleport> from Vue doesn't seem to work (at least not in my project as it keeps complaining that the target does not exist)
@ThaDaVos I'm using teleport for the modals and slide overs without problems.
vue 3.2.37 @inertiajs/inertia 0.11.0
@ThaDaVos I'm using teleport for the modals and slide overs without problems.
vue 3.2.37 @inertiajs/inertia 0.11.0
Is your Teleport target inside the Persistent Layout? Does your buildstack use Vite and uses Laravel as backend?
My teleport targets are in the blade base file (so, yes to Laravel). Right above </body>.
I'm still using Laravel Mix.
I also have a teleport target inside a component of an Inertia page, and it's also working.
I've kept up with this thread over the past couple of years but haven't been able to contribute lately since I haven't been on Vue 3 however now I am working on a new project using the latest version of Vue and Inertia and wanted to post my experience.
I've been able to use named slots in my persistent layouts without any problems when using code splitting. The only caveat to that is I have to use the old <div slot="breadcrumbs"> syntax instead of <template #breadcrumbs>
I am also using Vite instead of Mix which may be why I have not experienced the same problem.
I'm running an application using Laravel 8 and the legacy version of Laravel Vite. I've also included the relevant files for reference if this is useful for anyone who stumbles across this thread and is looking for an idea to spark a solution for their problem.
I was hoping I'd be able to contribute something more helpful, but all I have to offer is: a.) Try changing how your layout is injected into your components (see my app.js example below) b.) Maybe switch to Vite if that is feasible for your situation or possible change
vite.config.js
import { defineConfig } from 'laravel-vite'
import vue from '@vitejs/plugin-vue';
import Inertia from 'inertia-plugin/vite';
let config = defineConfig({
build: {
rollupOptions: {
output: {
entryFileNames: `assets/[name]-[hash].js`,
chunkFileNames: `assets/[hash].js`,
assetFileNames: `assets/[name]-[hash].[ext]`
}
}
}
}, {
"entrypoints": [
"resources\/js\/app.js",
"resources\/js\/bootstrap.js"
],
"ignore_patterns": [
"\/\\.d\\.ts$\/",
"\/\\.json$\/",
"\/\\.test\\.(ts|js)$\/",
"\/\\.spec\\.(ts|js)$\/"
],
"aliases": {
"@": "resources"
},
"public_directory": "",
"ping_timeout": 10,
"ping_url": "https://app.lcl:3000",
"build_path": "build",
"dev_url": "https://app.lcl:3000",
"commands": [],
})
.withPlugin(vue)
.withPlugin(Inertia());
export default config;
app.js
import { createApp, h } from 'vue';
import { createInertiaApp, Link, Head } from '@inertiajs/inertia-vue3';
import '@/css/app.css';
import PrimaryLayout from "./Shared/Layouts/Primary.vue";
import { InertiaProgress } from '@inertiajs/progress'
createInertiaApp({
resolve: async name => {
let page = (await import(`./Pages/${name}.vue`)).default;
page.layout ??= PrimaryLayout;
return page;
},
setup({ el, App, props, plugin }) {
createApp({ render: () => h(App, props) })
.use(plugin)
.component('Link', Link)
.component('Head', Head)
.mount(el)
},
title: name => `My App - ${name}`,
});
InertiaProgress.init();
Shared/Layouts/Primary.vue
<template>
<main class="bg-black h-16 z-1000 sticky top-0 flex items-center justify-between px-8">
<p class="text-center text-white">My App</p>
<div class="flex items-center space-x-4 text-white">
<Link href="/">Home</Link>
<Link href="/video">Videos</Link>
</div>
</main>
<slot name="breadcrumbs"></slot>
<slot />
</template>
Pages/Home.vue
<template>
<Head title="Home" />
<div slot="breadcrumbs">
<h1 class="text-lg font-bold m-4">Breadcrumbs</h1>
</div>
<div>
<h1 class="text-lg font-bold m-4">Home</h1>
</div>
</template>
@jamesburrow23 thanks for posting this. I just tried this in a new Laravel 9 app (Breeze + Vue) with latest version of Vue 3. Unfortunately it's a no go. The name slot content only readers in the default slot instead of the named slot.
@weavdale Sorry I'm just now seeing your response. You're not able to use named slots when using Vite and the old slot syntax like <div slot="">?
@weavdale Sorry I'm just now seeing your response. You're not able to use named slots when using Vite and the old slot syntax like
<div slot="">?
@jamesburrow23, it isn't working for me either. It renders everything in the default slot. And it throws a warning about a deprecated slot attribute ([vue/no-deprecated-slot-attribute] 'slot' attributes are deprecated). Using the new syntax (something like <div #header>Content</div>) squaks about [vue/valid-v-slot] Named slots must use '<template> on a custom element...
@weavdale, I got it to work with a <Teleport /> and waiting for the onMounted event on the page component.
Something like this:
In the persistent layout component:
<template>
<div>
<div data-slot="header" />
<main> <slot /> </main>
</div>
</template>
In the page component:
<script setup>
import { onMounted, ref } from 'vue';
const mounted = ref(false);
onMounted(() => mounted.value = true);
</script>
<template>
<Teleport to='[data-slot="header"]' v-if="mounted">
<h2>Title</h2>
</Teleport>
<div> The rest of the component... </div>
</template>
- The choice for using a
data-slotcustom attribute is just arbitrary... Any valid CSS selector may be used to target the "receiving" end of theTeleport. - The waiting for the
onMountedevent seems to be necessary due to the order in which Inertia builds the layout and pages.
@javoscript It makes sense that it would throw the error about using <div #header> because the #slot syntax is only valid for the template tag.
I'm not sure why it wouldn't work for you, the only thing I can think of like I mentioned before in my update a few weeks ago is I'm using Vite instead of webpack and I haven't had any issues.
@jamesburrow23 Thanks for posting, this seems to be working great! Now this might be a stupid question, but where te define the layout if you don't want to use the default layout setup (so not the PrimaryLayout you've configured in app.js)
Any update on this issue? 🥲
@jamesburrow23 Thanks for posting, this seems to be working great! Now this might be a stupid question, but where te define the layout if you don't want to use the default layout setup (so not the PrimaryLayout you've configured in app.js)
@kurtkonijn sorry for the late reply, I would specify that in whatever page I wanted to use a different layout. Say for example I have a custom marketing page that doesn't follow my application's default layout I can "hijack" the layout by specifying it in that component. Something like this:
<template>
<div>
This is a my marketing page
</div>
</template>
<script>
import Secondary from "@/js/Shared/Layouts/Secondary.vue";
export default {
layout: Secondary,
}
</script>