shadcn-phone-input

repository·main·Indexed 21 days ago

https://github.com/omeralpi/shadcn-phone-input

A 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.

Tokens
2.8K
Snippets
7
Records
8
Agent score
75%

What's inside shadcn-phone-input

  1. Use the PhoneInput component with React Hook Form and Zod

    main

    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.

    Implementation Steps:

    1. Define a Zod schema using .refine(isValidPhoneNumber, ...) to validate the phone string.
    2. Initialize useForm with the zodResolver.
    3. Use the FormField component from Shadcn UI to wrap the PhoneInput.
    4. Spread the field properties ({...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>
      );
    }
  2. Make the phone input optional in Zod

    main

    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("")),
    });
  3. Use the PhoneInput component

    main

    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.

    Key Features

    • Country Selection: Uses a Popover with a searchable Command menu to select countries.
    • Visual Feedback: Displays the country flag and calling code.
    • Input Integration: Uses a custom InputComponent styled to connect seamlessly with the country selector.
    • Value Handling: The 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)}
        />
      );
    }
  4. Configure Prettier with Tailwind CSS and Import Sorting

    main

    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;
  5. Configure Contentlayer Snippet document type

    main

    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(/\.[^.$]+$/, ""),
        },
      },
    }));
  6. Define Snippet frontmatter fields

    main

    When creating .mdx files for snippets, you must provide the following required fields in the frontmatter:

    FieldTypeDescription
    filestringThe name of the snippet
    ordernumberThe 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
    ---
  7. PhoneInputProps API Reference

    main

    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;
      };