loading...
Cover image for Seamless Vue components

Seamless Vue components

aminnairi profile image Amin ・3 min read

We all had a case where we needed to reuse a lot of HTML code. Maybe a form containing multiple inputs with their label. That's why we would create a component for that.

import Vue from "https://unpkg.com/vue/dist/vue.esm.browser.js";

const InputLabel = {
    template: `
        <label>
            <span>
                <slot></slot>
            </span>
            <input type="text">
        </label>
    `
};

new Vue({
    el: "#vue",
    components: {
        InputLabel
    },
    template: `
        <input-label>First Name</input-label>
    `
});

But what if you need to listen for the enter keypress on the input? Naturally, we would like to add the event on the component we added.

import Vue from "https://unpkg.com/vue/dist/vue.esm.browser.js";

const InputLabel = {
    template: `
        <label>
            <span>
                <slot></slot>
            </span>
            <input type="text">
        </label>
    `
};

new Vue({
    el: "#vue",
    components: {
        InputLabel
    },
    template: `
        <input-label @keydown.enter="keydownEnterHandler">First Name</input-label>
    `,
    methods: {
        keydownEnterHandler() {
            alert("enter key pressed!");
        }
    }
});

Aaaaaaand... That would not work. Because you need to attach the keydown event to the template of the component, and not the component itself. So, for that, you can use the native modifier.

import Vue from "https://unpkg.com/vue/dist/vue.esm.browser.js";

const InputLabel = {
    template: `
        <label>
            <span>
                <slot></slot>
            </span>
            <input type="text">
        </label>
    `
};

new Vue({
    el: "#vue",
    components: {
        InputLabel
    },
    template: `
        <input-label @keydown.enter.native="keydownEnterHandler">First Name</input-label>
    `,
    methods: {
        keydownEnterHandler() {
            alert("enter key pressed!");
        }
    }
});

And... Success! This now works as expected. But what if we want to add some custom attributes to our input? This would be great if the native modifier would work like events, right?

import Vue from "https://unpkg.com/vue/dist/vue.esm.browser.js";

const InputLabel = {
    template: `
        <label>
            <span>
                <slot></slot>
            </span>
            <input type="text">
        </label>
    `
};

new Vue({
    el: "#vue",
    components: {
        InputLabel
    },
    template: `
        <input-label
            @keydown.enter.native="keydownEnterHandler"
            value.native="John DOE">
            First Name
        </input-label>
    `,
    methods: {
        keydownEnterHandler() {
            alert("enter key pressed!");
        }
    }
});

Unfortunately, this won't. Eventually, we would like to not be forced to add that native each time we want to use our events, since we have no custom events at all. There is a solution for that my friends, actually two solutions which are.

$listeners & $attrs

In each Vue.js components, we have access to two special variables which are $listeners & $attrs.

$listeners will contain all events that are attached to the components that would normally be used. For instance, custom events like @thingyChanged. Whereas $attrs will contain all custom attributes that the components will receive like small-column.

Let's see how to use them in our component.

import Vue from "https://unpkg.com/vue/dist/vue.esm.browser.js";

const InputLabel = {
    template: `
        <label>
            <span>
                <slot></slot>
            </span>
            <input
                type="text"
                v-on="$listeners"
                v-bind="$attrs">
        </label>
    `
};

new Vue({
    el: "#vue",
    components: {
        InputLabel
    },
    template: `
        <input-label
            @keydown.enter="keydownEnterHandler"
            value="John DOE">
            First Name
        </input-label>
    `,
    methods: {
        keydownEnterHandler() {
            alert("enter key pressed!");
        }
    }
});

That's it! We don't need to add the native modifier anymore to our events, they will work seamlessly. Meaning that all future attributes & listeners will be bound to the <input> element.

This also means that our value attribute, which didn't work earlier anyway, now does. Thanks to those magic keywords.

As you may notice, I used a weird alternative syntax to the classic v-on:something="something" syntax. Actually, you can use v-on without any arguments. It will receive an object containing your wanted events. In fact, v-on:click="something" is just syntactic sugar to v-on="{click: something}". Same thing goes for v-bind.

It is also worth mentioning that @click is equivalent to v-on:click.

You can now enjoy writing little atomic components that will react the way you expect them to do in your applications.

Why would I need to do that?

Components should be simple and no-brainers. If we wanted to actually use our value attribute, we would have to define a props for that matter. Imagine doing that for fifteen, or more attributes & events. This solution does not scale. But using $listeners & $attrs does scale, a lot.

Let me know what kind of component refactoring you are intending to do so we can discuss about them in the comment section. Do no hesitate to ask me if you are still struggling to understand some part of that article, I'll be glad to help you understand it.

Thanks for reading and see you next time!

Posted on Mar 12 by:

Discussion

markdown guide