Zone Select
Demo
Installation
Shadcn CLI
npx shadcn@latest add https://laviecn.vercel.app/r/zone-select.json
pnpm dlx shadcn@latest add https://laviecn.vercel.app/r/zone-select.json
yarn dlx shadcn@latest add https://laviecn.vercel.app/r/zone-select.json
bun x shadcn@latest add https://laviecn.vercel.app/r/zone-select.json
Manual
Copy and paste the following code into your project.
"use client"
import { useEffect, useId, useState } from "react"
import { CheckIcon, ChevronDownIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command"
import { Label } from "@/components/ui/label"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
export interface ZoneItem {
id: string
name: string
name_slug: string
full_name: string
province_id?: string
district_id?: string
}
interface ZoneSelectProps {
zone: ZoneItem[]
label?: string
placeholder?: string
value?: string
onSelect?: (value: string) => void
disabled?: boolean
className?: string
}
export function ZoneSelect({
zone,
label,
placeholder = "Select zone",
value: externalValue,
onSelect,
disabled,
className,
}: ZoneSelectProps) {
const id = useId()
const [open, setOpen] = useState<boolean>(false)
const [internalValue, setInternalValue] = useState<string>(
externalValue || ""
)
// Sync internal value with external value
useEffect(() => {
setInternalValue(externalValue || "")
}, [externalValue])
return (
<div className={cn("*:not-first:mt-2", className)}>
{label && <Label htmlFor={id}>{label}</Label>}
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
id={id}
variant="outline"
role="combobox"
aria-expanded={open}
className="bg-background hover:bg-background border-input w-full justify-between px-3 font-normal outline-offset-0 outline-none focus-visible:outline-[3px]"
disabled={disabled}
>
<span
className={cn(
"truncate",
!internalValue && "text-muted-foreground"
)}
>
{internalValue
? zone?.find((item: ZoneItem) => item.id === internalValue)
?.full_name
: placeholder}
</span>
<ChevronDownIcon
size={16}
className="text-muted-foreground/80 shrink-0"
aria-hidden="true"
/>
</Button>
</PopoverTrigger>
<PopoverContent
className="border-input w-full min-w-[var(--radix-popper-anchor-width)] p-0"
align="start"
>
<Command>
<CommandInput placeholder={placeholder} />
<CommandList>
<CommandEmpty>No zone found.</CommandEmpty>
<CommandGroup>
{zone?.map((item: ZoneItem) => (
<CommandItem
key={item.id}
value={item.name_slug}
onSelect={(currentValue) => {
const selectedItem = zone.find(
(item) => item.name_slug === currentValue
)
const newValue = selectedItem ? selectedItem.id : ""
setInternalValue(newValue)
onSelect?.(newValue)
setOpen(false)
}}
>
{item.full_name}
{internalValue === item.id && (
<CheckIcon size={16} className="ml-auto" />
)}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
)
}
"use client"
import { useMemo, useState } from "react"
export interface Province {
id: string
name: string
name_slug: string
full_name: string
}
export interface District {
id: string
name: string
name_slug: string
full_name: string
province_id: string
}
export interface Ward {
id: string
name: string
name_slug: string
full_name: string
district_id: string
}
interface UseFilterZoneProps {
provinces: Province[]
districts: District[]
wards: Ward[]
}
export function useFilterZone({
provinces,
districts,
wards,
}: UseFilterZoneProps) {
const [selectedProvince, setSelectedProvince] = useState<string>("")
const [selectedDistrict, setSelectedDistrict] = useState<string>("")
const [selectedWard, setSelectedWard] = useState<string>("")
const filteredDistricts = useMemo(() => {
return districts.filter(
(district) => district.province_id === selectedProvince
)
}, [districts, selectedProvince])
const filteredWards = useMemo(() => {
return wards.filter((ward) => ward.district_id === selectedDistrict)
}, [wards, selectedDistrict])
const handleSelectProvince = (provinceId: string) => {
setSelectedProvince(provinceId)
setSelectedDistrict("")
setSelectedWard("")
}
const handleSelectDistrict = (districtId: string) => {
setSelectedDistrict(districtId)
setSelectedWard("")
}
const handleSelectWard = (wardId: string) => {
setSelectedWard(wardId)
}
return {
filteredProvinces: provinces,
filteredDistricts,
filteredWards,
selectedProvince,
selectedDistrict,
selectedWard,
handleSelectProvince,
handleSelectDistrict,
handleSelectWard,
}
}
Update the import paths to match your project setup.
Zone Data
Props
ZoneSelectProps
Prop | Type | Default |
---|---|---|
zone | ZoneItem[] | - |
label? | string | - |
placeholder? | string | - |
value? | string | - |
onSelect? | (value: string) => void | - |
disabled? | boolean | - |
className? | string | - |
ZoneItem
Prop | Type | Default |
---|---|---|
id | string | - |
name | string | - |
name_slug | string | - |
full_name | string | - |
province_id? | string | - |
district_id? | string | - |
UseFilterZoneProps
Prop | Type | Default |
---|---|---|
districts | District[] | - |
provinces | Province[] | - |
wards | Ward[] | - |
Province
Prop | Type | Default |
---|---|---|
id | string | - |
name | string | - |
name_slug | string | - |
full_name | string | - |
District
Prop | Type | Default |
---|---|---|
id | string | - |
name | string | - |
name_slug | string | - |
full_name | string | - |
province_id | string | - |
Ward
Prop | Type | Default |
---|---|---|
id | string | - |
name | string | - |
name_slug | string | - |
full_name | string | - |
district_id | string | - |