[Vuejs]-How do I make a vue component that accepts any prop?

1👍

Here is how I did it using a SFC with TypeScript based on @moritz’s answer

<template>
  <span
    class="material-symbols-outlined icon"
    :class="classObject"
    :style="style"
    >{{ name }}</span>
</template>
<script lang="ts" setup>
import { StyleValue, useAttrs } from "vue";

const { classObject, styleAttr, ...attrs } = useAttrs();
const style = styleAttr as StyleValue;
const name = Object.keys(attrs)[0];
</script>

This implementation does what I needed plus adds support for style and class passthroughs.

Here’s an improved one using @mdi/js to avail of tree-shaking

<template>
  <svg role="img" viewBox="0 0 24 24" :width="size" :height="size">
    <title v-if="alt">{{ alt }}</title>
    <path :d="path" />
  </svg>
</template>
<style scoped>
path {
  fill: currentColor;
}
</style>
<script lang="ts" setup>
import Icons from "./icons";
import { useAttrs } from "vue";
const { alt, size, name } = withDefaults(
  defineProps<{ alt?: string; size?: string | number; name?: string }>(),
  {
    size: 24,
  }
);
const attrs = useAttrs();
const iconName = name ?? Object.keys(attrs)[0];
const path = Icons[iconName] ?? Icons.mdiHelpBoxOutline;
</script>

It needs a separate import to pick specific icons

// https://pictogrammers.com/library/mdi/
// don't load the whole thing just add the ones that we require over time.
import {
  mdiChevronRight,
  mdiFacebook,
  mdiHelpBoxOutline,
  mdiInstagram,
  mdiLinkedin,
  mdiTwitter,
} from "@mdi/js";
// In order to pass Typechecks, the imports need to be copied here.
export default {
  mdiChevronRight,
  mdiFacebook,
  mdiHelpBoxOutline,
  mdiInstagram,
  mdiLinkedin,
  mdiTwitter,
} as Record<string, string>;

4👍

I think props have to have a fixed name, so that vue can bind it. If you use an attribute that is not bound by props, it will land in attrs, and you can access it from there.

However, if there are multiple attributes, the hard part is figuring out, which one describes the icon.

Another possibility is to use a slot, i.e.

<Icon>navigate_next</Icon>

Here is a simple example:

const { createApp, h } = Vue;

const IconFromAttrs = {
  setup(props, { attrs, slots, emit, expose }) {
    return () => [           
      h('span', {class: 'mdi mdi-36px mdi-' + Object.keys(attrs)[0]} ),
    ]
  }
}

const IconFromSlot = {
  setup(props, { attrs, slots, emit, expose }) {
    return () => [           
      h('span', {class: 'mdi mdi-36px mdi-' + slots.default()[0].children}),
    ]
  }
}


const App = { 
  components: { IconFromSlot, IconFromAttrs },
  data() {
    return {
    }  
  }
}
const app = createApp(App)
app.mount('#app')
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/@mdi/font@5.4.55/css/materialdesignicons.min.css">

<div id="app">

  <div>
    From attrs:
    <icon-from-attrs pig/>
  </div>

  <div>
    From slot:
    <icon-from-slot>dog</icon-from-slot>
  </div>
</div>

<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>

Neither slots nor attrs can be automatically typed, so you will have to provide types explicitly with as string.

Vuetify’s v-icon uses the approach with slots. Feels wrong to post their code here, but you can look at it in GitHub, it’s the slotIcon starting at line 35. This should give you an idea what checks are needed.

0👍

Basically you can pass the properties of an object without using any attribute in the component. Here is the documentation.

You can use v-bind without an argument.

<Icon v-bind="navigate_next" />

In Script :

navigate_next: {
  name: 'xyz'
}

Will be equivalent to :

<Icon :name="navigate_next.name" />

Leave a comment