1.0.10 • Published 10 months ago

@alfarizi/react-input v1.0.10

Weekly downloads
-
License
MIT
Repository
github
Last release
10 months ago

@alfarizi/react-input

A highly customizable, type-safe, and strict React input component. It simplifies form handling with enhanced DX (Developer Experience).

Note: If you use this component in Nextjs App Dir, you only can use this component in client component.

Demo

  • React-input demo: Demo
  • You can also wrap this component to create your custom style component. You can see the example result in here: Input Customization Demo

Features

  • Type Safety: The value and onValueChange props are strictly typed based on the type prop.
  • Strict Handling: Automatically converts empty strings to undefined.
  • Trimming: Optional trim prop to trim string values.
  • File Input: Handles single and multiple file uploads.
  • Number Input: Returns number | undefined for numeric input types.
  • Tel Input: Validates common phone number formats.
  • Customizable: Easily styled and extended.

Motivation

First Reason

The default onChange prop often falls short in terms of developer experience. To address this, I introduced a custom onValueChange prop, which provides a strictly typed value parameter based on the type prop. For example:

  • For type="number", it uses my package: @alfarizi/convert-to-number to convert strings to numbers.
  • For type="file", it ensures type-safe handling of file inputs.
  • For type="tel", it validates input based on common phone number formats.

This improves DX and ensures robust handling of various input types.

Second Reason

I frequently use Shadcn components for creating forms, often in combination with react-hook-form and zod. When defining a required string in a Zod schema, I often write:

z.string().trim().min(1);

By default, empty input fields are treated as empty strings. Because this component will return "undefined" if the string is empty, and by default this component will trim the value, you can simplify the schema to:

z.string(); // to require a string

Third Reason

Handling numbers in forms can be tricky. A common client-side validation error I encounter is: "expected number but received string". In Zod, this can be addressed using:

z.coerce.number().optional();

However, another issue arises. If the input is an empty string (""), Zod's coercion converts it to 0. I don't want to submit 0; I want to submit undefined or null.

With this component, the onValueChange prop receives number | undefined for numeric input types. If the input is empty, it is treated as undefined. When passing this to z.coerce.number().optional(), the result is null. If you prefer undefined over null, you can use my other package: @alfarizi/convert-undefined-null to convert between null and undefined.

Installation

npm install @alfarizi/react-input

or

yarn add @alfarizi/react-input

or

pnpm add @alfarizi/react-input

or

bun add @alfarizi/react-input

Usage

Basic Example

import React, { useState } from "react";
import { Input } from "@alfarizi/react-input";

const App = () => {
  const [value, setValue] = useState<string | undefined>(undefined);

  return (
    <Input
      type="text"
      value={value}
      onValueChange={setValue}
      placeholder="Enter your text"
    />
  );
};

export default App;

Number Input Example

import React, { useState } from "react";
import { Input } from "@alfarizi/react-input";

const NumberInput = () => {
  const [value, setValue] = useState<number | undefined>(undefined);

  return (
    <Input
      type="number"
      value={value}
      onValueChange={setValue}
      placeholder="Enter a number"
    />
  );
};

export default NumberInput;

You want to Custom it? Yes you can

Example of custom input component based on @alfarizi/react-input. Result of this example custom input component can be seen here: https://reactjs-components-five.vercel.app/?path=/docs/input--docs

If you often use shadcn-ui, you will love this custom component:

import * as React from "react";

import { cn } from "@/lib/utils";
import { type VariantProps, cva } from "class-variance-authority";
import type * as InputPrimitive from "@alfarizi/react-input";

export const inputVariants = cva(
  "flex items-center h-10 w-full text-sm bg-transparent file:border-0 file:text-sm file:font-medium placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50 border border-transparent focus-within:outline-none aria-invalid:ring-1 aria-invalid:ring-destructive aria-invalid:focus-within:ring-2 aria-invalid:focus-within:ring-destructive",
  {
    variants: {
      rounded: {
        none: "rounded-none",
        md: "rounded-md",
      },
      variant: {
        outline:
          "border-border focus-within:border-primary focus-within:shadow-[0_0px_0px_1px_hsl(var(--primary))] aria-invalid:border-transparent",
        filled:
          "border-2 bg-background focus-within:border-primary focus-within:bg-transparent",
        underlined:
          "rounded-none border-b-border focus-within:border-b-primary focus-within:shadow-[0_1px_0px_0px_hsl(var(--primary))]",
        unstyled: "",
      },
    },
    defaultVariants: {
      rounded: "md",
      variant: "outline",
    },
  },
);

export interface InputProps<
  T extends InputPrimitive.InputType = "text",
  M extends boolean | undefined = undefined,
> extends InputPrimitive.InputProps<T, M>,
    VariantProps<typeof inputVariants> {
  containerClassName?: string;
  startAdornment?: React.ReactNode;
  endAdornment?: React.ReactNode;
  startAdornmentClassName?: string;
  endAdornmentClassName?: string;
}

const Input = <
  T extends InputPrimitive.InputType = "text",
  M extends boolean | undefined = undefined,
>(
  {
    className,
    containerClassName,
    rounded,
    variant,
    type = "text" as T,
    value,
    startAdornment,
    endAdornment,
    startAdornmentClassName,
    endAdornmentClassName,
    ...props
  }: InputProps<T, M>,
  ref: React.Ref<HTMLInputElement>,
) => {
  return (
    <div
      className={cn(
        inputVariants({ variant, rounded, className: containerClassName }),
        "relative flex items-center",
      )}
    >
      {startAdornment && (
        <div
          className={cn(
            "inline-flex h-full items-center text-muted-foreground",
            "py-2 pl-3 pr-1.5",
            "rounded-l-md has-[+input:focus]:rounded-l-sm has-[+input:focus]:border-l-0", // this must be same with default value of variant
            {
              "rounded-l-md": rounded === "md",
              "rounded-l-none": rounded === "none",
            },
            startAdornmentClassName,
          )}
        >
          {startAdornment}
        </div>
      )}
      <input
        ref={ref}
        type={type}
        value={
          value !== undefined && value !== null
            ? type === "file"
              ? undefined
              : type === "number"
                ? String(value ?? "")
                : (value as string)
            : undefined
        }
        className={cn(
          "w-full overflow-clip bg-transparent px-3 py-2 outline-none focus-visible:outline-none",
          {
            "pl-1.5": !!startAdornment,
            "pr-1.5": !!endAdornment,
          },
          className,
        )}
        {...props}
      />
      {endAdornment && (
        <div
          className={cn(
            "inline-flex items-center text-muted-foreground",
            "py-2 pl-1.5 pr-3",
            "rounded-r-md has-[+input:focus]:rounded-r-sm has-[+input:focus]:border-r-0", // this must be same with default value of variant
            {
              "rounded-r-md": rounded === "md",
              "rounded-r-none": rounded === "none",
            },
            endAdornmentClassName,
          )}
        >
          {endAdornment}
        </div>
      )}
    </div>
  );
};

Input.displayName = "Input";

export { Input };
export default Input;

Props

InputProps

NameTypeDefaultDescription
typeInputType"text"The type of the input.
multiplebooleanfalseEnables multiple file selection for type="file".
valueInputValue<T, M>undefinedThe value of the input.
onValueChange(value: InputValue<T, M>) => voidundefinedCallback for when the value changes.
trimbooleantrueTrims string inputs on blur or enter key press.

License

This project is licensed under the MIT License.

1.0.10

10 months ago

1.0.9

10 months ago

1.0.8

10 months ago

1.0.7

10 months ago

1.0.6

10 months ago

1.0.5

10 months ago

1.0.4

10 months ago

1.0.3

10 months ago

1.0.2

10 months ago

1.0.1

10 months ago

1.0.0

10 months ago