EN VI

Javascript - How to create a custom select component with options passed as HTML instead of props in Vue 3?

2024-03-16 18:30:06
Javascript - How to create a custom select component with options passed as HTML instead of props in Vue 3?

I am trying to create a custom select component that can accept options as HTML like this:

<MySelect v-model='value'>
  <MyOption value='1'>Option 1</MyOption>
  <MyOption value='2'>Option 2</MyOption>
</MySelect>

This is my current implementation of the custom select component:

<template>
    <div class="select">
        <div class="text" @click="visible=!visible">{{modelValue ?? text}} <IconChevronDown/></div>
        <div class="options" v-if="visible">
            <span v-for="option in options" class="p-4 hover:bg-neutral-1 cursor-pointer capitalize" @click="select(option)">{{option}}</span>
        </div>
    </div>
</template>

<script setup>
const visible = ref(false)
const props = defineProps({
    text: String,
    options: Array,
    modelValue: String
})
const emit = defineEmits(["update:modelValue"])

function select(option){
    visible.value = false;
    emit("update:modelValue", option)
}
</script>

This component accepts options as a prop and just as an array of strings. But I need more customization of options, such as adding an icon or applying styles to text. So, the ability to pass options as HTML would have solved this problem. I would appreciate if you could share your ideas of how this can be implemented!

P.S. MySelect component should not contain native select and option tags. The whole purpose of creating a custom select component is for design customization.

Solution:

You can create a custom slot render function and collect default slots of the slotted child components:

VUE SFC PLAYGROUND

Usually I would prefer the scoped slot solution posted in my other answer here, BUT nobody stops from MERGING 2 solutions into 1: using the default slot from here and #option from the other answer. So the select component could be fed by both slots.

<script setup>
import { ref } from 'vue'
import MySelect from './MySelect.vue';
import MyOption from './MyOption.vue';

const selected = ref();
</script>

<template>
  <my-select v-model="selected">
    <my-option value="1"><span style="color:red">Red option</span></my-option>
    <my-option value="2"><span style="color:blue">Blue option</span></my-option>
  </my-select>
</template>

MySelect.vue

<template>
    <div class="select">
        <div class="text" @click="visible=!visible" style="cursor:pointer">
            <component v-if="modelValue" :is="options[modelValue]"/>
            <template v-else>{{ text }}</template>
        </div>
        <div class="options" v-if="visible">
            <render-options></render-options>
        </div>
    </div>
</template>

<script setup>
import {ref, useSlots} from 'vue';
const visible = ref(false)
const props = defineProps({
    text: {type: String, default: 'Toggle dropdown'},
    options: Array,
    modelValue: String
})
const options = ref({});
const emit = defineEmits(["update:modelValue"])

const $slots = useSlots();
const renderOptions = () => {
    options.value = {};
    return $slots.default()
    .map(vnode => {
        // collection options' default slot
        options.value[vnode.props.value] = vnode.children.default;
        Object.assign(vnode.props ??= {}, {onClick: () => select(vnode.props.value)}, vnode.props ?? {});
        return vnode;
    });
}

function select(option){
    visible.value = false;
    emit("update:modelValue", option)
}
</script>

MyOption.vue

<script setup>

</script>

<template>
  <div class="option">
    <slot></slot>
  </div>
</template>
<style scoped>
.option{
  padding: 5px 10px;
  border: 1px solid gray;
  border-radius: 4px;
  cursor: pointer;
}
</style>
Answer

Login


Forgot Your Password?

Create Account


Lost your password? Please enter your email address. You will receive a link to create a new password.

Reset Password

Back to login