Inertia: Issue with multiple slots in persistence layout.

Created on 18 Apr 2020  路  12Comments  路  Source: inertiajs/inertia

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?

vue 2

Most helpful comment

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:

  1. The Inertia component gets created, and it's render-method gets hit by Vue itself.
  2. 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.
  3. 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 Vue render method, 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:

  1. Prevent having to override our page component's render method
  2. 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:

  1. 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.
  2. In my app.js, I modified the resolveComponent method to hijack Inertia before it hits Vue's rendering process, and always inject the persistent layout on the "page"-component's options (as the resolveComponent method is simply how Inertia resolves the pages / the Vue component options that get used in the renderer described earlier)
  3. 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!

All 12 comments

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. inertiajs/inertia-vue#130

@puncoz There is a solution for now (Reference for https://github.com/inertiajs/inertia-vue/issues/130#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:

  1. The Inertia component gets created, and it's render-method gets hit by Vue itself.
  2. 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.
  3. 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 Vue render method, 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:

  1. Prevent having to override our page component's render method
  2. 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:

  1. 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.
  2. In my app.js, I modified the resolveComponent method to hijack Inertia before it hits Vue's rendering process, and always inject the persistent layout on the "page"-component's options (as the resolveComponent method is simply how Inertia resolves the pages / the Vue component options that get used in the renderer described earlier)
  3. 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:

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:

carbon (1)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

reinink picture reinink  路  3Comments

JoeCianflone picture JoeCianflone  路  4Comments

alighasemzadeh picture alighasemzadeh  路  4Comments

TimVanHerwijnen picture TimVanHerwijnen  路  5Comments

jalt007 picture jalt007  路  4Comments