Skip to content

Instantly share code, notes, and snippets.

@travishorn
Created May 13, 2023 04:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save travishorn/91ac5c6de66abdd02caef69f2b1cc65d to your computer and use it in GitHub Desktop.
Save travishorn/91ac5c6de66abdd02caef69f2b1cc65d to your computer and use it in GitHub Desktop.
Next.js Headless UI Listbox as Selection Filter
import { useState } from 'react'
import { useRouter } from 'next/router';
import { Listbox } from '@headlessui/react'
import { ChevronUpDownIcon } from '@heroicons/react/20/solid'
import queryString from "query-string";
export interface FilterOption {
value: string;
label: string;
}
export interface FilterProps {
id: string;
label: string;
options: FilterOption[];
selected: FilterOption;
}
export function Filter({
id,
label,
options,
selected: initialSelected
}: FilterProps) {
const router = useRouter();
const [selected, setSelected] = useState(initialSelected);
const filterChange = (newSelected: FilterOption) => {
setSelected(newSelected);
const newQuery = router.query;
newQuery[id] = newSelected.value;
const newQueryString = queryString.stringify(newQuery);
router.push(`${router.pathname}?${newQueryString}`);
};
return (
<div>
<Listbox value={selected} onChange={filterChange}>
<Listbox.Label
className="uppercase text-sm font-semibold text-gray-500"
>{label}</Listbox.Label>
<div className="relative">
<Listbox.Button className="relative mt-1 w-full cursor-default rounded-sm bg-white py-2 pl-3 pr-10 text-left shadow border border-gray-200">
<span className="block truncate">{selected.label}</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon
className="h-5 w-5 text-gray-400"
aria-hidden="true"
/>
</span>
</Listbox.Button>
<Listbox.Options className="z-10 absolute max-h-60 w-full overflow-auto bg-white py-1 shadow">
{options.map((option) => (
<Listbox.Option
key={option.value}
className={({ active }) =>
`relative cursor-default select-none py-2 px-4 ${active ? 'bg-blue-100 text-blue-900' : 'text-gray-800'
}`
}
value={option}
>
<span className="block truncate">{option.label}</span>
</Listbox.Option>
))}
</Listbox.Options>
</div>
</Listbox>
</div>
)
}
import { Filter, FilterOption } from "../components/Filter";
import { GetServerSidePropsContext } from "next";
export async function getServerSideProps({ query }: GetServerSidePropsContext) {
const people = [
{ value: "0", label: "Wade Cooper" },
{ value: "1", label: "Arlene Mccoy" },
{ value: "2", label: "Devon Webb" },
{ value: "3", label: "Tom Cook" },
{ value: "4", label: "Tanya Fox" },
{ value: "5", label: "Hellen Schmidt" },
];
const selectedPerson = people
.find((person) => person.value === query.personId)
|| people[0];
const locations = [
{ value: "0", label: "New York" },
{ value: "1", label: "Los Angeles" },
{ value: "2", label: "Chicago" },
{ value: "3", label: "Houston" },
{ value: "4", label: "Phoenix" },
{ value: "5", label: "Philadelphia" },
];
const selectedLocation = locations
.find((location) => location.value === query.locationId)
|| locations[0];
return {
props: { people, selectedPerson, locations, selectedLocation },
};
};
interface PageProps {
people: FilterOption[],
selectedPerson: FilterOption,
locations: FilterOption[],
selectedLocation: FilterOption,
}
export default function Page({
people,
selectedPerson,
locations,
selectedLocation
}: PageProps) {
return (
<main className="p-10 flex flex-col gap-10">
<Filter
id="personId"
label="Person"
options={people}
selected={selectedPerson}
/>
<Filter
id="locationId"
label="Location"
options={locations}
selected={selectedLocation}
/>
</main>
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment