Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

jackFlick's avatar

Dropdown Component Issue using Vue and Inertia

I hope someone can help me with this problem i'm currently dealing right now.

I have dropdown component BaseFormSimpleSelect. The problem is when I use a list options from the database using a computed property when I select an item it will be tracked/highlighted then let's say I submitted the form and received validation error when I checked the dropdown list it is no longer highlighted.

In this example I will be using selectedRole, when I checked Vue dev tools the selectedRole object is properly assigned with the item I have selected before.

Here's the BaseFormSimpleSelect.vue component:

import { ref, watch, onMounted } from 'vue';
import { Listbox, ListboxLabel, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/vue';
import { CheckIcon, ChevronUpDownIcon } from '@heroicons/vue/24/solid';

const props = defineProps({
    items: {
        type: Array,
        default() {
            return [
                { id: 1, name: 'Item 1', icon: '' },
                { id: 2, name: 'Item 2', icon: '' },
                { id: 3, name: 'Item 3', icon: '' },
            ];
        },
    },
  initialSelectedItem: {
    type: Object,
    default: null,
    // default() {
    //     return { name: 'Select One', icon: '' };
    // }
  },
  label: {
    type: String,
    default: '',
  },
  withIcon: {
    type: Boolean,
    default: false,
  },
  hasError: {
    type: [String, Boolean],
    default: false,
  },
  customCss: {
    type: String,
    default: null,
  },
});

const initialDefaultItem = { id: null, name: 'Select One', icon: '' };
const initialDefault = ref(false);

const selectedItem = ref(props.initialSelectedItem);

const emit = defineEmits(['selectedItem', 'update:modelValue']);

watch(selectedItem, () => {
  emit('selectedItem', selectedItem.value);
  emit('update:modelValue', initialDefault.value ? null : selectedItem.value);
});

// Handle the initial default value after the component is mounted
onMounted(() => {
  if (selectedItem.value === null) {
    initialDefault.value = true;
    selectedItem.value = props.initialSelectedItem ? props.initialSelectedItem : initialDefaultItem;
  }
});
</script>

<template>
    <div>
        <Listbox v-model="selectedItem">
            <ListboxLabel
                v-if="label"
                class="block text-sm font-medium text-gray-700 mb-1 dark:text-white"
            >
                {{ label }}
            </ListboxLabel>
            <div class="relative">
                <ListboxButton
                    class="relative w-full bg-white rounded-md shadow-sm pl-3 pr-10 py-2 text-left text-base cursor-default border border-gray-300 transition-all duration-150 focus:outline-none focus:ring-4 focus:ring-primary focus:ring-opacity-30 focus:border-primary focus:border-opacity-40 dark:bg-gray-800 dark:text-white dark:border-slate-700"
                    :class="[
                        hasError
                            ? 'text-red-300 border-red-400 placeholder-red-300/90 focus:ring-red-500 focus:ring-opacity-20 focus:border-red-500 focus:border-opacity-40'
                            : 'text-gray-700 border-gray-300 placeholder-slate-400/90 focus:ring-primary focus:ring-opacity-20 focus:border-primary focus:border-opacity-40',
                        customCss
                    ]"
                >
                    <span class="flex items-center text-sm space-x-2">
                        <component
                            :is="selectedItem.icon"
                            v-if="withIcon"
                            class="h-5 w-5 stroke-[1.5]"
                        />
                        <span class="block truncate">
                            {{ selectedItem ? selectedItem.name : (initialDefault ? (initialSelectedItem && initialSelectedItem.name ? initialSelectedItem.name : 'Select One') : '') }}
                        </span>
                    </span>
                    <span class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
                        <ChevronUpDownIcon
                            class="h-5 w-5 text-gray-400"
                            aria-hidden="true"
                        />
                    </span>
                </ListboxButton>

                <transition
                    leave-active-class="transition ease-in duration-100"
                    leave-from-class="opacity-100"
                    leave-to-class="opacity-0"
                >
                    <ListboxOptions
                        class="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-56 rounded-md py-1 text-sm ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none dark:bg-slate-900 dark:text-slate-200 dark:border-slate-700"
                    >
                        <ListboxOption
                            v-for="item in items"
                            :key="item.name"
                            v-slot="{ active, selected }"
                            as="template"
                            :value="item"
                        >
                            <li :class="[active ? 'text-white bg-primary' : 'text-gray-700', 'cursor-default select-none relative py-2 pl-3 dark:text-slate-200']">
                                <div class="flex items-center space-x-2">
                                    <component
                                        :is="item.icon"
                                        v-if="withIcon"
                                        class="flex-shrink-0 h-5 w-5 rounded-full stroke-[1.5]"
                                    />
                                    <span :class="[selected ? 'font-medium' : 'font-normal', 'block truncate']">
                                        {{ item.name }}
                                    </span>
                                </div>

                                <span
                                    v-if="selected"
                                    :class="[active ? 'text-white' : 'text-primary', 'absolute inset-y-0 right-0 flex items-center pr-4']"
                                >
                                    <CheckIcon
                                        class="h-5 w-5"
                                        aria-hidden="true"
                                    />
                                </span>
                            </li>
                        </ListboxOption>
                    </ListboxOptions>
                </transition>
            </div>
        </Listbox>
    </div>
</template>

This is how I use it:

						<BaseFormSimpleSelect
                            :key="renderKey"
                            v-model="form.roles"
                            :items="rolesOptions"
                            label="Roles"
                            :has-error="form.errors.roles"
                            :initial-selected-item="selectedRoles"
                            @selected-item="updateSelected('roles', $event)"
                        />

And here's my script setup. I have already tried mapping the roles and using the rolesOption but still the same issue.

<script setup>
const props = defineProps({
    roles: {
        type: Array,
        required: true,
    },
});

const form = useForm({
    name: '',
    last_name: '',
    first_name: '',
    email: '',
    role: '',
});

const roles = computed(() => {
    return props.roles;
});

const rolesOptions = computed(() => {
    return roles.value.map((role) => {
        return {
            name: role.name,
            id: role.id,
        };
    });
});

function updateSelected(type, value) {
    if (type === 'role') {
        selectedRole.value = value;
        form.clearErrors('role');
    } 
}
</script>

Also, there's no issue when I only use this kind of list items const genders = [{ name: 'Male' }, { name: 'Female' }];

0 likes
2 replies
LaryAI's avatar
Level 58

It seems like the issue you're facing is related to the state of the selectedItem not being preserved after a form submission that results in a validation error. This can happen if the state of the Vue component is reset or if the selectedItem is not properly bound to the v-model directive.

Here's a solution that ensures the selectedItem is properly bound and updated:

  1. Make sure that the v-model directive in the BaseFormSimpleSelect component is correctly bound to the selectedItem ref.

  2. Ensure that the selectedItem is updated when the initialSelectedItem prop changes. This can be done by watching the initialSelectedItem prop.

  3. When you receive a validation error, make sure that the selectedItem is not reset to its initial state unless that's the intended behavior.

Here's the updated BaseFormSimpleSelect.vue component with the necessary changes:

<script setup>
import { ref, watch, onMounted, watchEffect } from 'vue';
import { Listbox, ListboxLabel, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/vue';
import { CheckIcon, ChevronUpDownIcon } from '@heroicons/vue/24/solid';

const props = defineProps({
  // ... other props
  initialSelectedItem: {
    type: Object,
    default: null,
  },
});

const selectedItem = ref(props.initialSelectedItem);

const emit = defineEmits(['update:modelValue']);

// Watch for changes in initialSelectedItem prop
watch(() => props.initialSelectedItem, (newValue) => {
  selectedItem.value = newValue;
}, { immediate: true });

// Emit the selected item whenever it changes
watch(selectedItem, (newValue) => {
  emit('update:modelValue', newValue);
});

// ... rest of the script
</script>

<template>
  <!-- ... rest of the template -->
</template>

And in the parent component where you use BaseFormSimpleSelect, make sure to bind the v-model correctly:

<BaseFormSimpleSelect
  :key="renderKey"
  v-model="selectedRole" <!-- Make sure this is the correct model -->
  :items="rolesOptions"
  label="Roles"
  :has-error="form.errors.roles"
  :initial-selected-item="selectedRoles"
  @selected-item="updateSelected('roles', $event)"
/>

In the script setup of the parent component, ensure that selectedRole is a ref and is used as the model for the BaseFormSimpleSelect component:

<script setup>
// ... other imports
import { ref } from 'vue';

// ... other setup code

const selectedRole = ref(null); // Initialize the selectedRole ref

// ... rest of the script
</script>

Make sure that selectedRole is properly initialized with the value you want to be selected by default. If the selectedRole should retain its value after a validation error, ensure that it's not being reset in your form submission logic.

By following these steps, the BaseFormSimpleSelect component should correctly track and highlight the selected item, even after a form submission with validation errors.

Please or to participate in this conversation.