How to use inline templates in Vue 3 cover image

How to use inline templates in Vue 3

Kane Cohen • May 14, 2022

vue vue3 seo

It so happens that the types of web-based projects I usually work on have one important requirement - displaying content from the server as soon as possible. Due to that my main choice of frontend framework for the past few years has been Vue. Unlike React, which I still don't mind using for SPAs, Vue 2 had a fantastic feature which allowed my apps to serve HTML from the server as soon as possible and then progressively enhancing served HTML by utilizing "inline-template" feature of the framework. If you are reading this post, then most likely you're familiar with how inline templates work (why else would be here?), but let me give just a quick explanation for those who might not be familiar with it. Vue 2 essentially has two common ways of templating its components - you could do it in a traditional way by writing html templates in Component.vue files in a <template></template> section. Which vue would take, compile and execute. Or, like me, you could serve templates "inline" from the server in your HTML document <component inline-template><div>This will be compiled by Vue</div></component>. As I mentioned above - second approach had a huge benefit allowing websites to display their contents for users immediately and then progressively adding "bells and whistles" (enhancing) website.

Unfortunately, Vue 3 dropped native support for inline templates. I can't say that it didn't hurt, but I understand the reasoning. Official documentation for upgrading from Vue 2 to 3 describes couple of ways how to move forward, but they are either unacceptable (using <script> tags) or do not provide any helpful information (default slots). And below I will show in a more detailed way with more real-world examples how to refactor old Vue 2 "inline-template" code to work with Vue 3.

Upgrading "inline-template" components to Vue 3 default slots

Let's take a simple Vue 2 component that used to use "inline-temlpate" rendering:

<main-menu :init-notifications="notifications" inline-template>
    <div class="flex justify-between">
        <ul class="flex gap-2">
            <li>Item 1</li>
            <li>Item 2</li>
        </ul>
        <div>
            <button v-if="notifications" @click="clear">
                {{ notifications }}
            </button>
        </div>
    </div>
</main-menu>

Here you can see a MainMenu component that accepts a property "notifications" that represents the number of unread notifications a user might have. There's an "if" check which should show notifications counter only if there are notifications. In addition to that - there's also a @click handler that is attached to the button and should clear the notifications counter.

Here's how Vue 2 JS side might look like:

<script>
import Vue from 'vue';

export default Vue.extend({
    props: {
        initNotifications: Number,
    },

    data() {
        return {
            notifications: this.initNotifications
        };
    },

    methods: {
        clear() {
            this.notifications = 0;
        }
    }
});
</script>

Nice and simple - and what's critical, all of this stuff works great to progressively enchance a website without forcing user to wait while they download hundreds of kilobytes or sometimes even megabytes of JS before they can see this stuff.

And now let's convert this code to work in Vue 3 version of "inline-templates":

<main-menu :init-notifications="notifications" v-slot="vm">
    <div class="flex justify-between">
        <ul class="flex gap-2">
            <li>Item 1</li>
            <li>Item 2</li>
        </ul>
        <div>
            <button v-if="vm.notifications" @click="vm.clear">
                {{ vm.notifications }}
            </button>
        </div>
    </div>
</main-menu>

Notice how we removed the "inline-template" attribute from <main-menu> and instead added v-slot="vm". This part is important because vm is what we then have to refer first to access properties or functions of the component. Like vm.notifications or vm.clear. Basically anything that has to deal with the MainMenu component should explicitly refer to that component name specified in v-slot attribute. You could change vm to some other, more fitting name if you wish.

And now let's look at the upgraded JS part:

<script>
export default {
    template: `<div><slot v-bind="self"/></div>`,

    props: {
        initNotifications: Number,
    },

    data() {
        return {
            notifications: this.initNotifications
        };
    },

    computed: {
        self() {
            return this;
        },
    },

    methods: {
        clear() {
            this.notifications = 0;
        }
    }
};
</script>

You can see that the component grew a bit - it requires a couple of extra bits to make it work. First, we have a weird template property that renders a slot that references the component itself. And then there's also a computed property which actually tells the component what is "self" in the template property. If you're familiar with Vue 3 - then you will note that the code above represents the "Options API" approach to Vue components. Composition API approach would look a bit different:

<script>
export default {
    template: `<div><slot v-bind="self"/></div>`,

    props: {
        initNotifications: Number,
    },

    setup(props, context) {
        return {
            self: {
                notifications: props.initNotifications,
                clear: () => {
                    this.notifications = 0;
                }
            }
        };
    }
};
</script>

Generally - similar, but do note that when using setup method in this way, we can't return everything directly - we have to wrap returned properties in a self object or the component when rendering a slot won't know what self means.

There are few other things that have to be taken into account when using default slot templates with composition API. For example, taking the code above - if you have some computed properties defined, then you need to refer to data directly - vm.myComputed.value in template. Overall computed is the same - just remember to use .value to access the data. Furthermore - any component that you previously used "inline-template" approach with, will have to be registered globally on the app.

Another unfortunate issue that comes with this approach to using default slots in Vue 3 - there will be a lot of warnings in your console log during development complaining about properties being used, while not defined on the components. Stuff like "class" and "style". I haven't really looked into that issue for now since otherwise the code works.