Dependency Requirement: cmdk version
main⚠️ Critical Dependency
Ensure you are using version 1.0.0 of the cmdk package to avoid compatibility issues with the component.
repository·main·Indexed 21 days ago
https://github.com/omeralpi/shadcn-phone-inputA specialized phone input component built following Shadcn design system principles. It provides a modern UI for entering phone numbers with country selection capabilities and integrates with react-hook-form and zod for validation. The component is a styled wrapper around react-phone-number-input and requires cmdk version 1.0.0 for compatibility.
Ensure you are using version 1.0.0 of the cmdk package to avoid compatibility issues with the component.
The PhoneInput component is designed to integrate seamlessly with react-hook-form and zod for form validation. To ensure valid phone numbers, it is recommended to use the isValidPhoneNumber utility from react-phone-number-input within your Zod schema's .refine() method.
.refine(isValidPhoneNumber, ...) to validate the phone string.useForm with the zodResolver.FormField component from Shadcn UI to wrap the PhoneInput.{...field}) onto the PhoneInput component to connect it to the form state.import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { isValidPhoneNumber } from "react-phone-number-input";
import { z } from "zod";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { PhoneInput } from "@/components/ui/phone-input";
import { toast } from "@/components/ui/use-toast";
const FormSchema = z.object({
phone:
z
.string()
.refine(isValidPhoneNumber, { message: "Invalid phone number" }),
});
export default function Hero() {
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {
phone: "",
},
});
function onSubmit(data: z.infer<typeof FormSchema>) {
toast({
title: "You submitted the following values:",
description: (
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
</pre>
),
});
}
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col items-start space-y-8"
>
<FormField
control={form.control}
name="phone"
render={({ field }) => (
<FormItem className="flex flex-col items-start">
<FormLabel className="text-left">Phone Number</FormLabel>
<FormControl className="w-full">
<PhoneInput placeholder="Enter a phone number" {...field} />
</FormControl>
<FormDescription className="text-left">
Enter a phone number
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
);
}If you want to allow the phone number field to be empty, extend your Zod schema using .or(z.literal("")). This allows the validation to pass if the string is empty, while still applying isValidPhoneNumber if a value is provided.
const FormSchema = z.object({
phone:
z
.string()
.refine(isValidPhoneNumber, { message: "Invalid phone number" })
.or(z.literal("")),
});The PhoneInput component is a styled wrapper around react-phone-number-input that integrates with shadcn/ui components (Button, Command, Input, Popover, ScrollArea). It provides a unified interface for selecting a country flag and entering a phone number.
InputComponent styled to connect seamlessly with the country selector.onChange callback is coerced to return an empty string "" instead of undefined when a valid number is not present, making it easier to work with controlled components.import { PhoneInput } from "./path-to-your-component";
import { useState } from "react";
export function MyForm() {
const [value, setValue] = useState<string | undefined>();
return (
<PhoneInput
value={value}
onChange={(val) => setValue(val)}
/>
);
}This project uses Prettier with two specific plugins to maintain code style: @ianvs/prettier-plugin-sort-imports for organized import statements and prettier-plugin-tailwindcss for class sorting.
Key configurations include:
tailwindFunctions: Specifies custom functions that contain Tailwind classes (e.g., cn, cva).importOrder: Defines the sequence of import groups.importOrderParserPlugins: Enables parsing for typescript, jsx, and decorators-legacy to ensure correct import sorting in complex files.const config = {
plugins: [
"@ianvs/prettier-plugin-sort-imports",
"prettier-plugin-tailwindcss",
],
tailwindFunctions: ["cn", "cva"],
importOrder: [
"<TYPES>",
"^(react/(.*)$)|^(react$)",
"^(next/(.*)$)|^(next$)",
"<THIRD_PARTY_MODULES>",
"",
"<TYPES>^@nmoon",
"^@/(.*)$",
"",
"<TYPES>^[.|..|~]",
"^~/",
"^[../]",
"^[./]",
],
importOrderParserPlugins: ["typescript", "jsx", "decorators-legacy"],
importOrderTypeScriptVersion: "4.4.0",
};
export default config;The project uses Contentlayer to manage code snippets. The Snippet document type is defined to process .mdx files located in the snippets/ directory. Each snippet must include a file name and an order number in its frontmatter. A slug is automatically computed from the filename.
// Snippet Document Type Schema
export const Snippet = defineDocumentType(() => ({
name: "Snippet",
filePathPattern: `snippets/**/*.mdx`,
contentType: "mdx",
fields: {
file: {
type: "string",
description: "The name of the snippet",
required: true,
},
order: {
type: "number",
description: "The order of the snippet",
required: true,
},
},
computedFields: {
slug: {
type: "string",
resolve: (_) => _._raw.sourceFileName.replace(/\.[^.$]+$/, ""),
},
},
}));When creating .mdx files for snippets, you must provide the following required fields in the frontmatter:
| Field | Type | Description |
|---|---|---|
file | string | The name of the snippet |
order | number | The display order of the snippet |
Additionally, a slug field is automatically generated based on the filename (with the extension removed).
```markdown
---
file: "my-snippet-name"
order: 1
---The PhoneInput component accepts props that combine standard HTML input attributes with react-phone-number-input properties.
Note on onChange: The component overrides the default react-phone-number-input behavior. While the underlying library might return undefined for invalid inputs, PhoneInput ensures onChange receives an empty string "" (cast as RPNInput.Value) to maintain consistency in controlled inputs.
type PhoneInputProps = Omit<
React.ComponentProps<"input">,
"onChange" | "value" | "ref"
> &
Omit<RPNInput.Props<typeof RPNInput.default>, "onChange"> & {
onChange?: (value: RPNInput.Value) => void;
};