Skip to main content

Dynamically set components in `v-for` loop.

How to deal with mixed elements (buttons and nuxt-links) in a dynamic navigation and what to do when you need some items to render as a button with an `@click` function and some to navigate to another page.

Introduction

Recently I was working with a dynamic list of items that were being used to generate a navigation menu. At least that's how it started: links to other destinations. This soon turned into a mixture of these and actions that would trigger some modals or perform some other functions. I had started with a small list and a simple v-for loop:

<nuxt-link v-for="(item, index) in list" :key="index" :to="item.destination">
  {{ item.label }}
</nuxt-link>
<script>
export default {
  data: () => ({
    list: [
      {
        label: "Home",
        destination: "/",
      },
      {
        label: "Blog",
        destination: '/blog',
      },
    ],
  }),
};
</script>

Then the introduction of alternative actions happened. Now there is an argument to say that if this navigation menu has 2 different actions for items that look similar then this is bad UX: a user clicks 2 buttons in a navigation menu that looks the same, only the text is different, then the action that follows should be the same? Well, tell that to the client.

In this case, the 2 actions will be: Go to a new page or open a new thing in a modal.

Those 2 actions are not too different and a lot of people are happy with that being the case so we go with it.

Now to figure out how to make this work so we can have links and functions without going crazy with doing checks, and nesting components.

Now we can easily switch everything to divs, assign the correct roles, add some aria attributes. But we don't want to do that. We want to use <a></a> for links (or at least <nuxt-link></nuxt-link>) and <button></button> for items that have actions associated. The best way to think about it is that a link goes somewhere else, and a button does something here.

dynamic menu with some links and some buttons. some take you to new tabs or pages and some will open modals or take actions.

<script>
export default {
  data: () => ({
    list: [
      {
        label: "Home",
        component: "nuxt-link",
        destination: "/",
        click: false,
      },
      {
        label: "Test Alert",
        component: "button",
        destination: false,
        click: () => {
          alert("test");
        },
      },
    ],
  }),
};
</script>

Component component

Well, I figured the best option is to use the built-in <component></component> component. This component lets us pass in the component we want. In our data we can define the component we want using a string in this case:

[
  { label: "Home", component: "nuxt-link" },
  { label: "Page 2", component: "nuxt-link" },
  {
    label: "Modal",
    component: "button",
  },
];

and in our vue template:

<component
  v-for="(item, index) in list"
  :is="item.component"
  :key="index"
></component>

Now, this only gets half of the work done. We want the nuxt link, and we want the button because that is what the HTML spec says we should use. It gives us semantic roles and accessibility elements that are meaningful for all kinds of good reasons. But now we have to pass the destination or the function.

Final Code

<script>
export default {
  data: () => ({
    list: [
      {
        label: "Home",
        component: "nuxt-link",
        destination: "/",
        click: false,
      },
      {
        label: "Blog",
        component: "nuxt-link",
        destination: "/blog",
        click: false,
      },
      {
        label: "Test Alert",
        component: "button",
        destination: false,
        click: () => {
          alert("test");
        },
      },
    ],
  }),
};
</script>
<component
  v-for="(item, index) in list"
  :is="item.component"
  :key="index"
  :to="item.destination"
  @click="item.click"
>
  {{ item.label }}
</component>

The destination is set to false on items that are not the nuxt link means the :to="" will not be rendered, and for items where there is no function necessary passing in false to the click handler means nothing gets triggered and no error is produced by passing in nothing or something undefined.

This is the best option I have found for mixed types of content from a single list where the order is important. There are likely other ways of doing this but this is one I worked with recently.