Joseph
Joseph

Reputation: 2712

Dynamically render Vue component within another component

Using Vue 3.2.41 - @heroicons/vue 2.0.14 - inertiajs 1.0 - vite 4.0.0

I'm calling a Vue component using this:

<TimelineItem icon="CalendarDaysIcon" />

The component looks like this:

<template>
    <component :is="icon" /> <!-- doesn't work -->
    <CalendarDaysIcon /> <!-- works -->
</template>

<script setup>
    import {
        CalendarDaysIcon,
    } from '@heroicons/vue/20/solid'

    const props = defineProps(['icon'])
</script>

The HTML being rendered is like this:

<calendardaysicon></calendardaysicon> <!-- not what I want -->
<svg> ... </svg> <!-- correct but not dynamic -->

In other words, the <component :is /> is just rendering some empty <calendardaysicon> tags when I'd expect it to render the component. I can see that it has made it lowercase and have no idea how to force it back to PascalCase and I'm not even sure if that would help the situation.

I've simplified the code somewhat, but the full version would have a list of 10 different icons (all part of the Heroicons package which uses PascalCase names) which I'd like to be able to call easily from the main component.

Upvotes: 2

Views: 447

Answers (3)

Joseph
Joseph

Reputation: 2712

Using <component :is="icon" /> is only using a string containing CalendarDaysIcon

Instead, in the main component, pass the actual component reference like this:

<template>
  <TimelineItem :icon="CalendarDaysIcon" />
</template>

<script setup>
    import {
        CalendarDaysIcon,
    } from '@heroicons/vue/20/solid'

    const props = defineProps(['icon'])
</script>

Then, in the TimelineItem component, there is no need to reference any icons:

<template>
    <component :is="icon" /> <!-- now works -->
</template>

<script setup>
    const props = defineProps(['icon'])
</script>

Thanks to @Robert Boes on the Inertia Discord server for the guidance.

Upvotes: 0

Tolbxela
Tolbxela

Reputation: 5183

const { createApp, createElementVNode, openBlock, createElementBlock } = Vue;

const CalendarDaysIcon = {  
  render(_ctx, _cache) {
  return (openBlock(), createElementBlock("svg", {
    xmlns: "http://www.w3.org/2000/svg",
    viewBox: "0 0 20 20",
    fill: "currentColor",
    "aria-hidden": "true"
  }, [
    createElementVNode("path", { d: "M5.25 12a.75.75 0 01.75-.75h.01a.75.75 0 01.75.75v.01a.75.75 0 01-.75.75H6a.75.75 0 01-.75-.75V12zM6 13.25a.75.75 0 00-.75.75v.01c0 .414.336.75.75.75h.01a.75.75 0 00.75-.75V14a.75.75 0 00-.75-.75H6zM7.25 12a.75.75 0 01.75-.75h.01a.75.75 0 01.75.75v.01a.75.75 0 01-.75.75H8a.75.75 0 01-.75-.75V12zM8 13.25a.75.75 0 00-.75.75v.01c0 .414.336.75.75.75h.01a.75.75 0 00.75-.75V14a.75.75 0 00-.75-.75H8zM9.25 10a.75.75 0 01.75-.75h.01a.75.75 0 01.75.75v.01a.75.75 0 01-.75.75H10a.75.75 0 01-.75-.75V10zM10 11.25a.75.75 0 00-.75.75v.01c0 .414.336.75.75.75h.01a.75.75 0 00.75-.75V12a.75.75 0 00-.75-.75H10zM9.25 14a.75.75 0 01.75-.75h.01a.75.75 0 01.75.75v.01a.75.75 0 01-.75.75H10a.75.75 0 01-.75-.75V14zM12 9.25a.75.75 0 00-.75.75v.01c0 .414.336.75.75.75h.01a.75.75 0 00.75-.75V10a.75.75 0 00-.75-.75H12zM11.25 12a.75.75 0 01.75-.75h.01a.75.75 0 01.75.75v.01a.75.75 0 01-.75.75H12a.75.75 0 01-.75-.75V12zM12 13.25a.75.75 0 00-.75.75v.01c0 .414.336.75.75.75h.01a.75.75 0 00.75-.75V14a.75.75 0 00-.75-.75H12zM13.25 10a.75.75 0 01.75-.75h.01a.75.75 0 01.75.75v.01a.75.75 0 01-.75.75H14a.75.75 0 01-.75-.75V10zM14 11.25a.75.75 0 00-.75.75v.01c0 .414.336.75.75.75h.01a.75.75 0 00.75-.75V12a.75.75 0 00-.75-.75H14z" }),
    createElementVNode("path", {
      "fill-rule": "evenodd",
      d: "M5.75 2a.75.75 0 01.75.75V4h7V2.75a.75.75 0 011.5 0V4h.25A2.75 2.75 0 0118 6.75v8.5A2.75 2.75 0 0115.25 18H4.75A2.75 2.75 0 012 15.25v-8.5A2.75 2.75 0 014.75 4H5V2.75A.75.75 0 015.75 2zm-1 5.5c-.69 0-1.25.56-1.25 1.25v6.5c0 .69.56 1.25 1.25 1.25h10.5c.69 0 1.25-.56 1.25-1.25v-6.5c0-.69-.56-1.25-1.25-1.25H4.75z",
      "clip-rule": "evenodd"
    })
  ]))
}
}

const TimeLineItem = {   
  props: ['icon'],
  components: {
    CalendarDaysIcon
  },
  template: '<component :is="icon" class="icon"/>'
}

const App = {
  components: { 
    TimeLineItem
  } 
}

const app = createApp(App)
app.mount('#app')
.icon {
    width: 36px;
    height: 36px;
}
<div id="app">  
  <time-line-item icon="CalendarDaysIcon"></time-line-item>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>

Upvotes: 0

bilal
bilal

Reputation: 296

If you are authoring your templates directly in a DOM (e.g. as the content of a native element), the template will be subject to the browser's native HTML parsing behavior. In such cases, you will need to use kebab-case and explicit closing tags for components https://vuejs.org/guide/essentials/component-basics.html#dom-template-parsing-caveats

Try: <calendar-days-icon> </calendar-days-icon>

Upvotes: 2

Related Questions