N
Nuxt7d ago
Angh0r

[NuxtUI] InputMenu as Search window

Hi, I'm trying to use InputMenu for search bar in app. I'd like to display limited list of results in dropdown but I don't want dropdown to be open until there are at least 3 characters and fetch is initiated and done. I've tried mix "control open state" example with "witch fetched items", but dropdown is opening anyway. Did I pick a wrong tool or there is no way to block dropdown in any way?
14 Replies
kapa.ai
kapa.ai7d ago
Hi @Angh0r. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave: ---------------------- You're on the right track with using InputMenu for a search bar, but controlling when the dropdown opens requires some specific handling. Based on the knowledge sources, you can control the open state of the InputMenu component using the v-model:open directive. Here's how you can implement a solution that only opens the dropdown when there are at least 3 characters and the fetch is complete:
<script setup lang="ts">
const searchTerm = ref('')
const open = ref(false)
const items = ref([])
const loading = ref(false)

// Watch for changes to the search term
watch(searchTerm, async (newValue) => {
// Only fetch and open dropdown if search term is at least 3 characters
if (newValue.length >= 3) {
loading.value = true

// Fetch your data here
const { data, status } = await useFetch('your-api-endpoint', {
params: { q: newValue },
// Your transform logic here
})

items.value = data.value || []
loading.value = false

// Only open the dropdown when fetch is complete and we have results
open.value = items.value.length > 0
} else {
// Close the dropdown if search term is less than 3 characters
open.value = false
items.value = []
}
})
</script>

<template>
<UInputMenu
v-model="searchTerm"
v-model:open="open"
:items="items"
:loading="loading"
ignore-filter
placeholder="Search..."
/>
</template>
<script setup lang="ts">
const searchTerm = ref('')
const open = ref(false)
const items = ref([])
const loading = ref(false)

// Watch for changes to the search term
watch(searchTerm, async (newValue) => {
// Only fetch and open dropdown if search term is at least 3 characters
if (newValue.length >= 3) {
loading.value = true

// Fetch your data here
const { data, status } = await useFetch('your-api-endpoint', {
params: { q: newValue },
// Your transform logic here
})

items.value = data.value || []
loading.value = false

// Only open the dropdown when fetch is complete and we have results
open.value = items.value.length > 0
} else {
// Close the dropdown if search term is less than 3 characters
open.value = false
items.value = []
}
})
</script>

<template>
<UInputMenu
v-model="searchTerm"
v-model:open="open"
:items="items"
:loading="loading"
ignore-filter
placeholder="Search..."
/>
</template>
The key points in this implementation: 1. Use v-model:open="open" to control the dropdown state as shown in the control open state example 2. Set ignore-filter="true" to disable the internal filtering since you're handling the search on your own 3. Only set open.value = true after the fetch is complete and you have results 4. Explicitly set open.value = false when the search term is less than 3 characters This approach gives you full control over when the dropdown opens, ensuring it only appears when you have valid search results after the minimum character threshold is met. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai7d ago
<@439284866610233345> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
Angh0r
Angh0rOP7d ago
@kapa.ai unfortunately v-model doesn't seems to work. v-model:open works fine and thah value is reactive, but v-model is showing an error: Type 'string' is not assignable to type 'undefined'.
kapa.ai
kapa.ai7d ago
Let me check the knowledge base for you again. One moment! :books: ---------------------- I see the issue you're encountering with the InputMenu component. The error "Type 'string' is not assignable to type 'undefined'" suggests there's a type mismatch with the v-model binding. Let's fix this based on the InputMenu documentation. Looking at the InputMenu component documentation, there are a few important details to note about how v-model works with this component: When using InputMenu with an array of objects, the component expects the whole object to be passed to the v-model by default, not just a string value. This is different from how a regular Input component works. Here's how you can fix your implementation:
<script setup lang="ts">
const searchTerm = ref('') // This is for the search term
const open = ref(false)
const items = ref([])
const selectedValue = ref() // This should be initialized without a type or as null/undefined initially
const loading = ref(false)

// Watch for changes to the search term
watch(searchTerm, async (newValue) => {
if (newValue.length >= 3) {
loading.value = true

// Fetch your data here
// ...fetch logic...

items.value = data.value || []
loading.value = false
open.value = items.value.length > 0
} else {
open.value = false
items.value = []
}
})
</script>

<template>
<UInputMenu
v-model:search-term="searchTerm"
v-model="selectedValue"
v-model:open="open"
:items="items"
:loading="loading"
ignore-filter
placeholder="Search..."
/>
</template>
<script setup lang="ts">
const searchTerm = ref('') // This is for the search term
const open = ref(false)
const items = ref([])
const selectedValue = ref() // This should be initialized without a type or as null/undefined initially
const loading = ref(false)

// Watch for changes to the search term
watch(searchTerm, async (newValue) => {
if (newValue.length >= 3) {
loading.value = true

// Fetch your data here
// ...fetch logic...

items.value = data.value || []
loading.value = false
open.value = items.value.length > 0
} else {
open.value = false
items.value = []
}
})
</script>

<template>
<UInputMenu
v-model:search-term="searchTerm"
v-model="selectedValue"
v-model:open="open"
:items="items"
:loading="loading"
ignore-filter
placeholder="Search..."
/>
</template>
The key changes: 1. Use v-model:search-term="searchTerm" to control the input text as shown in the control search term example 2. Use v-model="selectedValue" for the selected item 3. Keep v-model:open="open" to control the dropdown visibility If you're working with an array of strings and want to bind to a string value, you can also use the value-key prop to specify which property of the object should be bound:
<UInputMenu
v-model:search-term="searchTerm"
v-model="selectedValue"
v-model:open="open"
value-key="id" <!-- If your items have an id property -->
:items="items"
:loading="loading"
ignore-filter
placeholder="Search..."
/>
<UInputMenu
v-model:search-term="searchTerm"
v-model="selectedValue"
v-model:open="open"
value-key="id" <!-- If your items have an id property -->
:items="items"
:loading="loading"
ignore-filter
placeholder="Search..."
/>
This approach separates the search term input from the selected value, giving you more control over both aspects of the component. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai7d ago
<@439284866610233345> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
Angh0r
Angh0rOP6d ago
@kapa.ai it's better but how do I pass url to found items and later redirect on select?
kapa.ai
kapa.ai6d ago
Let me check the knowledge base for you again. One moment! :books: ---------------------- To pass URLs to found items in the InputMenu component and handle redirection when an item is selected, you can use several approaches based on the Nuxt UI documentation. Adding URLs to Items When you fetch your items, include a to property for each item. The InputMenu component supports all properties from the Link component, including to for navigation:
<script setup lang="ts">
const searchTerm = ref('')
const open = ref(false)
const selectedValue = ref()
const loading = ref(false)
const items = ref([])

watch(searchTerm, async (newValue) => {
if (newValue.length >= 3) {
loading.value = true

// Example fetch that includes URLs in the items
const { data } = await useFetch('your-api-endpoint', {
params: { q: newValue },
transform: (data) => {
return data?.map(item => ({
label: item.name,
to: `/details/${item.id}`, // URL for navigation
// Other properties as needed
}))
}
})

items.value = data.value || []
loading.value = false
open.value = items.value.length > 0
} else {
open.value = false
items.value = []
}
})

// Handle item selection
function onSelect(item) {
if (item && item.to) {
// You can use programmatic navigation here if needed
navigateTo(item.to)
}
}
</script>

<template>
<UInputMenu
v-model:search-term="searchTerm"
v-model="selectedValue"
v-model:open="open"
:items="items"
:loading="loading"
ignore-filter
placeholder="Search..."
@update:model-value="onSelect"
/>
</template>
<script setup lang="ts">
const searchTerm = ref('')
const open = ref(false)
const selectedValue = ref()
const loading = ref(false)
const items = ref([])

watch(searchTerm, async (newValue) => {
if (newValue.length >= 3) {
loading.value = true

// Example fetch that includes URLs in the items
const { data } = await useFetch('your-api-endpoint', {
params: { q: newValue },
transform: (data) => {
return data?.map(item => ({
label: item.name,
to: `/details/${item.id}`, // URL for navigation
// Other properties as needed
}))
}
})

items.value = data.value || []
loading.value = false
open.value = items.value.length > 0
} else {
open.value = false
items.value = []
}
})

// Handle item selection
function onSelect(item) {
if (item && item.to) {
// You can use programmatic navigation here if needed
navigateTo(item.to)
}
}
</script>

<template>
<UInputMenu
v-model:search-term="searchTerm"
v-model="selectedValue"
v-model:open="open"
:items="items"
:loading="loading"
ignore-filter
placeholder="Search..."
@update:model-value="onSelect"
/>
</template>
Alternative Approaches 1. Using onSelect in item properties:
// When defining your items
const items = data?.map(item => ({
label: item.name,
to: `/details/${item.id}`,
onSelect: () => navigateTo(`/details/${item.id}`)
}))
// When defining your items
const items = data?.map(item => ({
label: item.name,
to: `/details/${item.id}`,
onSelect: () => navigateTo(`/details/${item.id}`)
}))
2. Using custom slots for more control:
<template>
<UInputMenu
v-model:search-term="searchTerm"
v-model="selectedValue"
v-model:open="open"
:items="items"
:loading="loading"
ignore-filter
placeholder="Search..."
>
<template #item="{ item }">
<ULink :to="item.to">
{{ item.label }}
</ULink>
</template>
</UInputMenu>
</template>
<template>
<UInputMenu
v-model:search-term="searchTerm"
v-model="selectedValue"
v-model:open="open"
:items="items"
:loading="loading"
ignore-filter
placeholder="Search..."
>
<template #item="{ item }">
<ULink :to="item.to">
{{ item.label }}
</ULink>
</template>
</UInputMenu>
</template>
3. For more complex scenarios, you can use the router-link approach shown in the Stack Overflow post:
<template>
<router-link v-slot="routerProps" :to="item.to" custom>
<a :href="routerProps.href" @click="routerProps.navigate">
{{ item.label }}
</a>
</router-link>
</template>
<template>
<router-link v-slot="routerProps" :to="item.to" custom>
<a :href="routerProps.href" @click="routerProps.navigate">
{{ item.label }}
</a>
</router-link>
</template>
The key is to include the to property in your items and then either let the component handle navigation automatically or use the @update:model-value event to handle navigation programmatically. This approach gives you flexibility to control both when the dropdown appears and how navigation works when items are selected. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai6d ago
<@439284866610233345> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
Angh0r
Angh0rOP6d ago
@kapa.ai adding "to:" with URL nor adding "onSelect:" with navigateTo() doesnt work. Documentation shows that item inside inputMenu does not support props of link compomnent.
kapa.ai
kapa.ai6d ago
Let me check the knowledge base for you again. One moment! :books: ---------------------- You're right, and I apologize for the confusion. Looking more carefully at the documentation, I see that InputMenu items don't directly support the Link component properties like to for navigation, unlike some other components like NavigationMenu or DropdownMenu. Let's implement a proper solution for handling navigation with InputMenu:
<script setup lang="ts">
const searchTerm = ref('')
const open = ref(false)
const selectedValue = ref()
const loading = ref(false)
const items = ref([])

watch(searchTerm, async (newValue) => {
if (newValue.length >= 3) {
loading.value = true

// Example fetch that includes URLs in a separate property
const { data } = await useFetch('your-api-endpoint', {
params: { q: newValue },
transform: (data) => {
return data?.map(item => ({
label: item.name,
// Store URL in a custom property
url: `/details/${item.id}`,
// Other properties as needed
}))
}
})

items.value = data.value || []
loading.value = false
open.value = items.value.length > 0
} else {
open.value = false
items.value = []
}
})

// Handle item selection for navigation
function handleSelection(item) {
if (item && item.url) {
navigateTo(item.url)
}
}
</script>

<template>
<UInputMenu
v-model:search-term="searchTerm"
v-model="selectedValue"
v-model:open="open"
:items="items"
:loading="loading"
ignore-filter
placeholder="Search..."
@update:model-value="handleSelection"
/>
</template>
<script setup lang="ts">
const searchTerm = ref('')
const open = ref(false)
const selectedValue = ref()
const loading = ref(false)
const items = ref([])

watch(searchTerm, async (newValue) => {
if (newValue.length >= 3) {
loading.value = true

// Example fetch that includes URLs in a separate property
const { data } = await useFetch('your-api-endpoint', {
params: { q: newValue },
transform: (data) => {
return data?.map(item => ({
label: item.name,
// Store URL in a custom property
url: `/details/${item.id}`,
// Other properties as needed
}))
}
})

items.value = data.value || []
loading.value = false
open.value = items.value.length > 0
} else {
open.value = false
items.value = []
}
})

// Handle item selection for navigation
function handleSelection(item) {
if (item && item.url) {
navigateTo(item.url)
}
}
</script>

<template>
<UInputMenu
v-model:search-term="searchTerm"
v-model="selectedValue"
v-model:open="open"
:items="items"
:loading="loading"
ignore-filter
placeholder="Search..."
@update:model-value="handleSelection"
/>
</template>
The key differences in this approach: 1. Store the URL in a custom property (like url) instead of using to 2. Use the @update:model-value event to handle navigation programmatically with the navigateTo() function 3. In the event handler, check for the selected item and navigate to its URL This approach separates the navigation logic from the item definition, giving you full control over when and how navigation occurs after selection. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai6d ago
<@439284866610233345> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
Angh0r
Angh0rOP6d ago
@kapa.ai This way redirect doesn't work as well. Value is selected and filled in input field fairing another search. It acts like select more than text input. It seemes that this componentsis not meant to be used as search.
kapa.ai
kapa.ai6d ago
Let me check the knowledge base for you again. One moment! :books: ---------------------- You're right about the behavior you're experiencing. Based on the documentation, the InputMenu component is designed to function more like a select component with search capabilities rather than a pure search input that redirects on selection. Looking at the documentation, InputMenu is described as "an autocomplete input with real-time suggestions" that's similar to SelectMenu but using an Input instead of a Select. It's primarily designed for selecting values from a list, not as a search-and-redirect component. For a search component that redirects on selection, you might want to consider these alternatives: 1. Create a custom solution using a basic UInput with a separate dropdown that you control manually:
<script setup>
const searchTerm = ref('')
const showResults = ref(false)
const results = ref([])
const loading = ref(false)

watch(searchTerm, async (newValue) => {
if (newValue.length >= 3) {
loading.value = true
// Fetch results
// ...
showResults.value = results.value.length > 0
loading.value = false
} else {
showResults.value = false
}
})

function selectResult(result) {
// Navigate without updating the input
navigateTo(result.url)
// Optionally close the results dropdown
showResults.value = false
}
</script>

<template>
<div class="relative">
<UInput v-model="searchTerm" placeholder="Search..." />

<div v-if="showResults" class="absolute w-full mt-1 bg-white border rounded shadow-lg z-10">
<div v-if="loading" class="p-2">Loading...</div>
<div v-else>
<div
v-for="result in results"
:key="result.id"
class="p-2 hover:bg-gray-100 cursor-pointer"
@click="selectResult(result)"
>
{{ result.label }}
</div>
</div>
</div>
</div>
</template>
<script setup>
const searchTerm = ref('')
const showResults = ref(false)
const results = ref([])
const loading = ref(false)

watch(searchTerm, async (newValue) => {
if (newValue.length >= 3) {
loading.value = true
// Fetch results
// ...
showResults.value = results.value.length > 0
loading.value = false
} else {
showResults.value = false
}
})

function selectResult(result) {
// Navigate without updating the input
navigateTo(result.url)
// Optionally close the results dropdown
showResults.value = false
}
</script>

<template>
<div class="relative">
<UInput v-model="searchTerm" placeholder="Search..." />

<div v-if="showResults" class="absolute w-full mt-1 bg-white border rounded shadow-lg z-10">
<div v-if="loading" class="p-2">Loading...</div>
<div v-else>
<div
v-for="result in results"
:key="result.id"
class="p-2 hover:bg-gray-100 cursor-pointer"
@click="selectResult(result)"
>
{{ result.label }}
</div>
</div>
</div>
</div>
</template>
2. Use CommandPalette instead, which is designed for search-like interactions: The CommandPalette component might be better suited for a search interface, as it's designed specifically for command/search patterns and supports navigation. You're correct in your assessment - the InputMenu component behaves more like a select element with search capabilities rather than a true search component that redirects on selection. For a search-and-redirect pattern, you'll likely need to build a custom solution or use a different component that better matches that interaction model. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai6d ago
<@439284866610233345> Kapa.ai is still learning and improving, please let me know how I did by reacting below.

Did you find this page helpful?