Render Functions, Icons, and Badges With Vuetify

Steve | April 15th, 2020

In the Vue app that I'm currently working on, I ran into a case where I had to use two tools that are, shall we say, not my strongest: render functions and slots. Fortunately, I was able to figure out how to do what I needed to do, and I'm here to share about it.

My use case is that I need to display an icon with a badge (yes, cue the classic movie line about how we don't need no stinking badges) that contains a number. If I could just do it with the default HTML tags, it wold be easy. Here is the HTML from the Vuetify docs:

<v-badge left>
<template v-slot:badge>
<span>6</span>
</template>
<v-icon
large
color="grey lighten-1"
>
shopping_cart
</v-icon>
</v-badge>

However, in this app, the bottom half of every page is a data table that uses a Vuetify v-data-table component to list data, and each column uses a functional component to render the content.

<script>
export default {
name: 'BaseTableColumn',
props: {
format: Function,
item: Object,
index: String,
},
methods: {
getText() {
return this.item[this.index];
},
},
render(createElement) {
if (this.$props.format) {
return this.$props.format(this.item, this.index, createElement);
}
return createElement('div', this.getText());
},
};
</script>

So in this case, where I need to generate the markup is in this.$props.format, where format is an object key.. As you can see from the arguments passed above, the signature is

format: (item, index, createElement) => {
// rendering code goes here.
});

As you can see at my initial Stackoverflow post, this got me pretty close:

format: (item, index, createElement) => {
  const propsObj = {
    attrs: {
      color: 'blue',
    },
    props: {
      overlap: true,
      left: true,
    },
    slots: {
      badge: 'dummy',
    },
  };
  return createElement('v-badge', propsObj, [createElement('v-icon', { attrs: { color: 'success', large: true } }, 'account_circle')]);
}

but I still wasn't getting the badge or badge content, even though I could see in my markup that the outer <span> for the badge was there around the icon elements. Thanks to my buddy  - and fellow Views on Vue panelist - Austin Gil (aka Stegosource), I was able to get it working. Here's the final working code:

format: (item, index, createElement) => {
const propsObj = {
props: {
overlap: true,
left: true,
color: 'success',
},
};
const span = createElement('span', { slot: 'badge' }, '5');
const icon = createElement('v-icon', { props: { color: 'warning', large: true } }, 'account_circle');
return createElement('v-badge', propsObj, [span, icon]);
}

What we did was break the render functions in parts to match the desired markup. Looking at the markup, there are three parts:

  1. The outer v-badge
  2. The inner badge slot
  3. The inner v-icon element

So first, we use createElement to generate the badge slot:

const span = createElement('span', { slot: 'badge' }, '5');

Next, we generate the markup for the icon:

const icon = createElement('v-icon', { props: { color: 'warning', large: true } }, 'account_circle');

And finally we put it together, passing the span and icon values created above, also passing in propsObj with attributes for the v-badge element.

return createElement('v-badge', propsObj, [span, icon]);

Here is the end result:

icon_with_badge.png

From a coding standpoint, now I just need to control the value that is displayed in the badge, but that's trivial (and outside the scope of this post).

Thanks for reading, and happy coding.