Component within component possible?

When writing a component I often have the need to reuse small parts of logic.

As an example:
I get an integer from API and want to display a string instead.

I can do that with a small function in my code:

const mapIntegerToString = (i: number) => {
  if (i === 0) return 'foo'
  if (i === 1) return 'bar'
  if (i === 2) return 'baz'
  return 'unknown: ' + i
}

and use this function multiple times in the template

{{ mapIntegerToString(1) }}

Great :+1:
It keeps logic that is only relevant for this component within the component.

However, as soon as I don’t want to return a string, but something that requires a component, e.g. an icon, that does not work any more and I have to write a “sub component” in a separate file.

E.g.

<template>
  <span>
  <FooIcon v-if="i === 0" />
  <BarIcon v-if="i === 1" />
  <BazIcon v-if="i === 2" />
  </span>
</template>

<script lang="ts" setup>
defineProps<{ i: number }>()
</script>

What I’d rather do is in my script part:

const mapIntegerToComponent = (i: number) => {
  if (i === 0) return <FooIcon>
  if (i === 1) return <BarIcon>
  if (i === 2) return <BazIcon>
  return <UnknownIcon>
}

and use that in my template as a component. Is that somehow possible?

I’ve looked into functional components and render functions, but as far as I understand it, this is only relevant if I don’t use a template in my component at all.

I think you are looking for the special <component> element in Vue.

1 Like

Thanks for your comment.

For this simplified example, it helps, you’re right. But assume my real world problem is a bit more complex. E.g. I also want to pass different properties to the different components. I would then have to write multiple functions mapIntegerToComponentName, mapIntegerToComponentPropX, mapIntegerToComponentPropY and invoke all of them in the template. Frankly speaking that doesn’t look like a good structure to me.

I have also cases where it’s not just a single component:

// ...
if (a) return <FooIcon><span>Foo</span>
if (b) return <BarIcon>
// ...

Is there no way of programmatically defining a component in a script that references other components?

Well, somewhere you have to write code that tells the computer ComponentA should be handled like this, ComponentB should be handled like that, etc. If you are lucky, you can generalize and come up with a general solution that can handle all your components the same way. If you can’t, well then you have to write a specific solution for each component.

But you say that each component should be passed different props. Is it not possible to “package” all the props in one and the same object, and pass that as a single prop to the components? And then each component “unpackage” the object and obtains the different props that way?

1 Like

I don’t think that’s possible is I don’t have control over the components want to display (these are 3rd party libraries).

Maybe let’s try with a better example. I want to render traffic lights multiple times in the parent component:

// Parent.vue
<template>
...
<TrafficLight color="green" />
...
<TrafficLight v-for=... :color="color" />
...
</template>

The traffic light is defined in a separate component:

// TrafficLight.vue
<template>
  <span>
    <span v-if="color === 'green'"><CircleIcon color="#00ff00" /> All good, let's go!</span>
    <span v-if="color === 'yellow'"><SomeFancyAnimation>Please wait...</SomeFanceAnimation></span>
    <span v-if="color === 'red'"><Octagon size="large" />Stop!<Octagon size="large" /></span>
  </span>
</template>

<script lang="ts" setup>
defineProps<{ color: string }>()
</script>

I’m looking for a solution that I can embed this logic contained in TrafficLight.vue into Parent.vue (because it’s only used there and I want to have it close together) without duplicating the code, of course.

Ideally I could write something like (pseudo-code):

// Parent.vue
<template>
...
<TrafficLight color="green" />
...
<TrafficLight v-for=... :color="color" />
...
</template>

<script setup lang="ts">
const TrafficLight = (color: string) => {
  if (color === 'green') return <CircleIcon color="#00ff00" />All good, let's go!
  if (color === 'yellow') return <SomeFancyAnimation>Please wait...</SomeFanceAnimation>
  if (color === 'red') return <Octagon size="large" />Stop!<Octagon size="large" />
}
</script>

I don’t think, this is possible using <component>, is it? Or how would the template/script look like?

As far as I know, then no, you can’t do anything like that in Vue. But you can use something like <component :is="getComponentType()" v-bind="getPropsAsJsObject()">, but that just feels weird IMO, and you would need to duplicate all this each time you want to use it.

To get the two components close together, you can create two components in one and the same file, but it hasn’t been designed to support it, so it’s a bit ugly:

Other than that, I don’t know.

1 Like