i am using this headless ui combobox https://headlessui.dev/react/combobox
for my own auto complete search box suggestion. For this case i am wondering, how can i make the combobox to send the values when i press the enter key? currently i have ways to send the value with onclick on the suggested options but i would like to have something for enter key as well
<>
<Combobox
as="div"
className="relative mb-10 mx-auto max-w-x1 rounded-x1 bg-white shadow-2x1 ring-1 ring-black/5 divide-y divide-gray-100 overflow-hidden"
value={searchCardName}
onChange={setSearchCardName}
>
<div className="flex items-center px-4">
<SearchIcon className="h-6 w-6 text-gray-500" />
<Combobox.Input
className="h-12 w-full border-0 focus:ring-0"
placeholder="Search..."
onChange={(event) => onChangeHandler(event.target.value)}
/>
</div>
<Combobox.Options className="max-h-96 py-4 text-sm overflow-y-auto">
{suggestions.map((cardName) => (
<Combobox.Option key={cardName} value={cardName}>
{({ active }) => (
<div
className={`space-x-1 px-4 py-2 ${
active ? "bg-indigo-600" : "bg-white"
}`}
onClick={() => onSuggestHandler(cardName)}
>
<span
className={`font-medium text-gray-900 ${
active ? "text-white" : "text-gray-900"
}`}
>
{cardName}
</span>
</div>
)}
</Combobox.Option>
))}
</Combobox.Options>
</Combobox>
{isResult && <Tabs />}
</>;
edit for updated, tried onKeyDown with this
<>
<Combobox as="div" className="relative mb-10 mx-auto max-w-x1 rounded-x1 bg-white shadow-2x1 ring-1 ring-black/5 divide-y divide-gray-100 overflow-hidden" value={searchCardName} onChange={setSearchCardName}>
<div className="flex items-center px-4">
<SearchIcon className="h-6 w-6 text-gray-500" />
<Combobox.Input className="h-12 w-full border-0 focus:ring-0" placeholder="Search..." onChange={(event) => onChangeHandler(event.target.value)} onKeyPress={(e) => e.key === 'Enter' && console.log("testing")}/>
</div>
<Combobox.Options className="max-h-96 py-4 text-sm overflow-y-auto">
{suggestions.map((cardName) => (
<Combobox.Option key={cardName} value={cardName}>
{({ active }) => (
<div className={`space-x-1 px-4 py-2 ${active ? 'bg-indigo-600' : 'bg-white'}`} onClick={() => onSuggestHandler(cardName)}>
<span className={`font-medium text-gray-900 ${active ? 'text-white' : 'text-gray-900'}`}>{cardName}</span>
</div>
)}
</Combobox.Option>
))}
</Combobox.Options>
</Combobox>
{isResult &&
<Tabs />}
</>
the onKeyDown is under my Combobox.Input
<Combobox.Input className="h-12 w-full border-0 focus:ring-0" placeholder="Search..." onChange={(event) => onChangeHandler(event.target.value)} onKeyPress={(e) => e.key === 'Enter' && console.log("testing")}/>
solved it i guess is using onKeyUp instead of onKeyDown.
<Combobox.Input className="h-12 w-full border-0 focus:ring-0" placeholder="Search..." onChange={(event) => onChangeHandler(event.target.value)} onKeyUp={(e) => onKeyboardHandler(e)} />
Related
type Props = {
topic?: string;
}
function Header({topic}: Props) {
const [istopic, setTopic] = useState("");
const{loading, data, error} = !topic? useQuery(GET_POSTS): useQuery(GET_POSTS_BY_TOPIC,{
variables:{
topic:topic,
}
});
const posts: Post[\] = !topic ? data?.getPostList : data?.getPostListByTopic
//HERE IS THE METHOD I USED TO CREATE AN ARRAY FROM THE QUERY LIST
//HERE IS THE FORM INSIDE OF MY NAVBAR THAT I WANT TO USE TO SEARCH THE RESULTS
<form
onSubmit={handleSubmit}
className="flex flex-1 items-center basis-1/3 space-x-2 rounded-md border-none border-gray-500 px-3 py-1"
>
<input
value={istopic}
onChange={(e) => setTopic(e.target.value)}
type="text"
placeholder="Search"
onSubmit={handleSubmit}
className="flex-1 rounded-md border-gray-400 bg-transparent outline-none "
/>
<SearchIcon className="h-7 w-7 text-gray-400" />
<button disabled={!istopic} type="submit" hidden />
</form>;
{
posts ? (
posts
?.filter((post: any) => post.title.match(new RegExp(istopic, "i")))
?.map((post: any) => {
<div>
<a
href={`/channel/${post.channels[0]?.topic}`}
className=" absolute top-6 z-99 bg-blue-600 block py-2 pl-3 pr-4 text-gray-700 rounded"
aria-current="page"
>
{post.title}
</a>
</div>;
console.log(post.title);
})
) : (
<li>
<a
href="#"
className=" absolute top-6 z-99 bg-blue-600 block py-2 pl-3 pr-4 text-gray-700 rounded hover:bg-gray-100"
aria-current="page"
>
Nothing Found
</a>
</li>
);
}
I want to use the data from the array the to check if there is any that matches from the input field and display those matches
I want the first option to be selected by default in dropdown. I tried combobox defaultValue property but didn't work. How can i do this?
Combobox Component
import { useState } from 'react'
import { CheckIcon, ChevronUpDownIcon } from '#heroicons/react/20/solid'
import { Combobox } from '#headlessui/react'
function classNames(...classes) {
return classes.filter(Boolean).join(' ')
}
export default function FormCombobox({
comboboxData,
label,
questionRange,
setQuestionRange,
}) {
const [query, setQuery] = useState('')
let items = comboboxData.map((item) => ({
id: item.content_object.nanoid,
name: item.content_object.name,
multiplier: item.multiplier,
}))
const filteredItems =
query === ''
? items
: items.filter((item) => {
return item.name.toLowerCase().includes(query.toLowerCase())
})
return (
<Combobox
as="div"
value={questionRange}
onChange={setQuestionRange}
className="my-5"
>
<Combobox.Label className="block text-left font-bold text-gray-700">
{label}
</Combobox.Label>
<div className="relative mt-1">
<Combobox.Input
className="w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-10 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm"
onChange={(event) => setQuery(event.target.value)}
displayValue={(item) => item?.name}
/>
<Combobox.Button className="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none">
<ChevronUpDownIcon
className="h-5 w-5 text-gray-400"
aria-hidden="true"
/>
</Combobox.Button>
{filteredItems.length > 0 && (
<Combobox.Options className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
{filteredItems.map((item) => (
<Combobox.Option
key={item.id}
value={item}
className={({ active }) =>
classNames(
'relative cursor-default select-none py-2 pl-3 pr-9',
active ? 'bg-indigo-600 text-white' : 'text-gray-900'
)
}
>
{({ active, selected }) => (
<>
<span
className={classNames(
'block truncate',
selected && 'font-semibold'
)}
>
{item.name}
</span>
{selected && (
<span
className={classNames(
'absolute inset-y-0 right-0 flex items-center pr-4',
active ? 'text-white' : 'text-indigo-600'
)}
>
<CheckIcon className="h-5 w-5" aria-hidden="true" />
</span>
)}
</>
)}
</Combobox.Option>
))}
</Combobox.Options>
)}
</div>
</Combobox>
)
}
initialize the state of questionRange when declaring it .
const [questionRange, setQuestionRange] = useState(items[0])
The search filter works practically as I wish, you enter a city and when you press the button it returns the results of the clinics that match the list of cities. The only problem is that I can only do a single search, then I have to reload the page, I need to delete the city from the input so that the complete list appears again and I can do another search, using typescript is complicating this part. To make it clearer, what I am trying to do is that whenever I enter a new city in the search bar and press the button, I get the result of the clinics in that city, now it just gives me the result only once, the next search gives me the result of clinic not found.
import React, { useState, useEffect } from 'react'
import { getClinic } from '../../api/drupalAPI'
import {Clinic} from '#icofcv/common';
import contentUtils from '../../lib/contentUtils'
import Loader from '../spinner/Loader';
interface Props {
showModalLocator: boolean,
closeModalLocator: () => void
}
export const ClinicLocator: React.FC<Props> = ({ children, showModalLocator, closeModalLocator }) => {
const [clinicList, setClinicList] = useState<Clinic[] | undefined >([]);
const [text, setText] = useState("")
const textInput = () => {
text === "" ? clinicList : null
}
const fetchClinicList = async () => {
getClinic().then((response)=>{
console.log(response)
setClinicList(response)
}).catch ( (error) => {
console.error(error);
throw error;
});
}
const handleChange = () => {
const filterClinicList = clinicList && clinicList?.length > 0
? clinicList?.filter((clinic) => clinic?.province?.toLowerCase() === text.toLowerCase())
: undefined;
setClinicList(filterClinicList)
}
useEffect (() => {
fetchClinicList();
}, []);
return (
<>
<div>
{showModalLocator ? (
<>
<div className="justify-center items-center flex overflow-x-hidden overflow-y-auto fixed inset-0 z-50 outline-none focus:outline-none">
<div className="relative p-2 w-full max-w-3xl h-full md:h-auto">
{/*content*/}
<div className="relative bg-white rounded-lg shadow">
{/*header*/}
<div className="flex justify-between items-start px-4 py-3 rounded-t border-b">
<h3 className="text-lg font-medium">Localizador de clinicas</h3>
<button className="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center" onClick={closeModalLocator}>
<svg aria-hidden="true" className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
</svg>
</button>
</div>
{/*body*/}
<div className="relative px-3 py-3 flex-auto overflow-auto modal-body">
<h2 className="text-sm font-medium mb-2">¿Dónde te encuentras?</h2>
<input
value={text}
onChange= {(e) => {setText(e.target.value)
textInput}}
type="search"
className="w-100 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2"
placeholder="Introduce una ubicación"
/>
<div>
<h2 className="text-sm font-medium my-3">Resultados</h2>
<div className="w-100">
<iframe className="w-100" src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d2664.3238269926374!2d-0.3805919350162851!3d39.46959682083709!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0xd604f4bee0957f3%3A0x6686ff7d230b3965!2zQy4gZGUgU2FudC
BWaWNlbnQgTcOgcnRpciwgNjEsIHBpc28gMsK6LCBwdGEgMsKqLCA0NjAwMiBWYWzDqG5jaWEsIEVzcGHDsWE!5e0!3m2!1ses!2sus!4v1662388390673!5m2!1ses!2sus" loading="lazy"></iframe>
</div>
<div className="md:mt-4 overflow-auto relative py-2">
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
{clinicList && clinicList?.length === 0 && (
<div>Clinica no encontrada</div>
)}
{!clinicList ? <Loader /> :
clinicList.map((clinicFilter) => (
<div className="card bg-white px-2 py-3 h-36 md:h-32">
<button key={clinicFilter.id} type="button" className="text-left">
<div className="flex items-center gap-2 md:gap-4 md:gap-4">
<img className="h-24 w-2/5 min-w-40 object-cover object-center rounded-lg" src={contentUtils.getLargeImageUrl(clinicFilter.logo)} alt="#"/>
<div className="w-3/5">
<div className="text-md font-medium leading-5 clinic-title uppercase">{clinicFilter.title}</div>
<div className="flex items-center gap-2">
<div className="text-neutral-500 text-sm">{clinicFilter.propsPhone}</div>
<div className="text-neutral-500 text-sm">{clinicFilter.mobile}</div>
</div>
<div className="text-teal-600 text-sm underline clinic-mail">{clinicFilter.email}</div>
<div className="text-neutral-500 text-sm">{clinicFilter.registry}</div>
</div>
</div>
</button>
</div>
))
}
</div>
</div>
</div>
</div>
{/*footer*/}
<div className="flex items-center justify-end px-4 py-2 border-t border-solid border-slate-200 rounded-b gap-2">
<button className="btn text-black text-sm background-transparent px-8 outline-none focus:outline-none focus:ring-teal-600 focus:border-teal-600" type="button" onClick={closeModalLocator}>Cancelar</button>
<button className="btn bg-teal-600 hover:bg-teal-700 text-white text-sm active:bg-teal-700 px-8 outline-none focus:outline-none" type="button" onClick={handleChange} >Buscar</button>
</div>
</div>
</div>
</div>
<div className="opacity-25 fixed inset-0 z-40 bg-black"></div>
</>
) : null}
</div>
</>
)
}
In your handleChange just return the List itself when text is null
const filterClinicList = clinicList && clinicList?.length > 0
? clinicList?.filter((clinic) => (!text || clinic?.province?.toLowerCase() === text.toLowerCase()))
: undefined; //!text evaluates to true when text is '' i.e. short circuit the filter
```
ideally what you do would be:
```
let filterClinicList = clinicList;
//only filter when you *need* to filter
if (text) {
filterClinicList = clinicList?.filter((clinic) => (!text || clinic?.province?.toLowerCase() === text.toLowerCase()));
}
```
I have social media posts stored in firebase and each post has a like button. I am using map() function to render those posts but I have declared a single state for like button using useState().
When I click on like button in any of the posts, the state is getting changed for all the posts and same is happening when I dislike.
How do I change the state for that particular post on which like is cliked??
function Feed(){
const [like, setLike] = useState(false);
const [heart, setHeart] = useState(false);
{posts.map((item) => (
<div
key={item.id}
className=" flex flex-col gap-2 bg-white rounded-xl p-3 border border-gray-300 shadow-
lg mb-2"
>
<div className=" flex gap-2 items-center">
<img
src={userImage}
className=" w-[48px] h-[48px] rounded-full "
alt=""
/>
<span className=" text-black font-semibold text-sm">
{userName}
</span>
</div>
<hr />
{item.body && <p>{item.body}</p>}
{item.imageUrl && <img src={item.imageUrl} alt="" />}
{item.videoUrl && (
<iframe
src={item.videoUrl}
title={item.id}
frameBorder="0"
className=" w-full h-[20rem] "
></iframe>
)}
<hr />
<div className=" flex gap-2">
{like ? (
<ThumbUpIcon
className=" cursor-pointer text-blue-600 hover:bg-gray-100 px-2 py-2
hover:rounded-md"
fontSize="large"
onClick={() => setLike(!like)}
/>
) : (
<ThumbUpOutlinedIcon
className=" cursor-pointer text-gary-500 hover:bg-gray-100 px-2 py-2
hover:rounded-md"
fontSize="large"
onClick={() => setLike(!like)}
/>
)}
{heart ? (
<FavoriteOutlinedIcon
className=" cursor-pointer text-red-500 hover:bg-gray-100 px-2 py-2
hover:rounded-md"
fontSize="large"
onClick={() => setHeart(!heart)}
/>
) : (
<FavoriteBorderOutlinedIcon
className=" cursor-pointer text-gary-500 hover:bg-gray-100 px-2 py-2
hover:rounded-md"
fontSize="large"
onClick={() => setHeart(!heart)}
/>
)}
</div>
</div>
))}
}
I suggest instead of a boolean variable for keeping like for posts, define an array and add the id of each element that liked
const [likes, setLikes] = useState([]);
And
likes.findIndex(x=>x===item.id)<0 ?
onClick={() => {likes.add(item.id);setLikse(...likes)}}
Sandbox: Simple Sample is here
I have two siblings and one is watching the state of another.
<Controller
control={control}
name="selectedBirthYear"
defaultValue={years[0]}
render={({ field }) => (
<SelectBirthYear
field={field}
years={years}
value={selectedYear}
defaultValue={selectedYear}
onChange={useEffect(() => {setSelectedYear(field.value)})}
/>
)}
/>
and
<Controller
control={control}
name="selectedBirthMonth"
defaultValue={months[0]}
render={({ field }) => (
<SelectBirthMonth
field={field}
startYear={startYear}
selectedYear={selectedYear}
months={months}
value={selectedMonth}
defaultValue={selectedMonth}
reducedMonths={reducedMonths}
onChange={useEffect(() => setSelectedMonth(field.value))}
/>
)}
/>
These two components are part of a Form parent. But, the issue is, that my useEffect, which watches year change, is not firing in combination with the Controller:
const watchYearChange = () => {
if(Number(selectedYear.name) == startYear){
setSelectedMonth(reducedMonths[reducedMonths.length - 1]);
}
};
useEffect(() => watchYearChange(), [selectedYear])
So, I was thinking to put this logic inside of the component itself and 'reset' the value of month on year change (Number(selectedYear.name) == startYear)
I've been experimenting, but for now with no success.
export const SelectBirthMonth = ({
startYear,
selectedYear,
months,
field,
selectedMonth,
setSelectedMonth,
reducedMonths}) => {
const { onChange, value } = field;
const watchYearChange = () => {
if(Number(selectedYear.name) == startYear){
setSelectedMonth(reducedMonths[reducedMonths.length - 1]);
}
console.log(reducedMonths[reducedMonths.length - 1]);
};
useEffect(() => watchYearChange(), [selectedYear])
return (
<Listbox value={Number(selectedYear.name) == startYear ? value == reducedMonths[reducedMonths.length - 1] : value} onChange={onChange}>
{({ open }) => (
<>
<Listbox.Label className="block text-sm font-medium text-gray-700">Geburtsmonat</Listbox.Label>
<div className="mt-1 relative">
<Listbox.Button className="bg-white relative w-full border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-yellow-500 focus:border-yellow-500 sm:text-sm">
<span className="block truncate">{value?.name}</span>
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<SelectorIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
</span>
</Listbox.Button>
<Transition
show={open}
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options
static
className="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
>
{ Number(selectedYear.name) == startYear ? reducedMonths.map((month) => (
<ListboxOption
key={month.id}
value={month}
date={month.name}
>
</ListboxOption>
)) : months.map((month) => (
<ListboxOption
key={month.id}
value={month}
date={month.name}
>
</ListboxOption>
))
}
</Listbox.Options>
</Transition>
</div>
</>
)}
</Listbox>
)
}
Any ideas, how to 'reset' value, which is already coming from field of react hook form? I am really stuck.