Customization & Optimization

Customization & Optimization setup of tags input with shadecn and tailwind.

Error Handling

With the onError prop, you can handle errors when adding tags.

const handleError = (code: TagErrorCode) => {
  switch (code) {
    case "E01":
      console.log("E01: Tag already exists");
      break;
    case "E02":
      console.log("E02: Tag not found in suggestions");
      break;
  }
};

Client component

  1. Create a client component:
"use client";

import { useState } from "react";

import { Tag, TagErrorCode, TagsInput } from "@/components/ui/tags-input";

export default function Example() {
  const [selectedTags, setSelectedTags] = useState<Tag[]>([]);
  const suggestions: Tag[] = [
    { id: "1", name: "JavaScript" },
    { id: "2", name: "TypeScript" },
    { id: "3", name: "React" },
  ];

  const handleError = (code: TagErrorCode) => {
    switch (code) {
      case "E01":
        console.log("E01: Tag already exists");
        break;
      case "E02":
        console.log("E02: Tag not found in suggestions");
        break;
    }
  };

  return (
    <TagsInput
      value={selectedTags}
      onChange={setSelectedTags}
      suggestions={suggestions}
      onlyFromSuggestions={true}
      maxTags={3}
      onError={handleError}
      placeholder="add tag & press enter"
      className="bg-neutral-50 rounded-xl"
      inputClassName=""
      badgeClassName=""
      badgeCloseClassName="hover:bg-red-500"
      autocompleteClassName=""
    />
  );
}

Custom autocomplete

You can customize the component with tailwindcss classes.

In autocompleteClassName, you can use the following blocks:

  • div
  • ul
  • ul>li
// Default style in autocompleteClassName
<div
  className={cn(
    "absolute z-10 w-full mt-1 bg-popover border rounded-md shadow-md",
    autocompleteClassName,
    "[&>ul]:py-1",
    "[&>ul>li]:px-3 [&>ul>li]:py-2 [&>ul>li]:cursor-pointer"
  )}
/>

With zod

Define a schema

export const schemaTags = z.object({
  tags: z
    .array(
      z.object({
        id: z.string(),
        name: z.string(),
      })
    )
    .optional()
    .default([]),
});

Use the schema in a form

const defaultFormValuesTags = {
  tags: [],
};

const form = useForm<z.infer<typeof schemaTags>>({
  resolver: zodResolver(schemaTags),
  defaultValues: defaultFormValuesTags,
});

Use the form in a component

<FormField
  control={form.control}
  name="tags"
  render={({ field }) => (
    <TagsInput
      value={field.value}
      onChange={field.onChange}
      suggestions={suggestions}
      onlyFromSuggestions={true}
      placeholder="add tag"
    />
  )}
/>

Output the form

[Log] selectedTags – [{id: "1", name: "JavaScript"}] (1)
[Log] selectedTags – [{id: "1", name: "JavaScript"}, {id: "3", name: "React"}] (2)

Change placeholder

Change placeholder with form shadecn.

import React, { useState, useEffect } from "react";
import { Tag, TagsInput } from "@/components/ui/tags-input";
import { useForm } from "react-hook-form";

export default function ChangePlaceholder() {
  const form = useForm();
  const [maxTags, setMaxTags] = useState(3);
  const [placeholder, setPlaceholder] = useState("add tag");
  const suggestions: Tag[] = [
    { id: "1", name: "JavaScript" },
    { id: "2", name: "TypeScript" },
    { id: "3", name: "React" },
  ];
  useEffect(() => {
    // Update placeholder based on the number of tags
    const currentTags = form.watch("tags").length;
    if (currentTags === 0) {
      setPlaceholder(`add ${maxTags} max`);
    } else if (currentTags < maxTags) {
      setPlaceholder(`add more ${maxTags - currentTags} tags ...`);
    } else {
      setPlaceholder("no more tags");
    }
  }, [form.watch("tags"), maxTags]);

  return (
    <Form {...form}>
      <form
        onSubmit={form.handleSubmit(onSubmit)}
        className="flex flex-col space-y-6 text-left "
      >
        <FormField
          control={form.control}
          name="tags"
          render={({ field }) => (
            <TagsInput
              value={field.value}
              onChange={field.onChange}
              suggestions={suggestions}
              onlyFromSuggestions={false}
              maxTags={maxTags}
              placeholder={placeholder}
              className=""
              inputClassName=""
              badgeClassName=""
              badgeCloseClassName=""
              autocompleteClassName=""
            />
          )}
        />
      </form>
    </Form>
  );
}

Fetch suggestions

use Tag Input with a api routes for fetch suggestions.

version > 1.3.1

Added description for dynamic fetching This is an example of dynamic fetching on every change in the input field, which fetches data using UseSWR and refreshes the list of tags with mutate.

// route.ts with prisma
const searchTerm = req.nextUrl.searchParams.get("search") || "";
const tags = await db.tags.findMany({
  where: {
    name: {
      contains: searchTerm,
      mode: "insensitive",
    },
  },
  select: {
    id: true,
    name: true,
  },
});
return NextResponse.json(tags);

composant.tsx

const [searchTerm, setSearchTerm] = useState("");
const {
  data: suggestions,
  error: suggestionsError,
  isLoading: suggestionsLoading,
  mutate: mutateSuggestions,
} = useSWR<Tag[]>(`/api/link/tags/search?search=${searchTerm}`, fetcher);

// listen search tag key
useEffect(() => {
  if (searchTerm) {
    mutateSuggestions();
  }
}, [searchTerm, mutateSuggestions]);
<TagsInput
  value={field.value}
  onChange={field.onChange}
  suggestions={suggestions}
  onlyFromSuggestions={false}
  maxTags={maxTags}
  placeholder={placeholder}
  onInputChange={(value: string) => setSearchTerm(value)}
/>;