DEV Community

Serif COLAKEL
Serif COLAKEL

Posted on

Creating a Highlighted Input Component with React and TypeScript

Creating a Highlighted Input Component with React and TypeScript

In this article, we’ll walk through the creation of a HighlightedInput component using React, TypeScript, and various other tools. This component is designed to highlight dynamic text within an input field, such as environment variables, URLs, or placeholders wrapped in curly braces {{}}. We'll also utilize the @faker-js/faker library to generate mock data for testing.

Links

Youtube Video
Live Demo
Github Repo

Packages Used

Before we dive into the code, let's discuss some of the key packages involved:

  • React: A JavaScript library for building user interfaces React.
  • TypeScript: Superset of JavaScript providing type safety and enhanced tooling for React applications TypeScript.
  • @faker-js/faker: A powerful library for generating fake data, useful in development and testing. We will use it to create mock URL options Faker JS.
  • Lucide-react: A collection of icons as React components Lucide React.
  • Tailwind CSS: A utility-first CSS framework to style the component efficiently Tailwind CSS.
  • shadcn-ui: A collection of UI components for React applications Shadcn UI.

Component Overview

The HighlightedInput component highlights text based on user input. For example, if the user types {{TEST_URL}}, the component matches and highlights the TEST_URL environment variable. You can define these variables in the options prop.

Here’s how we structure the component:

import { PropsWithChildren, useId } from "react";
import { InfoIcon } from "lucide-react";

import {
  HoverCard,
  HoverCardContent,
  HoverCardTrigger,
} from "@/components/ui/hover-card";
import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils";
import CopyText from "./copy-text";
Enter fullscreen mode Exit fullscreen mode

We import essential utilities like useId (for generating unique IDs), UI components (HoverCard, Label), and a helper function cn (to manage conditional class names).

Additionally, we define a regex pattern, REGEX = /({{.*?}})/g;, to match text between {{ and }}, allowing us to detect the variable-like placeholders in the input field.

Key Features

1. Handling the Input Field

The input field is where the user types, and its value is managed via props like value, onChange, and onBlur.

<input
  className="w-full px-3 py-2 border rounded-lg focus:outline-none focus:border-blue-500"
  id={id}
  onBlur={onBlur}
  onChange={onChange}
  placeholder="Enter URL or paste text"
  value={value}
/>
Enter fullscreen mode Exit fullscreen mode

The value is processed to identify and highlight the dynamic portions of the text (i.e., anything inside {{}}).

 2. Highlighting Dynamic Text

The component highlights parts of the input matching the regex by splitting the value string:

value.split(REGEX).map((word, i) => {
  if (word.match(REGEX) !== null) {
    const foundOption = getOption(word, options);
    return (
      <HoverCard key={i}>
        <HoverCardTrigger asChild>
          <span className={foundOption ? "text-orange-500" : "text-red-500"}>
            {word}
          </span>
        </HoverCardTrigger>
        <HoverCardContent className={cn(foundOption ? "bg-white" : "bg-red-50")}>
          {foundOption ? (
            // Render option details
          ) : (
            <div className="flex flex-col gap-y-2">
              <InfoIcon className="w-6 h-6 text-red-600" />
              <span className="text-base font-bold">Unresolved Variable</span>
              <p className="text-sm">The variable {word} is not defined.</p>
            </div>
          )}
        </HoverCardContent>
      </HoverCard>
    );
  }
  return <span key={i}>{word}</span>;
});
Enter fullscreen mode Exit fullscreen mode

Each matched variable is displayed inside a HoverCard, providing additional details when hovered. If the variable is not found in the options, an error message is displayed.

 3. Managing Options

The component accepts a list of options through the options prop. Each option consists of a name and value (e.g., TEST_URL maps to https://example.com). These options are used to replace the matched dynamic text.

We use @faker-js/faker to generate mock data for the options:

const handleAddOption = () => {
  setOptions((prev) => [
    ...prev,
    {
      name: faker.finance.accountName().replace(/\s/g, "_").toUpperCase(),
      value: faker.internet.url(),
    },
  ]);
};
Enter fullscreen mode Exit fullscreen mode

By clicking a button, a new option is generated dynamically, providing a randomized environment variable name and URL.

Styles

We used Tailwind CSS to style the component, ensuring a clean and responsive UI. The following classes are applied:

.input-container {
  @apply h-10 relative;
}

.input-container input {
  @apply w-full h-full absolute inset-0 bg-transparent outline-none text-base z-10 px-2 rounded border-input;
}

.input-container .input-renderer {
  @apply absolute inset-0 mx-2 text-base text-foreground flex items-center whitespace-nowrap overflow-x-auto select-none;
}
Enter fullscreen mode Exit fullscreen mode

This ensures the input field has a consistent size, responsive styling, and custom behavior such as hiding the placeholder text when not empty.

Using the Component

Here’s how you can use the HighlightedInput component within an application:

import { faker } from "@faker-js/faker";
import { useState } from "react";
import { Star, Trash2 } from "lucide-react";
import HighlightedInput, { getOptionValue } from "@/components/highlight-input";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import CopyText from "@/components/copy-text";

function HighlightExample() {
  const [options, setOptions] = useState([
    {
      value: "https://jsonplaceholder.typicode.com/todos",
      name: "TEST_URL",
    },
  ]);

  const isUrl = (url: string): boolean => {
    try {
      new URL(getOptionValue(url, options));
      // new URL(url);
      return true;
    } catch {
      return false;
    }
  };

  const optionNameFormat = (name: string) =>
    name.replace(/\s/g, "_").toLocaleUpperCase();

  const handleAddOption = () => {
    setOptions((prev) => [
      ...prev,
      {
        name: optionNameFormat(faker.finance.accountName()),
        value: faker.internet.url(),
      },
    ]);
  };

  const handleDeleteOption = (index: number) => {
    setOptions((prev) => prev.filter((_, i) => i !== index));
  };

  const [url, setUrl] = useState("https://jsonplaceholder.typicode.com/todos");

  const resultUrl = isUrl(url) ? new URL(getOptionValue(url, options)) : "";

  const finalUrl = typeof resultUrl === "object" ? resultUrl.href : "";

  return (
    <div className="w-full">
      <div className="flex flex-row gap-4">
        <HighlightedInput
          onChange={(event) => setUrl(event.target.value)}
          options={options}
          value={url}
          className="mb-4"
        />
        <Button className="mb-4" onClick={handleAddOption}>
          Add Option
        </Button>
      </div>
      <article className="flex flex-row gap-x-2">
        <Label className="text-lg select-none">Result URL:</Label>
        <Label weight={"bold"} className="text-lg select-none">
          {finalUrl}
        </Label>
        <CopyText text={finalUrl} />
      </article>
      <article className="py-4">
        <Label className="text-lg">
          Options:{" "}
          <Label className="text-xs">
            (Click on the trash icon to delete an option)
          </Label>
        </Label>
      </article>
      <section
        hidden={options.length === 0}
        className="overflow-hidden border rounded-lg"
      >
        {options.map((option, index) => (
          <div
            key={index}
            className="flex flex-row items-center justify-center px-4 py-2 border-b gap-x-2 last:border-none hover:bg-primary-foreground hover:bg-opacity-10"
          >
            <Star
              size={16}
              className="text-primary fill-primary dark:text-muted-foreground dark:fill-muted-foreground"
            />
            <Label
              className="flex flex-row items-center select-none"
              weight={"bold"}
            >
              <CopyText text={option.name} />
              {option.name}:
            </Label>
            <Label className="select-none">{option.value}</Label>
            <Button
              variant={"ghost"}
              className="p-2 m-0 ml-auto mr-0 cursor-pointer text-destructive"
            >
              <Trash2 size={16} onClick={() => handleDeleteOption(index)} />
            </Button>
          </div>
        ))}
      </section>
    </div>
  );
}

export default HighlightExample;
Enter fullscreen mode Exit fullscreen mode

In this example, we define a Usage component that utilizes the HighlightedInput component. We manage the options, handle URL validation, and display the result URL based on the input.

Conclusion

In this article, we built a HighlightedInput component using React, TypeScript, and various other tools. This component allows users to highlight dynamic text within an input field, such as environment variables or URLs. We also explored key features like handling the input field, highlighting dynamic text, and managing options. By using @faker-js/faker, we generated mock data for testing purposes. This component can be a valuable addition to your projects, enhancing user experience and providing a clean interface for managing dynamic text inputs.

I hope you found this article helpful. Feel free to reach out with any questions or feedback. Happy coding!

Top comments (0)