vue.jsvuejs3vuejs3-composition-api

Keep passed slots to a slotted component via a functional wrapper


Playground

I have the following:

<script setup>
import { ref } from 'vue'
import Input from './Input.js'
import Text from './Text.vue'

const msg = ref('Hello World!')
</script>

<template>
  <Input label="Message">
    <Text v-model="msg">
      <template #icon>❤️</template>
    </Text>
  </Input>
</template>

Input.js:

import {h} from 'vue';
export default function Input({label}, {slots}){
  return slots.default().map(vnode => h(vnode, {}, {label: () => label}))
};

Text.vue:

<script setup>
const model = defineModel();
</script>

<template>
  <div :class="{iconed: $slots.icon}">
    <div v-if="$slots.label"><slot name="label"/></div>
    <span v-if="$slots.icon" class="icon"><slot name="icon"/></span>
    <input v-model="model"/>
  </div>
</template>

Any way keep the icon slot for Text? I know that I can put the slot into Input and forward it the Text but I would rather keep it like in the template to state explicitly that the slot belongs to Text.


Solution

  • h(vnode) is a reliable but not documented alternative to cloneVNode which merges props but not children.

    { label: ... } overrides VNode's children, this needs to be additionally taken care of:

    return slots.default().map(vnode => {
      const children = Object.fromEntries(
        Object.entries(vnode.children || {})
          .filter(([name]) => !name.includes('_'))
      );
    
      return h(vnode, {}, { ...children, label: () => label });
    });
    

    Not filtering out internal underscored properties from children may result in bugs.