An ISO 4217 compliant currency select component.
>_
The CurrencySelect
component uses theSelect
component from the shadcn/ui library.
The CountryDropdown
is built using the country-data-list package which provides ISO 4217 country codes.
My component imports a set of constants, customCurrencies
and allCurrencies
. The customCurrencies
array include only countries i want to show in the dropdown. The allCurrencies
array include all countries except the ones in customCurrencies
array.
Install the country-dropdown
package using your preferred package manager:
# Install dependencies
pnpm add country-data-list
# Install shadcn components
npx shadcn@latest add select
Copy the component below to components/ui/currency-select.tsx
.
import React from "react";
import { cn } from "@/lib/utils";
// data
import { currencies as AllCurrencies } from "country-data-list";
// shadcn
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
// radix-ui
import { SelectProps } from "@radix-ui/react-select";
// types
export interface Currency {
code: string;
decimals: number;
name: string;
number: string;
symbol?: string;
}
// constants
import { customCurrencies, allCurrencies } from "@/lib/constants/currencies";
interface CurrencySelectProps extends Omit<SelectProps, "onValueChange"> {
onValueChange?: (value: string) => void;
onCurrencySelect?: (currency: Currency) => void;
name: string;
placeholder?: string;
currencies?: "custom" | "all";
variant?: "default" | "small";
valid?: boolean;
}
const CurrencySelect = React.forwardRef<HTMLButtonElement, CurrencySelectProps>(
(
{
value,
onValueChange,
onCurrencySelect,
name,
placeholder = "Select currency",
currencies = "withdrawal",
variant = "default",
valid = true,
...props
},
ref
) => {
const [selectedCurrency, setSelectedCurrency] =
React.useState<Currency | null>(null);
const uniqueCurrencies = React.useMemo<Currency[]>(() => {
const currencyMap = new Map<string, Currency>();
AllCurrencies.all.forEach((currency: Currency) => {
if (currency.code && currency.name && currency.symbol) {
let shouldInclude = false;
switch (currencies) {
case "custom":
shouldInclude = customCurrencies.includes(currency.code);
break;
case "all":
shouldInclude = !allCurrencies.includes(currency.code);
break;
default:
shouldInclude = !allCurrencies.includes(currency.code);
}
if (shouldInclude) {
// Special handling for Euro
if (currency.code === "EUR") {
currencyMap.set(currency.code, {
code: currency.code,
name: "Euro",
symbol: currency.symbol,
decimals: currency.decimals,
number: currency.number,
});
} else {
currencyMap.set(currency.code, {
code: currency.code,
name: currency.name,
symbol: currency.symbol,
decimals: currency.decimals,
number: currency.number,
});
}
}
}
});
// Convert the map to an array and sort by currency name
return Array.from(currencyMap.values()).sort((a, b) =>
a.name.localeCompare(b.name)
);
}, [currencies]);
const handleValueChange = (newValue: string) => {
const fullCurrencyData = uniqueCurrencies.find(
(curr) => curr.code === newValue
);
if (fullCurrencyData) {
setSelectedCurrency(fullCurrencyData);
if (onValueChange) {
onValueChange(newValue);
}
if (onCurrencySelect) {
onCurrencySelect(fullCurrencyData);
}
}
};
void selectedCurrency;
return (
<Select
value={value}
onValueChange={handleValueChange}
{...props}
name={name}
data-valid={valid}
>
<SelectTrigger
className={cn("w-full", variant === "small" && "w-fit gap-2")}
data-valid={valid}
ref={ref}
>
{value && variant === "small" ? (
<SelectValue placeholder={placeholder}>
<span>{value}</span>
</SelectValue>
) : (
<SelectValue placeholder={placeholder} />
)}
</SelectTrigger>
<SelectContent>
<SelectGroup>
{uniqueCurrencies.map((currency) => (
<SelectItem key={currency?.code} value={currency?.code || ""}>
<div className="flex items-center w-full gap-2">
<span className="text-sm text-muted-foreground w-8 text-left">
{currency?.code}
</span>
<span className="hidden">{currency?.symbol}</span>
<span>{currency?.name}</span>
</div>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
);
}
);
CurrencySelect.displayName = "CurrencySelect";
export { CurrencySelect };
The small variant allows you to have a shorthand version displaying only the currency code in the SelectTrigger
. With the example below you can see how to use it in a form although it requires a little bit more work to display the FormLabel
and validation messages correctly, see code below.
>_